roblox-opencode 1.0.0 → 1.0.1
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/README.md +112 -122
- package/commands/setup-game.md +108 -108
- package/commands/sync-check.md +53 -53
- package/core/roblox-core.md +93 -93
- package/dist/server.js +189 -167
- package/package.json +35 -35
- package/skills/roblox-analytics/SKILL.md +277 -277
- package/skills/roblox-analytics/references/event-batcher.luau +75 -75
- package/skills/roblox-animation-vfx/SKILL.md +1325 -1325
- package/skills/roblox-architecture/SKILL.md +863 -863
- package/skills/roblox-architecture/references/combat-systems.md +1381 -1381
- package/skills/roblox-code-review/SKILL.md +686 -686
- package/skills/roblox-data/SKILL.md +889 -889
- package/skills/roblox-data/references/inventory-systems.md +1729 -1729
- package/skills/roblox-debug/SKILL.md +98 -98
- package/skills/roblox-gui/SKILL.md +1103 -1103
- package/skills/roblox-gui-fusion/SKILL.md +150 -150
- package/skills/roblox-gui-fusion/references/inventory.luau +427 -427
- package/skills/roblox-gui-fusion/references/settings-menu.luau +579 -579
- package/skills/roblox-gui-fusion/references/shop.luau +411 -411
- package/skills/roblox-luau-mastery/SKILL.md +1519 -1519
- package/skills/roblox-monetization/SKILL.md +1084 -1084
- package/skills/roblox-monetization/references/process-receipt.luau +131 -131
- package/skills/roblox-networking/SKILL.md +669 -669
- package/skills/roblox-networking/references/remote-validator.luau +193 -193
- package/skills/roblox-publish-checklist/SKILL.md +127 -127
- package/skills/roblox-runtime/SKILL.md +753 -753
- package/skills/roblox-sharp-edges/SKILL.md +294 -294
- package/skills/roblox-sync/SKILL.md +126 -126
- package/skills/roblox-testing/SKILL.md +943 -943
- package/skills/roblox-tooling/SKILL.md +149 -149
- package/vendor/LICENSES/ProfileStore-LICENSE +201 -201
- package/vendor/LICENSES/RbxUtil-LICENSE +7 -7
- package/vendor/LICENSES/promise-LICENSE +20 -20
- package/vendor/LICENSES/t-LICENSE +21 -21
- package/vendor/LICENSES/testez-LICENSE +200 -200
- package/vendor/README.md +83 -83
- package/vendor/fusion/Animation/ExternalTime.luau +83 -83
- package/vendor/fusion/Animation/Spring.luau +321 -321
- package/vendor/fusion/Animation/Stopwatch.luau +127 -127
- package/vendor/fusion/Animation/Tween.luau +187 -187
- package/vendor/fusion/Animation/getTweenDuration.luau +27 -27
- package/vendor/fusion/Animation/getTweenRatio.luau +47 -47
- package/vendor/fusion/Animation/lerpType.luau +163 -163
- package/vendor/fusion/Animation/packType.luau +99 -99
- package/vendor/fusion/Animation/springCoefficients.luau +80 -80
- package/vendor/fusion/Animation/unpackType.luau +102 -102
- package/vendor/fusion/Colour/Oklab.luau +70 -70
- package/vendor/fusion/Colour/sRGB.luau +54 -54
- package/vendor/fusion/External.luau +167 -167
- package/vendor/fusion/ExternalDebug.luau +69 -69
- package/vendor/fusion/Graph/Observer.luau +113 -113
- package/vendor/fusion/Graph/castToGraph.luau +28 -28
- package/vendor/fusion/Graph/change.luau +80 -80
- package/vendor/fusion/Graph/depend.luau +32 -32
- package/vendor/fusion/Graph/evaluate.luau +55 -55
- package/vendor/fusion/Instances/Attribute.luau +57 -57
- package/vendor/fusion/Instances/AttributeChange.luau +46 -46
- package/vendor/fusion/Instances/AttributeOut.luau +63 -63
- package/vendor/fusion/Instances/Child.luau +21 -21
- package/vendor/fusion/Instances/Children.luau +147 -147
- package/vendor/fusion/Instances/Hydrate.luau +32 -32
- package/vendor/fusion/Instances/New.luau +52 -52
- package/vendor/fusion/Instances/OnChange.luau +49 -49
- package/vendor/fusion/Instances/OnEvent.luau +53 -53
- package/vendor/fusion/Instances/Out.luau +69 -69
- package/vendor/fusion/Instances/applyInstanceProps.luau +148 -148
- package/vendor/fusion/Instances/defaultProps.luau +194 -194
- package/vendor/fusion/LICENSE +21 -21
- package/vendor/fusion/Logging/formatError.luau +48 -48
- package/vendor/fusion/Logging/messages.luau +51 -51
- package/vendor/fusion/Logging/parseError.luau +24 -24
- package/vendor/fusion/Memory/checkLifetime.luau +133 -133
- package/vendor/fusion/Memory/deriveScope.luau +23 -23
- package/vendor/fusion/Memory/deriveScopeImpl.luau +44 -44
- package/vendor/fusion/Memory/doCleanup.luau +78 -78
- package/vendor/fusion/Memory/innerScope.luau +33 -33
- package/vendor/fusion/Memory/legacyCleanup.luau +17 -17
- package/vendor/fusion/Memory/needsDestruction.luau +16 -16
- package/vendor/fusion/Memory/poisonScope.luau +33 -33
- package/vendor/fusion/Memory/scopePool.luau +54 -54
- package/vendor/fusion/Memory/scoped.luau +26 -26
- package/vendor/fusion/Memory/whichLivesLonger.luau +74 -74
- package/vendor/fusion/RobloxExternal.luau +97 -97
- package/vendor/fusion/State/Computed.luau +138 -138
- package/vendor/fusion/State/For/Disassembly.luau +210 -210
- package/vendor/fusion/State/For/ForTypes.luau +30 -30
- package/vendor/fusion/State/For/init.luau +109 -109
- package/vendor/fusion/State/ForKeys.luau +93 -93
- package/vendor/fusion/State/ForPairs.luau +96 -96
- package/vendor/fusion/State/ForValues.luau +93 -93
- package/vendor/fusion/State/Value.luau +87 -87
- package/vendor/fusion/State/castToState.luau +25 -25
- package/vendor/fusion/State/peek.luau +30 -30
- package/vendor/fusion/Types.luau +314 -314
- package/vendor/fusion/Utility/Contextual.luau +90 -90
- package/vendor/fusion/Utility/Safe.luau +22 -22
- package/vendor/fusion/Utility/isSimilar.luau +29 -29
- package/vendor/fusion/Utility/merge.luau +35 -35
- package/vendor/fusion/Utility/nameOf.luau +34 -34
- package/vendor/fusion/Utility/never.luau +13 -13
- package/vendor/fusion/Utility/nicknames.luau +10 -10
- package/vendor/fusion/Utility/xtypeof.luau +26 -26
- package/vendor/fusion/init.luau +82 -82
- package/vendor/profilestore/init.luau +2242 -2242
- package/vendor/promise/init.luau +1982 -1982
- package/vendor/rbxutil/buffer-util/Buffer.test.luau +25 -25
- package/vendor/rbxutil/buffer-util/BufferReader.luau +228 -228
- package/vendor/rbxutil/buffer-util/BufferWriter.luau +269 -269
- package/vendor/rbxutil/buffer-util/DataTypeBuffer.luau +223 -223
- package/vendor/rbxutil/buffer-util/Types.luau +60 -60
- package/vendor/rbxutil/buffer-util/index.d.ts +153 -153
- package/vendor/rbxutil/buffer-util/init.luau +41 -41
- package/vendor/rbxutil/buffer-util/package.json +16 -16
- package/vendor/rbxutil/buffer-util/wally.toml +9 -9
- package/vendor/rbxutil/comm/Client/ClientComm.luau +232 -232
- package/vendor/rbxutil/comm/Client/ClientRemoteProperty.luau +156 -156
- package/vendor/rbxutil/comm/Client/ClientRemoteSignal.luau +109 -109
- package/vendor/rbxutil/comm/Client/init.luau +135 -135
- package/vendor/rbxutil/comm/Server/RemoteProperty.luau +295 -295
- package/vendor/rbxutil/comm/Server/RemoteSignal.luau +211 -211
- package/vendor/rbxutil/comm/Server/ServerComm.luau +211 -211
- package/vendor/rbxutil/comm/Server/init.luau +140 -140
- package/vendor/rbxutil/comm/Types.luau +18 -18
- package/vendor/rbxutil/comm/Util.luau +27 -27
- package/vendor/rbxutil/comm/init.luau +35 -35
- package/vendor/rbxutil/comm/wally.toml +13 -13
- package/vendor/rbxutil/component/init.luau +759 -759
- package/vendor/rbxutil/component/init.test.luau +311 -311
- package/vendor/rbxutil/component/wally.toml +14 -14
- package/vendor/rbxutil/concur/init.luau +542 -542
- package/vendor/rbxutil/concur/init.test.luau +364 -364
- package/vendor/rbxutil/concur/wally.toml +8 -8
- package/vendor/rbxutil/enum-list/init.luau +101 -101
- package/vendor/rbxutil/enum-list/init.test.luau +91 -91
- package/vendor/rbxutil/enum-list/wally.toml +8 -8
- package/vendor/rbxutil/find/index.d.ts +20 -20
- package/vendor/rbxutil/find/init.luau +44 -44
- package/vendor/rbxutil/find/package.json +17 -17
- package/vendor/rbxutil/find/wally.toml +8 -8
- package/vendor/rbxutil/input/Gamepad.luau +559 -559
- package/vendor/rbxutil/input/Keyboard.luau +124 -124
- package/vendor/rbxutil/input/Mouse.luau +278 -278
- package/vendor/rbxutil/input/PreferredInput.luau +91 -91
- package/vendor/rbxutil/input/Touch.luau +120 -120
- package/vendor/rbxutil/input/init.luau +33 -33
- package/vendor/rbxutil/input/wally.toml +12 -12
- package/vendor/rbxutil/loader/index.d.ts +15 -15
- package/vendor/rbxutil/loader/init.luau +137 -137
- package/vendor/rbxutil/loader/wally.toml +8 -8
- package/vendor/rbxutil/log/index.d.ts +38 -38
- package/vendor/rbxutil/log/init.luau +746 -746
- package/vendor/rbxutil/log/wally.toml +8 -8
- package/vendor/rbxutil/net/init.luau +190 -190
- package/vendor/rbxutil/net/wally.toml +8 -8
- package/vendor/rbxutil/option/index.d.ts +44 -44
- package/vendor/rbxutil/option/init.luau +489 -489
- package/vendor/rbxutil/option/init.test.luau +342 -342
- package/vendor/rbxutil/option/wally.toml +8 -8
- package/vendor/rbxutil/pid/index.d.ts +53 -53
- package/vendor/rbxutil/pid/init.luau +195 -195
- package/vendor/rbxutil/pid/package.json +16 -16
- package/vendor/rbxutil/pid/wally.toml +9 -9
- package/vendor/rbxutil/quaternion/index.d.ts +117 -117
- package/vendor/rbxutil/quaternion/init.luau +570 -570
- package/vendor/rbxutil/quaternion/package.json +16 -16
- package/vendor/rbxutil/quaternion/wally.toml +9 -9
- package/vendor/rbxutil/query/index.d.ts +43 -43
- package/vendor/rbxutil/query/init.luau +117 -117
- package/vendor/rbxutil/query/package.json +18 -18
- package/vendor/rbxutil/query/wally.toml +9 -9
- package/vendor/rbxutil/sequent/index.d.ts +28 -28
- package/vendor/rbxutil/sequent/init.luau +340 -340
- package/vendor/rbxutil/sequent/package.json +16 -16
- package/vendor/rbxutil/sequent/wally.toml +9 -9
- package/vendor/rbxutil/ser/init.luau +175 -175
- package/vendor/rbxutil/ser/init.test.luau +50 -50
- package/vendor/rbxutil/ser/wally.toml +11 -11
- package/vendor/rbxutil/shake/index.d.ts +36 -36
- package/vendor/rbxutil/shake/init.luau +532 -532
- package/vendor/rbxutil/shake/init.test.luau +267 -267
- package/vendor/rbxutil/shake/package.json +16 -16
- package/vendor/rbxutil/shake/wally.toml +9 -9
- package/vendor/rbxutil/signal/index.d.ts +100 -100
- package/vendor/rbxutil/signal/init.luau +432 -432
- package/vendor/rbxutil/signal/init.test.luau +190 -190
- package/vendor/rbxutil/signal/package.json +17 -17
- package/vendor/rbxutil/signal/wally.toml +9 -9
- package/vendor/rbxutil/silo/TableWatcher.luau +65 -65
- package/vendor/rbxutil/silo/Util.luau +55 -55
- package/vendor/rbxutil/silo/init.luau +338 -338
- package/vendor/rbxutil/silo/init.test.luau +215 -215
- package/vendor/rbxutil/silo/wally.toml +8 -8
- package/vendor/rbxutil/spring/index.d.ts +40 -40
- package/vendor/rbxutil/spring/init.luau +97 -97
- package/vendor/rbxutil/spring/package.json +17 -17
- package/vendor/rbxutil/spring/wally.toml +8 -8
- package/vendor/rbxutil/stream/index.d.ts +88 -88
- package/vendor/rbxutil/stream/init.luau +597 -597
- package/vendor/rbxutil/stream/package.json +18 -18
- package/vendor/rbxutil/stream/wally.toml +9 -9
- package/vendor/rbxutil/streamable/Streamable.luau +202 -202
- package/vendor/rbxutil/streamable/StreamableUtil.luau +80 -80
- package/vendor/rbxutil/streamable/init.luau +8 -8
- package/vendor/rbxutil/streamable/wally.toml +12 -12
- package/vendor/rbxutil/symbol/init.luau +56 -56
- package/vendor/rbxutil/symbol/init.test.luau +37 -37
- package/vendor/rbxutil/symbol/wally.toml +8 -8
- package/vendor/rbxutil/table-util/init.luau +938 -938
- package/vendor/rbxutil/table-util/init.test.luau +439 -439
- package/vendor/rbxutil/task-queue/index.d.ts +27 -27
- package/vendor/rbxutil/task-queue/init.luau +97 -97
- package/vendor/rbxutil/task-queue/wally.toml +8 -8
- package/vendor/rbxutil/timer/index.d.ts +81 -81
- package/vendor/rbxutil/timer/init.luau +249 -249
- package/vendor/rbxutil/timer/init.test.luau +73 -73
- package/vendor/rbxutil/timer/wally.toml +11 -11
- package/vendor/rbxutil/tree/index.d.ts +15 -15
- package/vendor/rbxutil/tree/init.luau +137 -137
- package/vendor/rbxutil/tree/wally.toml +8 -8
- package/vendor/rbxutil/trove/index.d.ts +46 -46
- package/vendor/rbxutil/trove/init.luau +787 -787
- package/vendor/rbxutil/trove/init.test.luau +203 -203
- package/vendor/rbxutil/trove/wally.toml +8 -8
- package/vendor/rbxutil/typed-remote/init.luau +196 -196
- package/vendor/rbxutil/typed-remote/wally.toml +8 -8
- package/vendor/rbxutil/wait-for/index.d.ts +17 -17
- package/vendor/rbxutil/wait-for/init.luau +257 -257
- package/vendor/rbxutil/wait-for/init.test.luau +182 -182
- package/vendor/rbxutil/wait-for/wally.toml +11 -11
- package/vendor/t/t.lua +1350 -1350
- package/vendor/testez/Context.lua +26 -26
- package/vendor/testez/Expectation.lua +311 -311
- package/vendor/testez/ExpectationContext.lua +38 -38
- package/vendor/testez/LifecycleHooks.lua +89 -89
- package/vendor/testez/Reporters/TeamCityReporter.lua +101 -101
- package/vendor/testez/Reporters/TextReporter.lua +105 -105
- package/vendor/testez/Reporters/TextReporterQuiet.lua +96 -96
- package/vendor/testez/TestBootstrap.lua +146 -146
- package/vendor/testez/TestEnum.lua +27 -27
- package/vendor/testez/TestPlan.lua +304 -304
- package/vendor/testez/TestPlanner.lua +39 -39
- package/vendor/testez/TestResults.lua +111 -111
- package/vendor/testez/TestRunner.lua +188 -188
- package/vendor/testez/TestSession.lua +243 -243
- package/vendor/testez/init.lua +39 -39
|
@@ -1,131 +1,131 @@
|
|
|
1
|
-
--[[
|
|
2
|
-
ProcessReceipt + ProfileStore Integration
|
|
3
|
-
Source: madstudioroblox.github.io/ProfileStore/devproducts/ (Apache 2.0)
|
|
4
|
-
|
|
5
|
-
This is the robust "PurchaseId caching" approach that yields the ProcessReceipt
|
|
6
|
-
callback until the purchase is confirmed saved to DataStore. Prevents item loss
|
|
7
|
-
on server crashes.
|
|
8
|
-
|
|
9
|
-
ADAPT THIS: Replace ProductFunctions entries with your actual products.
|
|
10
|
-
The PurchaseIdCheckAsync and processReceipt functions are copy-paste ready.
|
|
11
|
-
]]
|
|
12
|
-
|
|
13
|
-
local MarketplaceService = game:GetService("MarketplaceService")
|
|
14
|
-
local Players = game:GetService("Players")
|
|
15
|
-
|
|
16
|
-
-- Reference to your loaded profiles table (from your ProfileStore setup)
|
|
17
|
-
local Profiles: {[Player]: typeof(PlayerStore:StartSessionAsync())} = {}
|
|
18
|
-
|
|
19
|
-
-- How many purchase IDs to cache per player (FIFO eviction)
|
|
20
|
-
local PURCHASE_ID_CACHE_SIZE = 100
|
|
21
|
-
|
|
22
|
-
-------------------------------------------------------------------------------
|
|
23
|
-
-- Product handlers: one function per Developer Product ID
|
|
24
|
-
-- Each receives (receiptInfo, player, profile). Mutate profile.Data directly.
|
|
25
|
-
-- Do NOT call profile:Save() here — PurchaseIdCheckAsync handles persistence.
|
|
26
|
-
-------------------------------------------------------------------------------
|
|
27
|
-
|
|
28
|
-
local ProductFunctions = {}
|
|
29
|
-
|
|
30
|
-
ProductFunctions[456456] = function(_receipt, _player, profile)
|
|
31
|
-
profile.Data.Cash += 100
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
ProductFunctions[789789] = function(_receipt, _player, profile)
|
|
35
|
-
table.insert(profile.Data.Inventory, "SpeedBoost")
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
-------------------------------------------------------------------------------
|
|
39
|
-
-- PurchaseIdCheckAsync: yields until purchase is confirmed saved to DataStore.
|
|
40
|
-
-- Returns PurchaseGranted only after DataStore persistence is verified.
|
|
41
|
-
-- Returns NotProcessedYet if profile releases before save confirms.
|
|
42
|
-
-------------------------------------------------------------------------------
|
|
43
|
-
|
|
44
|
-
function PurchaseIdCheckAsync(
|
|
45
|
-
profile,
|
|
46
|
-
purchase_id: string,
|
|
47
|
-
grant_product: () -> ()
|
|
48
|
-
): Enum.ProductPurchaseDecision
|
|
49
|
-
|
|
50
|
-
if profile:IsActive() ~= true then
|
|
51
|
-
return Enum.ProductPurchaseDecision.NotProcessedYet
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
local purchase_id_cache = profile.Data.PurchaseIdCache
|
|
55
|
-
if purchase_id_cache == nil then
|
|
56
|
-
purchase_id_cache = {}
|
|
57
|
-
profile.Data.PurchaseIdCache = purchase_id_cache
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
-- Already granted in a previous session (idempotency check)
|
|
61
|
-
if table.find(purchase_id_cache, purchase_id) ~= nil then
|
|
62
|
-
return Enum.ProductPurchaseDecision.PurchaseGranted
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
-- Grant the product and record the PurchaseId atomically
|
|
66
|
-
local success, result = pcall(grant_product)
|
|
67
|
-
if success ~= true then
|
|
68
|
-
warn(`[ProcessReceipt] Grant failed: {result}`)
|
|
69
|
-
return Enum.ProductPurchaseDecision.NotProcessedYet
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
-- FIFO eviction: keep cache bounded
|
|
73
|
-
while #purchase_id_cache >= PURCHASE_ID_CACHE_SIZE do
|
|
74
|
-
table.remove(purchase_id_cache, 1)
|
|
75
|
-
end
|
|
76
|
-
table.insert(purchase_id_cache, purchase_id)
|
|
77
|
-
|
|
78
|
-
-- Yield until we confirm the PurchaseId was saved to DataStore
|
|
79
|
-
-- profile.LastSavedData updates after each successful DataStore write
|
|
80
|
-
while profile:IsActive() == true do
|
|
81
|
-
local last_saved_cache = profile.LastSavedData
|
|
82
|
-
and profile.LastSavedData.PurchaseIdCache
|
|
83
|
-
|
|
84
|
-
if last_saved_cache and table.find(last_saved_cache, purchase_id) then
|
|
85
|
-
return Enum.ProductPurchaseDecision.PurchaseGranted
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
-- Wait for next save cycle
|
|
89
|
-
profile.OnAfterSave:Wait()
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
return Enum.ProductPurchaseDecision.NotProcessedYet
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
-------------------------------------------------------------------------------
|
|
96
|
-
-- processReceipt: the single callback assigned to MarketplaceService.
|
|
97
|
-
-- Waits for profile to load, then delegates to PurchaseIdCheckAsync.
|
|
98
|
-
-------------------------------------------------------------------------------
|
|
99
|
-
|
|
100
|
-
local function processReceipt(receiptInfo): Enum.ProductPurchaseDecision
|
|
101
|
-
local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
|
|
102
|
-
if not player then
|
|
103
|
-
return Enum.ProductPurchaseDecision.NotProcessedYet
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
-- Wait for profile to load (player may have just joined)
|
|
107
|
-
local profile = Profiles[player]
|
|
108
|
-
while profile == nil and player:IsDescendantOf(Players) do
|
|
109
|
-
task.wait()
|
|
110
|
-
profile = Profiles[player]
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
if profile == nil then
|
|
114
|
-
return Enum.ProductPurchaseDecision.NotProcessedYet
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
-- Look up the handler for this product
|
|
118
|
-
local handler = ProductFunctions[receiptInfo.ProductId]
|
|
119
|
-
if handler == nil then
|
|
120
|
-
warn(`[ProcessReceipt] No handler for ProductId {receiptInfo.ProductId}`)
|
|
121
|
-
return Enum.ProductPurchaseDecision.NotProcessedYet
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
-- Yield until purchase is confirmed saved
|
|
125
|
-
return PurchaseIdCheckAsync(profile, receiptInfo.PurchaseId, function()
|
|
126
|
-
handler(receiptInfo, player, profile)
|
|
127
|
-
end)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
-- Assign the callback (only ONE script may do this)
|
|
131
|
-
MarketplaceService.ProcessReceipt = processReceipt
|
|
1
|
+
--[[
|
|
2
|
+
ProcessReceipt + ProfileStore Integration
|
|
3
|
+
Source: madstudioroblox.github.io/ProfileStore/devproducts/ (Apache 2.0)
|
|
4
|
+
|
|
5
|
+
This is the robust "PurchaseId caching" approach that yields the ProcessReceipt
|
|
6
|
+
callback until the purchase is confirmed saved to DataStore. Prevents item loss
|
|
7
|
+
on server crashes.
|
|
8
|
+
|
|
9
|
+
ADAPT THIS: Replace ProductFunctions entries with your actual products.
|
|
10
|
+
The PurchaseIdCheckAsync and processReceipt functions are copy-paste ready.
|
|
11
|
+
]]
|
|
12
|
+
|
|
13
|
+
local MarketplaceService = game:GetService("MarketplaceService")
|
|
14
|
+
local Players = game:GetService("Players")
|
|
15
|
+
|
|
16
|
+
-- Reference to your loaded profiles table (from your ProfileStore setup)
|
|
17
|
+
local Profiles: {[Player]: typeof(PlayerStore:StartSessionAsync())} = {}
|
|
18
|
+
|
|
19
|
+
-- How many purchase IDs to cache per player (FIFO eviction)
|
|
20
|
+
local PURCHASE_ID_CACHE_SIZE = 100
|
|
21
|
+
|
|
22
|
+
-------------------------------------------------------------------------------
|
|
23
|
+
-- Product handlers: one function per Developer Product ID
|
|
24
|
+
-- Each receives (receiptInfo, player, profile). Mutate profile.Data directly.
|
|
25
|
+
-- Do NOT call profile:Save() here — PurchaseIdCheckAsync handles persistence.
|
|
26
|
+
-------------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
local ProductFunctions = {}
|
|
29
|
+
|
|
30
|
+
ProductFunctions[456456] = function(_receipt, _player, profile)
|
|
31
|
+
profile.Data.Cash += 100
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
ProductFunctions[789789] = function(_receipt, _player, profile)
|
|
35
|
+
table.insert(profile.Data.Inventory, "SpeedBoost")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
-------------------------------------------------------------------------------
|
|
39
|
+
-- PurchaseIdCheckAsync: yields until purchase is confirmed saved to DataStore.
|
|
40
|
+
-- Returns PurchaseGranted only after DataStore persistence is verified.
|
|
41
|
+
-- Returns NotProcessedYet if profile releases before save confirms.
|
|
42
|
+
-------------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
function PurchaseIdCheckAsync(
|
|
45
|
+
profile,
|
|
46
|
+
purchase_id: string,
|
|
47
|
+
grant_product: () -> ()
|
|
48
|
+
): Enum.ProductPurchaseDecision
|
|
49
|
+
|
|
50
|
+
if profile:IsActive() ~= true then
|
|
51
|
+
return Enum.ProductPurchaseDecision.NotProcessedYet
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
local purchase_id_cache = profile.Data.PurchaseIdCache
|
|
55
|
+
if purchase_id_cache == nil then
|
|
56
|
+
purchase_id_cache = {}
|
|
57
|
+
profile.Data.PurchaseIdCache = purchase_id_cache
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
-- Already granted in a previous session (idempotency check)
|
|
61
|
+
if table.find(purchase_id_cache, purchase_id) ~= nil then
|
|
62
|
+
return Enum.ProductPurchaseDecision.PurchaseGranted
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
-- Grant the product and record the PurchaseId atomically
|
|
66
|
+
local success, result = pcall(grant_product)
|
|
67
|
+
if success ~= true then
|
|
68
|
+
warn(`[ProcessReceipt] Grant failed: {result}`)
|
|
69
|
+
return Enum.ProductPurchaseDecision.NotProcessedYet
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
-- FIFO eviction: keep cache bounded
|
|
73
|
+
while #purchase_id_cache >= PURCHASE_ID_CACHE_SIZE do
|
|
74
|
+
table.remove(purchase_id_cache, 1)
|
|
75
|
+
end
|
|
76
|
+
table.insert(purchase_id_cache, purchase_id)
|
|
77
|
+
|
|
78
|
+
-- Yield until we confirm the PurchaseId was saved to DataStore
|
|
79
|
+
-- profile.LastSavedData updates after each successful DataStore write
|
|
80
|
+
while profile:IsActive() == true do
|
|
81
|
+
local last_saved_cache = profile.LastSavedData
|
|
82
|
+
and profile.LastSavedData.PurchaseIdCache
|
|
83
|
+
|
|
84
|
+
if last_saved_cache and table.find(last_saved_cache, purchase_id) then
|
|
85
|
+
return Enum.ProductPurchaseDecision.PurchaseGranted
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
-- Wait for next save cycle
|
|
89
|
+
profile.OnAfterSave:Wait()
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
return Enum.ProductPurchaseDecision.NotProcessedYet
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
-------------------------------------------------------------------------------
|
|
96
|
+
-- processReceipt: the single callback assigned to MarketplaceService.
|
|
97
|
+
-- Waits for profile to load, then delegates to PurchaseIdCheckAsync.
|
|
98
|
+
-------------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
local function processReceipt(receiptInfo): Enum.ProductPurchaseDecision
|
|
101
|
+
local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
|
|
102
|
+
if not player then
|
|
103
|
+
return Enum.ProductPurchaseDecision.NotProcessedYet
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
-- Wait for profile to load (player may have just joined)
|
|
107
|
+
local profile = Profiles[player]
|
|
108
|
+
while profile == nil and player:IsDescendantOf(Players) do
|
|
109
|
+
task.wait()
|
|
110
|
+
profile = Profiles[player]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
if profile == nil then
|
|
114
|
+
return Enum.ProductPurchaseDecision.NotProcessedYet
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
-- Look up the handler for this product
|
|
118
|
+
local handler = ProductFunctions[receiptInfo.ProductId]
|
|
119
|
+
if handler == nil then
|
|
120
|
+
warn(`[ProcessReceipt] No handler for ProductId {receiptInfo.ProductId}`)
|
|
121
|
+
return Enum.ProductPurchaseDecision.NotProcessedYet
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
-- Yield until purchase is confirmed saved
|
|
125
|
+
return PurchaseIdCheckAsync(profile, receiptInfo.PurchaseId, function()
|
|
126
|
+
handler(receiptInfo, player, profile)
|
|
127
|
+
end)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
-- Assign the callback (only ONE script may do this)
|
|
131
|
+
MarketplaceService.ProcessReceipt = processReceipt
|