roblox-opencode 1.0.0 → 1.0.2

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.
Files changed (246) hide show
  1. package/README.md +112 -122
  2. package/commands/setup-game.md +108 -108
  3. package/commands/sync-check.md +53 -53
  4. package/core/roblox-core.md +93 -93
  5. package/dist/server.js +189 -167
  6. package/package.json +35 -35
  7. package/skills/roblox-analytics/SKILL.md +277 -277
  8. package/skills/roblox-analytics/references/event-batcher.luau +75 -75
  9. package/skills/roblox-animation-vfx/SKILL.md +1325 -1325
  10. package/skills/roblox-architecture/SKILL.md +877 -863
  11. package/skills/roblox-architecture/references/combat-systems.md +1381 -1381
  12. package/skills/roblox-code-review/SKILL.md +686 -686
  13. package/skills/roblox-data/SKILL.md +889 -889
  14. package/skills/roblox-data/references/inventory-systems.md +1729 -1729
  15. package/skills/roblox-debug/SKILL.md +98 -98
  16. package/skills/roblox-gui/SKILL.md +1103 -1103
  17. package/skills/roblox-gui-fusion/SKILL.md +150 -150
  18. package/skills/roblox-gui-fusion/references/inventory.luau +427 -427
  19. package/skills/roblox-gui-fusion/references/settings-menu.luau +579 -579
  20. package/skills/roblox-gui-fusion/references/shop.luau +411 -411
  21. package/skills/roblox-luau-mastery/SKILL.md +1618 -1519
  22. package/skills/roblox-monetization/SKILL.md +1084 -1084
  23. package/skills/roblox-monetization/references/process-receipt.luau +131 -131
  24. package/skills/roblox-networking/SKILL.md +669 -669
  25. package/skills/roblox-networking/references/remote-validator.luau +193 -193
  26. package/skills/roblox-publish-checklist/SKILL.md +127 -127
  27. package/skills/roblox-runtime/SKILL.md +753 -753
  28. package/skills/roblox-sharp-edges/SKILL.md +294 -294
  29. package/skills/roblox-sync/SKILL.md +126 -126
  30. package/skills/roblox-testing/SKILL.md +943 -943
  31. package/skills/roblox-tooling/SKILL.md +149 -149
  32. package/vendor/LICENSES/ProfileStore-LICENSE +201 -201
  33. package/vendor/LICENSES/RbxUtil-LICENSE +7 -7
  34. package/vendor/LICENSES/promise-LICENSE +20 -20
  35. package/vendor/LICENSES/t-LICENSE +21 -21
  36. package/vendor/LICENSES/testez-LICENSE +200 -200
  37. package/vendor/README.md +83 -83
  38. package/vendor/fusion/Animation/ExternalTime.luau +83 -83
  39. package/vendor/fusion/Animation/Spring.luau +321 -321
  40. package/vendor/fusion/Animation/Stopwatch.luau +127 -127
  41. package/vendor/fusion/Animation/Tween.luau +187 -187
  42. package/vendor/fusion/Animation/getTweenDuration.luau +27 -27
  43. package/vendor/fusion/Animation/getTweenRatio.luau +47 -47
  44. package/vendor/fusion/Animation/lerpType.luau +163 -163
  45. package/vendor/fusion/Animation/packType.luau +99 -99
  46. package/vendor/fusion/Animation/springCoefficients.luau +80 -80
  47. package/vendor/fusion/Animation/unpackType.luau +102 -102
  48. package/vendor/fusion/Colour/Oklab.luau +70 -70
  49. package/vendor/fusion/Colour/sRGB.luau +54 -54
  50. package/vendor/fusion/External.luau +167 -167
  51. package/vendor/fusion/ExternalDebug.luau +69 -69
  52. package/vendor/fusion/Graph/Observer.luau +113 -113
  53. package/vendor/fusion/Graph/castToGraph.luau +28 -28
  54. package/vendor/fusion/Graph/change.luau +80 -80
  55. package/vendor/fusion/Graph/depend.luau +32 -32
  56. package/vendor/fusion/Graph/evaluate.luau +55 -55
  57. package/vendor/fusion/Instances/Attribute.luau +57 -57
  58. package/vendor/fusion/Instances/AttributeChange.luau +46 -46
  59. package/vendor/fusion/Instances/AttributeOut.luau +63 -63
  60. package/vendor/fusion/Instances/Child.luau +21 -21
  61. package/vendor/fusion/Instances/Children.luau +147 -147
  62. package/vendor/fusion/Instances/Hydrate.luau +32 -32
  63. package/vendor/fusion/Instances/New.luau +52 -52
  64. package/vendor/fusion/Instances/OnChange.luau +49 -49
  65. package/vendor/fusion/Instances/OnEvent.luau +53 -53
  66. package/vendor/fusion/Instances/Out.luau +69 -69
  67. package/vendor/fusion/Instances/applyInstanceProps.luau +148 -148
  68. package/vendor/fusion/Instances/defaultProps.luau +194 -194
  69. package/vendor/fusion/LICENSE +21 -21
  70. package/vendor/fusion/Logging/formatError.luau +48 -48
  71. package/vendor/fusion/Logging/messages.luau +51 -51
  72. package/vendor/fusion/Logging/parseError.luau +24 -24
  73. package/vendor/fusion/Memory/checkLifetime.luau +133 -133
  74. package/vendor/fusion/Memory/deriveScope.luau +23 -23
  75. package/vendor/fusion/Memory/deriveScopeImpl.luau +44 -44
  76. package/vendor/fusion/Memory/doCleanup.luau +78 -78
  77. package/vendor/fusion/Memory/innerScope.luau +33 -33
  78. package/vendor/fusion/Memory/legacyCleanup.luau +17 -17
  79. package/vendor/fusion/Memory/needsDestruction.luau +16 -16
  80. package/vendor/fusion/Memory/poisonScope.luau +33 -33
  81. package/vendor/fusion/Memory/scopePool.luau +54 -54
  82. package/vendor/fusion/Memory/scoped.luau +26 -26
  83. package/vendor/fusion/Memory/whichLivesLonger.luau +74 -74
  84. package/vendor/fusion/RobloxExternal.luau +97 -97
  85. package/vendor/fusion/State/Computed.luau +138 -138
  86. package/vendor/fusion/State/For/Disassembly.luau +210 -210
  87. package/vendor/fusion/State/For/ForTypes.luau +30 -30
  88. package/vendor/fusion/State/For/init.luau +109 -109
  89. package/vendor/fusion/State/ForKeys.luau +93 -93
  90. package/vendor/fusion/State/ForPairs.luau +96 -96
  91. package/vendor/fusion/State/ForValues.luau +93 -93
  92. package/vendor/fusion/State/Value.luau +87 -87
  93. package/vendor/fusion/State/castToState.luau +25 -25
  94. package/vendor/fusion/State/peek.luau +30 -30
  95. package/vendor/fusion/Types.luau +314 -314
  96. package/vendor/fusion/Utility/Contextual.luau +90 -90
  97. package/vendor/fusion/Utility/Safe.luau +22 -22
  98. package/vendor/fusion/Utility/isSimilar.luau +29 -29
  99. package/vendor/fusion/Utility/merge.luau +35 -35
  100. package/vendor/fusion/Utility/nameOf.luau +34 -34
  101. package/vendor/fusion/Utility/never.luau +13 -13
  102. package/vendor/fusion/Utility/nicknames.luau +10 -10
  103. package/vendor/fusion/Utility/xtypeof.luau +26 -26
  104. package/vendor/fusion/init.luau +82 -82
  105. package/vendor/profilestore/init.luau +2242 -2242
  106. package/vendor/promise/init.luau +1982 -1982
  107. package/vendor/rbxutil/buffer-util/Buffer.test.luau +25 -25
  108. package/vendor/rbxutil/buffer-util/BufferReader.luau +228 -228
  109. package/vendor/rbxutil/buffer-util/BufferWriter.luau +269 -269
  110. package/vendor/rbxutil/buffer-util/DataTypeBuffer.luau +223 -223
  111. package/vendor/rbxutil/buffer-util/Types.luau +60 -60
  112. package/vendor/rbxutil/buffer-util/index.d.ts +153 -153
  113. package/vendor/rbxutil/buffer-util/init.luau +41 -41
  114. package/vendor/rbxutil/buffer-util/package.json +16 -16
  115. package/vendor/rbxutil/buffer-util/wally.toml +9 -9
  116. package/vendor/rbxutil/comm/Client/ClientComm.luau +232 -232
  117. package/vendor/rbxutil/comm/Client/ClientRemoteProperty.luau +156 -156
  118. package/vendor/rbxutil/comm/Client/ClientRemoteSignal.luau +109 -109
  119. package/vendor/rbxutil/comm/Client/init.luau +135 -135
  120. package/vendor/rbxutil/comm/Server/RemoteProperty.luau +295 -295
  121. package/vendor/rbxutil/comm/Server/RemoteSignal.luau +211 -211
  122. package/vendor/rbxutil/comm/Server/ServerComm.luau +211 -211
  123. package/vendor/rbxutil/comm/Server/init.luau +140 -140
  124. package/vendor/rbxutil/comm/Types.luau +18 -18
  125. package/vendor/rbxutil/comm/Util.luau +27 -27
  126. package/vendor/rbxutil/comm/init.luau +35 -35
  127. package/vendor/rbxutil/comm/wally.toml +13 -13
  128. package/vendor/rbxutil/component/init.luau +759 -759
  129. package/vendor/rbxutil/component/init.test.luau +311 -311
  130. package/vendor/rbxutil/component/wally.toml +14 -14
  131. package/vendor/rbxutil/concur/init.luau +542 -542
  132. package/vendor/rbxutil/concur/init.test.luau +364 -364
  133. package/vendor/rbxutil/concur/wally.toml +8 -8
  134. package/vendor/rbxutil/enum-list/init.luau +101 -101
  135. package/vendor/rbxutil/enum-list/init.test.luau +91 -91
  136. package/vendor/rbxutil/enum-list/wally.toml +8 -8
  137. package/vendor/rbxutil/find/index.d.ts +20 -20
  138. package/vendor/rbxutil/find/init.luau +44 -44
  139. package/vendor/rbxutil/find/package.json +17 -17
  140. package/vendor/rbxutil/find/wally.toml +8 -8
  141. package/vendor/rbxutil/input/Gamepad.luau +559 -559
  142. package/vendor/rbxutil/input/Keyboard.luau +124 -124
  143. package/vendor/rbxutil/input/Mouse.luau +278 -278
  144. package/vendor/rbxutil/input/PreferredInput.luau +91 -91
  145. package/vendor/rbxutil/input/Touch.luau +120 -120
  146. package/vendor/rbxutil/input/init.luau +33 -33
  147. package/vendor/rbxutil/input/wally.toml +12 -12
  148. package/vendor/rbxutil/loader/index.d.ts +15 -15
  149. package/vendor/rbxutil/loader/init.luau +137 -137
  150. package/vendor/rbxutil/loader/wally.toml +8 -8
  151. package/vendor/rbxutil/log/index.d.ts +38 -38
  152. package/vendor/rbxutil/log/init.luau +746 -746
  153. package/vendor/rbxutil/log/wally.toml +8 -8
  154. package/vendor/rbxutil/net/init.luau +190 -190
  155. package/vendor/rbxutil/net/wally.toml +8 -8
  156. package/vendor/rbxutil/option/index.d.ts +44 -44
  157. package/vendor/rbxutil/option/init.luau +489 -489
  158. package/vendor/rbxutil/option/init.test.luau +342 -342
  159. package/vendor/rbxutil/option/wally.toml +8 -8
  160. package/vendor/rbxutil/pid/index.d.ts +53 -53
  161. package/vendor/rbxutil/pid/init.luau +195 -195
  162. package/vendor/rbxutil/pid/package.json +16 -16
  163. package/vendor/rbxutil/pid/wally.toml +9 -9
  164. package/vendor/rbxutil/quaternion/index.d.ts +117 -117
  165. package/vendor/rbxutil/quaternion/init.luau +570 -570
  166. package/vendor/rbxutil/quaternion/package.json +16 -16
  167. package/vendor/rbxutil/quaternion/wally.toml +9 -9
  168. package/vendor/rbxutil/query/index.d.ts +43 -43
  169. package/vendor/rbxutil/query/init.luau +117 -117
  170. package/vendor/rbxutil/query/package.json +18 -18
  171. package/vendor/rbxutil/query/wally.toml +9 -9
  172. package/vendor/rbxutil/sequent/index.d.ts +28 -28
  173. package/vendor/rbxutil/sequent/init.luau +340 -340
  174. package/vendor/rbxutil/sequent/package.json +16 -16
  175. package/vendor/rbxutil/sequent/wally.toml +9 -9
  176. package/vendor/rbxutil/ser/init.luau +175 -175
  177. package/vendor/rbxutil/ser/init.test.luau +50 -50
  178. package/vendor/rbxutil/ser/wally.toml +11 -11
  179. package/vendor/rbxutil/shake/index.d.ts +36 -36
  180. package/vendor/rbxutil/shake/init.luau +532 -532
  181. package/vendor/rbxutil/shake/init.test.luau +267 -267
  182. package/vendor/rbxutil/shake/package.json +16 -16
  183. package/vendor/rbxutil/shake/wally.toml +9 -9
  184. package/vendor/rbxutil/signal/index.d.ts +100 -100
  185. package/vendor/rbxutil/signal/init.luau +432 -432
  186. package/vendor/rbxutil/signal/init.test.luau +190 -190
  187. package/vendor/rbxutil/signal/package.json +17 -17
  188. package/vendor/rbxutil/signal/wally.toml +9 -9
  189. package/vendor/rbxutil/silo/TableWatcher.luau +65 -65
  190. package/vendor/rbxutil/silo/Util.luau +55 -55
  191. package/vendor/rbxutil/silo/init.luau +338 -338
  192. package/vendor/rbxutil/silo/init.test.luau +215 -215
  193. package/vendor/rbxutil/silo/wally.toml +8 -8
  194. package/vendor/rbxutil/spring/index.d.ts +40 -40
  195. package/vendor/rbxutil/spring/init.luau +97 -97
  196. package/vendor/rbxutil/spring/package.json +17 -17
  197. package/vendor/rbxutil/spring/wally.toml +8 -8
  198. package/vendor/rbxutil/stream/index.d.ts +88 -88
  199. package/vendor/rbxutil/stream/init.luau +597 -597
  200. package/vendor/rbxutil/stream/package.json +18 -18
  201. package/vendor/rbxutil/stream/wally.toml +9 -9
  202. package/vendor/rbxutil/streamable/Streamable.luau +202 -202
  203. package/vendor/rbxutil/streamable/StreamableUtil.luau +80 -80
  204. package/vendor/rbxutil/streamable/init.luau +8 -8
  205. package/vendor/rbxutil/streamable/wally.toml +12 -12
  206. package/vendor/rbxutil/symbol/init.luau +56 -56
  207. package/vendor/rbxutil/symbol/init.test.luau +37 -37
  208. package/vendor/rbxutil/symbol/wally.toml +8 -8
  209. package/vendor/rbxutil/table-util/init.luau +938 -938
  210. package/vendor/rbxutil/table-util/init.test.luau +439 -439
  211. package/vendor/rbxutil/task-queue/index.d.ts +27 -27
  212. package/vendor/rbxutil/task-queue/init.luau +97 -97
  213. package/vendor/rbxutil/task-queue/wally.toml +8 -8
  214. package/vendor/rbxutil/timer/index.d.ts +81 -81
  215. package/vendor/rbxutil/timer/init.luau +249 -249
  216. package/vendor/rbxutil/timer/init.test.luau +73 -73
  217. package/vendor/rbxutil/timer/wally.toml +11 -11
  218. package/vendor/rbxutil/tree/index.d.ts +15 -15
  219. package/vendor/rbxutil/tree/init.luau +137 -137
  220. package/vendor/rbxutil/tree/wally.toml +8 -8
  221. package/vendor/rbxutil/trove/index.d.ts +46 -46
  222. package/vendor/rbxutil/trove/init.luau +787 -787
  223. package/vendor/rbxutil/trove/init.test.luau +203 -203
  224. package/vendor/rbxutil/trove/wally.toml +8 -8
  225. package/vendor/rbxutil/typed-remote/init.luau +196 -196
  226. package/vendor/rbxutil/typed-remote/wally.toml +8 -8
  227. package/vendor/rbxutil/wait-for/index.d.ts +17 -17
  228. package/vendor/rbxutil/wait-for/init.luau +257 -257
  229. package/vendor/rbxutil/wait-for/init.test.luau +182 -182
  230. package/vendor/rbxutil/wait-for/wally.toml +11 -11
  231. package/vendor/t/t.lua +1350 -1350
  232. package/vendor/testez/Context.lua +26 -26
  233. package/vendor/testez/Expectation.lua +311 -311
  234. package/vendor/testez/ExpectationContext.lua +38 -38
  235. package/vendor/testez/LifecycleHooks.lua +89 -89
  236. package/vendor/testez/Reporters/TeamCityReporter.lua +101 -101
  237. package/vendor/testez/Reporters/TextReporter.lua +105 -105
  238. package/vendor/testez/Reporters/TextReporterQuiet.lua +96 -96
  239. package/vendor/testez/TestBootstrap.lua +146 -146
  240. package/vendor/testez/TestEnum.lua +27 -27
  241. package/vendor/testez/TestPlan.lua +304 -304
  242. package/vendor/testez/TestPlanner.lua +39 -39
  243. package/vendor/testez/TestResults.lua +111 -111
  244. package/vendor/testez/TestRunner.lua +188 -188
  245. package/vendor/testez/TestSession.lua +243 -243
  246. package/vendor/testez/init.lua +39 -39
