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.
- 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 +877 -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 +1618 -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,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
|
```
|