@@ -1,295 +1,295 @@
1
- ---
2
- name: roblox-sharp-edges
3
- description: >
4
- 12 production footguns ranked by severity. Data loss, exploits, memory leaks, mobile perf.
5
- last_reviewed: 2026-05-22
6
- ---
7
-
8
- <!-- Source: brockmartin/roblox-game-skill (MIT) -->
9
-
10
- # Roblox Sharp Edges (Gotchas) Reference
11
-
12
- > Every entry here represents a real production footgun that has caused data loss, exploits,
13
- > crashes, or hours of debugging in Roblox games.
14
- >
15
- > **Severity Levels:**
16
- > - **Critical** - Data loss, security breach, or revenue loss. Fix before shipping.
17
- > - **High** - Server instability, degraded experience, or exploit surface. Fix in current sprint.
18
- > - **Medium** - Correctness bugs, performance issues, or dev confusion. Fix before scale.
19
- > - **Low** - Code quality, maintainability, or minor timing issues. Fix when convenient.
20
-
21
- ---
22
-
23
- ## SE-1 | Critical | DataStore Data Loss from Session Handling
24
-
25
- **See roblox-data → Session Locking and ProfileStore for full details.**
26
-
27
- When a player server-hops, the old server may still be saving while the new server loads stale data. ProfileStore handles session locking automatically - only one server owns a player's data at a time. Never use raw DataStoreService for player data without session locking.
28
-
29
- ---
30
-
31
- ## SE-2 | Critical | Client-Side Currency Manipulation
32
-
33
- **See roblox-networking → Security Hardening for full details.**
34
-
35
- Currency and all authoritative game state must live exclusively on the server. Never accept currency amounts from the client. The server computes all transactions internally and pushes display-only updates to the client. This is the single most common exploit in Roblox games.
36
-
37
- ---
38
-
39
- ## SE-3 | Critical | ProcessReceipt Mishandling
40
-
41
- **See roblox-monetization → ProcessReceipt for full details.**
42
-
43
- `MarketplaceService.ProcessReceipt` must return `PurchaseGranted` ONLY after the item is granted AND saved. If you return `PurchaseGranted` before granting, the player loses Robux. If you don't return it, Roblox retries on every join - potentially granting duplicates. Grant first, save second, return third.
44
-
45
- ---
46
-
47
- ## SE-4 | High | Memory Leaks from Undisconnected Events
48
-
49
- ### Problem
50
-
51
- Every `:Connect()` returns an `RBXScriptConnection`. If you never `:Disconnect()` it, the connection persists for the script's lifetime - even after the object is destroyed. In per-player systems, memory grows linearly with every player who has ever joined.
52
-
53
- ### Symptoms
54
-
55
- - Server memory climbs steadily over time.
56
- - Server FPS degrades after hours.
57
- - Callbacks fire for players who left.
58
-
59
- ### Solution
60
-
61
- Use the vendored **Trove** module (`vendor/rbxutil/trove/`) to group connections per-player and clean them all on `PlayerRemoving`:
62
-
63
- ```luau
64
- local Players = game:GetService("Players")
65
- local Trove = require(game.ReplicatedStorage.Packages.Trove)
66
-
67
- local playerTroves: { [Player]: typeof(Trove.new()) } = {}
68
-
69
- local function onPlayerAdded(player: Player)
70
- local trove = Trove.new()
71
- playerTroves[player] = trove
72
-
73
- trove:Connect(player.CharacterAdded, function(character)
74
- local humanoid = character:WaitForChild("Humanoid")
75
- trove:Connect(humanoid.Died, function()
76
- task.wait(3)
77
- player:LoadCharacter()
78
- end)
79
- end)
80
- end
81
-
82
- local function onPlayerRemoving(player: Player)
83
- local trove = playerTroves[player]
84
- if trove then
85
- trove:Clean()
86
- playerTroves[player] = nil
87
- end
88
- end
89
-
90
- Players.PlayerAdded:Connect(onPlayerAdded)
91
- Players.PlayerRemoving:Connect(onPlayerRemoving)
92
- ```
93
-
94
- ---
95
-
96
- ## SE-5 | High | RemoteEvent Flooding
97
-
98
- ### Problem
99
-
100
- RemoteEvents have no built-in rate limiting. Exploiters can fire thousands of times per second, flooding the server with DataStore calls, instance creation, or raycasts.
101
-
102
- ### Solution
103
-
104
- Implement per-player, per-remote rate limiting on the server. See **roblox-networking → Rate Limiting** for production patterns.
105
-
106
- Minimal inline example:
107
-
108
- ```luau
109
- local lastFire: { [Player]: number } = {}
110
- local COOLDOWN = 0.1
111
-
112
- AttackRemote.OnServerEvent:Connect(function(player: Player, targetId: number)
113
- local now = os.clock()
114
- if lastFire[player] and now - lastFire[player] < COOLDOWN then return end
115
- lastFire[player] = now
116
- -- process attack
117
- end)
118
- ```
119
-
120
- ---
121
-
122
- ## SE-6 | High | BindToClose Timeout
123
-
124
- **See roblox-data → Best Practices (BindToClose Handler) for full details.**
125
-
126
- `game:BindToClose()` gives at most 30 seconds. If using ProfileStore, this is automatic. If using raw DataStore, save all players in parallel with `task.spawn` - sequential saves with 50 players will timeout.
127
-
128
- ---
129
-
130
- ## SE-7 | Medium | Part Count on Mobile
131
-
132
- Mobile devices struggle above ~10,000 visible parts. Enable **StreamingEnabled** and configure `StreamingMinRadius`/`StreamingTargetRadius`. Use `ModelStreamingMode` to mark distant models as Opportunistic and gameplay-critical models as Persistent.
133
-
134
- See **roblox-runtime → StreamingEnabled** for configuration details.
135
-
136
- ---
137
-
138
- ## SE-8 | Medium | Yielding in Module Require
139
-
140
- ### Problem
141
-
142
- `require()` executes the module body synchronously. If it yields (`WaitForChild`, `task.wait`, HTTP), every script requiring that module blocks. Two modules requiring each other with yields = deadlock.
143
-
144
- ### Solution
145
-
146
- Never yield in a module body. Use Init/Start lifecycle:
147
-
148
- ```luau
149
- local CombatSystem = {}
150
-
151
- function CombatSystem:Init()
152
- -- WaitForChild is safe here (called by bootstrap, not during require)
153
- self._remotes = game.ReplicatedStorage:WaitForChild("Remotes", 10)
154
- end
155
-
156
- function CombatSystem:Start()
157
- -- Connect events after all modules are Init'd
158
- end
159
-
160
- return CombatSystem
161
- ```
162
-
163
- Bootstrap script calls `:Init()` on all modules, then `:Start()` on all modules.
164
-
165
- ---
166
-
167
- ## SE-9 | Medium | Table Length with Nil Gaps
168
-
169
- ### Problem
170
-
171
- `#` is only reliable for sequence tables (consecutive integer keys, no nil gaps). Setting `tbl[3] = nil` creates a hole; `#tbl` may return any valid boundary.
172
-
173
- ### Solution
174
-
175
- - Never set array elements to `nil`. Use `table.remove()` to shift elements.
176
- - Use generalized iteration (`for _, v in tbl do`) instead of `for i = 1, #tbl`.
177
- - For sparse data, use dictionary keys instead of integer indices.
178
-
179
- ---
180
-
181
- ## SE-10 | Low | Deprecated wait()/spawn()/delay()
182
-
183
- **See roblox-luau-mastery → Task Library for full details.**
184
-
185
- Replace `wait()` → `task.wait()`, `spawn()` → `task.spawn()`, `delay()` → `task.delay()`. Legacy functions have minimum yield issues, unpredictable timing, and swallow errors.
186
-
187
- ---
188
-
189
- ## SE-11 | Medium | Infinite Yield Warning
190
-
191
- ### Problem
192
-
193
- `WaitForChild(name)` without a timeout yields forever if the child never appears. Common with renamed instances, StreamingEnabled, or race conditions.
194
-
195
- ### Solution
196
-
197
- Always pass a timeout. Handle `nil` return:
198
-
199
- ```luau
200
- local folder = ReplicatedStorage:WaitForChild("Weapons", 10)
201
- if not folder then
202
- warn("[Init] Weapons folder not found after 10s")
203
- return
204
- end
205
- ```
206
-
207
- ---
208
-
209
- ## SE-12 | Low | String Patterns vs Regex
210
-
211
- ### Problem
212
-
213
- Luau uses Lua string patterns, not regex. `\d` doesn't work - use `%d`. Escape with `%` not `\`. No alternation (`|`), no non-greedy `*?` (use `-` instead), no lookahead.
214
-
215
- ### Key Differences
216
-
217
- - Digits: `%d` not `\d`
218
- - Word chars: `%w` not `\w`
219
- - Whitespace: `%s` not `\s`
220
- - Escape special chars: `%.` not `\.`
221
- - Non-greedy: `.-` not `.*?`
222
- - Literal `%`: `%%`
223
-
224
- ---
225
-
226
- ## SE-13 | Medium | Local Function Declaration Order
227
-
228
- ### Problem
229
-
230
- Luau has no hoisting. A `local function` is invisible to code above its declaration. AI assistants frequently place helper functions below the functions that call them, causing nil-value runtime errors.
231
-
232
- ### Rule
233
-
234
- **Callees above callers. Always.** If `functionA()` calls `helperB()`, then `helperB` must be declared first.
235
-
236
- ```luau
237
- -- BAD: helperB is nil when functionA runs
238
- local function functionA()
239
- helperB() -- ERROR: attempt to call a nil value
240
- end
241
-
242
- local function helperB()
243
- print("I'm a helper")
244
- end
245
-
246
- -- GOOD: helper declared first
247
- local function helperB()
248
- print("I'm a helper")
249
- end
250
-
251
- local function functionA()
252
- helperB() -- works
253
- end
254
- ```
255
-
256
- ### When you need mutual recursion
257
-
258
- Use forward declaration:
259
-
260
- ```luau
261
- local functionB -- forward declare
262
- local function functionA()
263
- functionB()
264
- end
265
- function functionB() -- note: no 'local' (already declared above)
266
- functionA()
267
- end
268
- ```
269
-
270
- ---
271
-
272
- ## Quick Reference
273
-
274
- ```
275
- CRITICAL (fix before shipping):
276
- SE-1 DataStore session locking → Use ProfileStore
277
- SE-2 Client-side currency → Server-authoritative only
278
- SE-3 ProcessReceipt order → Grant THEN PurchaseGranted
279
-
280
- HIGH (fix in current sprint):
281
- SE-4 Undisconnected events → Trove pattern (vendored)
282
- SE-5 RemoteEvent flooding → Per-player rate limiter
283
- SE-6 BindToClose 30s timeout → Parallel saves with task.spawn
284
-
285
- MEDIUM (fix before scale):
286
- SE-7 Mobile part count → StreamingEnabled + <10K parts
287
- SE-8 Yielding in module require → Init/Start lifecycle pattern
288
- SE-9 Table # with nil gaps → table.remove or explicit length
289
- SE-11 Infinite yield WaitForChild → Always pass timeout parameter
290
- SE-13 Local function order → Callees above callers (no hoisting)
291
-
292
- LOW (fix when convenient):
293
- SE-10 Deprecated wait/spawn/delay → task.wait/spawn/delay
294
- SE-12 String patterns vs regex → %d not \d, % not \
1
+ ---
2
+ name: roblox-sharp-edges
3
+ description: >
4
+ 12 production footguns ranked by severity. Data loss, exploits, memory leaks, mobile perf.
5
+ last_reviewed: 2026-05-22
6
+ ---
7
+
8
+ <!-- Source: brockmartin/roblox-game-skill (MIT) -->
9
+
10
+ # Roblox Sharp Edges (Gotchas) Reference
11
+
12
+ > Every entry here represents a real production footgun that has caused data loss, exploits,
13
+ > crashes, or hours of debugging in Roblox games.
14
+ >
15
+ > **Severity Levels:**
16
+ > - **Critical** - Data loss, security breach, or revenue loss. Fix before shipping.
17
+ > - **High** - Server instability, degraded experience, or exploit surface. Fix in current sprint.
18
+ > - **Medium** - Correctness bugs, performance issues, or dev confusion. Fix before scale.
19
+ > - **Low** - Code quality, maintainability, or minor timing issues. Fix when convenient.
20
+
21
+ ---
22
+
23
+ ## SE-1 | Critical | DataStore Data Loss from Session Handling
24
+
25
+ **See roblox-data → Session Locking and ProfileStore for full details.**
26
+
27
+ When a player server-hops, the old server may still be saving while the new server loads stale data. ProfileStore handles session locking automatically - only one server owns a player's data at a time. Never use raw DataStoreService for player data without session locking.
28
+
29
+ ---
30
+
31
+ ## SE-2 | Critical | Client-Side Currency Manipulation
32
+
33
+ **See roblox-networking → Security Hardening for full details.**
34
+
35
+ Currency and all authoritative game state must live exclusively on the server. Never accept currency amounts from the client. The server computes all transactions internally and pushes display-only updates to the client. This is the single most common exploit in Roblox games.
36
+
37
+ ---
38
+
39
+ ## SE-3 | Critical | ProcessReceipt Mishandling
40
+
41
+ **See roblox-monetization → ProcessReceipt for full details.**
42
+
43
+ `MarketplaceService.ProcessReceipt` must return `PurchaseGranted` ONLY after the item is granted AND saved. If you return `PurchaseGranted` before granting, the player loses Robux. If you don't return it, Roblox retries on every join - potentially granting duplicates. Grant first, save second, return third.
44
+
45
+ ---
46
+
47
+ ## SE-4 | High | Memory Leaks from Undisconnected Events
48
+
49
+ ### Problem
50
+
51
+ Every `:Connect()` returns an `RBXScriptConnection`. If you never `:Disconnect()` it, the connection persists for the script's lifetime - even after the object is destroyed. In per-player systems, memory grows linearly with every player who has ever joined.
52
+
53
+ ### Symptoms
54
+
55
+ - Server memory climbs steadily over time.
56
+ - Server FPS degrades after hours.
57
+ - Callbacks fire for players who left.
58
+
59
+ ### Solution
60
+
61
+ Use the vendored **Trove** module (`vendor/rbxutil/trove/`) to group connections per-player and clean them all on `PlayerRemoving`:
62
+
63
+ ```luau
64
+ local Players = game:GetService("Players")
65
+ local Trove = require(game.ReplicatedStorage.Packages.Trove)
66
+
67
+ local playerTroves: { [Player]: typeof(Trove.new()) } = {}
68
+
69
+ local function onPlayerAdded(player: Player)
70
+ local trove = Trove.new()
71
+ playerTroves[player] = trove
72
+
73
+ trove:Connect(player.CharacterAdded, function(character)
74
+ local humanoid = character:WaitForChild("Humanoid")
75
+ trove:Connect(humanoid.Died, function()
76
+ task.wait(3)
77
+ player:LoadCharacter()
78
+ end)
79
+ end)
80
+ end
81
+
82
+ local function onPlayerRemoving(player: Player)
83
+ local trove = playerTroves[player]
84
+ if trove then
85
+ trove:Clean()
86
+ playerTroves[player] = nil
87
+ end
88
+ end
89
+
90
+ Players.PlayerAdded:Connect(onPlayerAdded)
91
+ Players.PlayerRemoving:Connect(onPlayerRemoving)
92
+ ```
93
+
94
+ ---
95
+
96
+ ## SE-5 | High | RemoteEvent Flooding
97
+
98
+ ### Problem
99
+
100
+ RemoteEvents have no built-in rate limiting. Exploiters can fire thousands of times per second, flooding the server with DataStore calls, instance creation, or raycasts.
101
+
102
+ ### Solution
103
+
104
+ Implement per-player, per-remote rate limiting on the server. See **roblox-networking → Rate Limiting** for production patterns.
105
+
106
+ Minimal inline example:
107
+
108
+ ```luau
109
+ local lastFire: { [Player]: number } = {}
110
+ local COOLDOWN = 0.1
111
+
112
+ AttackRemote.OnServerEvent:Connect(function(player: Player, targetId: number)
113
+ local now = os.clock()
114
+ if lastFire[player] and now - lastFire[player] < COOLDOWN then return end
115
+ lastFire[player] = now
116
+ -- process attack
117
+ end)
118
+ ```
119
+
120
+ ---
121
+
122
+ ## SE-6 | High | BindToClose Timeout
123
+
124
+ **See roblox-data → Best Practices (BindToClose Handler) for full details.**
125
+
126
+ `game:BindToClose()` gives at most 30 seconds. If using ProfileStore, this is automatic. If using raw DataStore, save all players in parallel with `task.spawn` - sequential saves with 50 players will timeout.
127
+
128
+ ---
129
+
130
+ ## SE-7 | Medium | Part Count on Mobile
131
+
132
+ Mobile devices struggle above ~10,000 visible parts. Enable **StreamingEnabled** and configure `StreamingMinRadius`/`StreamingTargetRadius`. Use `ModelStreamingMode` to mark distant models as Opportunistic and gameplay-critical models as Persistent.
133
+
134
+ See **roblox-runtime → StreamingEnabled** for configuration details.
135
+
136
+ ---
137
+
138
+ ## SE-8 | Medium | Yielding in Module Require
139
+
140
+ ### Problem
141
+
142
+ `require()` executes the module body synchronously. If it yields (`WaitForChild`, `task.wait`, HTTP), every script requiring that module blocks. Two modules requiring each other with yields = deadlock.
143
+
144
+ ### Solution
145
+
146
+ Never yield in a module body. Use Init/Start lifecycle:
147
+
148
+ ```luau
149
+ local CombatSystem = {}
150
+
151
+ function CombatSystem:Init()
152
+ -- WaitForChild is safe here (called by bootstrap, not during require)
153
+ self._remotes = game.ReplicatedStorage:WaitForChild("Remotes", 10)
154
+ end
155
+
156
+ function CombatSystem:Start()
157
+ -- Connect events after all modules are Init'd
158
+ end
159
+
160
+ return CombatSystem
161
+ ```
162
+
163
+ Bootstrap script calls `:Init()` on all modules, then `:Start()` on all modules.
164
+
165
+ ---
166
+
167
+ ## SE-9 | Medium | Table Length with Nil Gaps
168
+
169
+ ### Problem
170
+
171
+ `#` is only reliable for sequence tables (consecutive integer keys, no nil gaps). Setting `tbl[3] = nil` creates a hole; `#tbl` may return any valid boundary.
172
+
173
+ ### Solution
174
+
175
+ - Never set array elements to `nil`. Use `table.remove()` to shift elements.
176
+ - Use generalized iteration (`for _, v in tbl do`) instead of `for i = 1, #tbl`.
177
+ - For sparse data, use dictionary keys instead of integer indices.
178
+
179
+ ---
180
+
181
+ ## SE-10 | Low | Deprecated wait()/spawn()/delay()
182
+
183
+ **See roblox-luau-mastery → Task Library for full details.**
184
+
185
+ Replace `wait()` → `task.wait()`, `spawn()` → `task.spawn()`, `delay()` → `task.delay()`. Legacy functions have minimum yield issues, unpredictable timing, and swallow errors.
186
+
187
+ ---
188
+
189
+ ## SE-11 | Medium | Infinite Yield Warning
190
+
191
+ ### Problem
192
+
193
+ `WaitForChild(name)` without a timeout yields forever if the child never appears. Common with renamed instances, StreamingEnabled, or race conditions.
194
+
195
+ ### Solution
196
+
197
+ Always pass a timeout. Handle `nil` return:
198
+
199
+ ```luau
200
+ local folder = ReplicatedStorage:WaitForChild("Weapons", 10)
201
+ if not folder then
202
+ warn("[Init] Weapons folder not found after 10s")
203
+ return
204
+ end
205
+ ```
206
+
207
+ ---
208
+
209
+ ## SE-12 | Low | String Patterns vs Regex
210
+
211
+ ### Problem
212
+
213
+ Luau uses Lua string patterns, not regex. `\d` doesn't work - use `%d`. Escape with `%` not `\`. No alternation (`|`), no non-greedy `*?` (use `-` instead), no lookahead.
214
+
215
+ ### Key Differences
216
+
217
+ - Digits: `%d` not `\d`
218
+ - Word chars: `%w` not `\w`
219
+ - Whitespace: `%s` not `\s`
220
+ - Escape special chars: `%.` not `\.`
221
+ - Non-greedy: `.-` not `.*?`
222
+ - Literal `%`: `%%`
223
+
224
+ ---
225
+
226
+ ## SE-13 | Medium | Local Function Declaration Order
227
+
228
+ ### Problem
229
+
230
+ Luau has no hoisting. A `local function` is invisible to code above its declaration. AI assistants frequently place helper functions below the functions that call them, causing nil-value runtime errors.
231
+
232
+ ### Rule
233
+
234
+ **Callees above callers. Always.** If `functionA()` calls `helperB()`, then `helperB` must be declared first.
235
+
236
+ ```luau
237
+ -- BAD: helperB is nil when functionA runs
238
+ local function functionA()
239
+ helperB() -- ERROR: attempt to call a nil value
240
+ end
241
+
242
+ local function helperB()
243
+ print("I'm a helper")
244
+ end
245
+
246
+ -- GOOD: helper declared first
247
+ local function helperB()
248
+ print("I'm a helper")
249
+ end
250
+
251
+ local function functionA()
252
+ helperB() -- works
253
+ end
254
+ ```
255
+
256
+ ### When you need mutual recursion
257
+
258
+ Use forward declaration:
259
+
260
+ ```luau
261
+ local functionB -- forward declare
262
+ local function functionA()
263
+ functionB()
264
+ end
265
+ function functionB() -- note: no 'local' (already declared above)
266
+ functionA()
267
+ end
268
+ ```
269
+
270
+ ---
271
+
272
+ ## Quick Reference
273
+
274
+ ```
275
+ CRITICAL (fix before shipping):
276
+ SE-1 DataStore session locking → Use ProfileStore
277
+ SE-2 Client-side currency → Server-authoritative only
278
+ SE-3 ProcessReceipt order → Grant THEN PurchaseGranted
279
+
280
+ HIGH (fix in current sprint):
281
+ SE-4 Undisconnected events → Trove pattern (vendored)
282
+ SE-5 RemoteEvent flooding → Per-player rate limiter
283
+ SE-6 BindToClose 30s timeout → Parallel saves with task.spawn
284
+
285
+ MEDIUM (fix before scale):
286
+ SE-7 Mobile part count → StreamingEnabled + <10K parts
287
+ SE-8 Yielding in module require → Init/Start lifecycle pattern
288
+ SE-9 Table # with nil gaps → table.remove or explicit length
289
+ SE-11 Infinite yield WaitForChild → Always pass timeout parameter
290
+ SE-13 Local function order → Callees above callers (no hoisting)
291
+
292
+ LOW (fix when convenient):
293
+ SE-10 Deprecated wait/spawn/delay → task.wait/spawn/delay
294
+ SE-12 String patterns vs regex → %d not \d, % not \
295
295
  ```