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.
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 +863 -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 +1519 -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,669 +1,669 @@
1
- ---
2
- name: roblox-networking
3
- description: >
4
- Server-authoritative networking, RemoteEvent validation, rate limiting, exploit prevention,
5
- security hardening.
6
- last_reviewed: 2026-05-22
7
- ---
8
-
9
- <!-- Source: brockmartin/roblox-game-skill (MIT) -->
10
-
11
- # Roblox Networking & Security Reference
12
-
13
- ---
14
-
15
- ## Overview
16
-
17
- **Load this reference when:**
18
-
19
- - Validating RemoteEvent/RemoteFunction input on the server
20
- - Implementing rate limiting or anti-exploit measures
21
- - Designing server-authoritative systems (damage, currency, inventory)
22
- - Hardening existing networking code against exploiters
23
-
24
- This document covers server-side validation, rate limiting, suspicion scoring, and server-authoritative design patterns. For player lifecycle (PlayerAdded/Removing), see **roblox-architecture**.
25
-
26
- ---
27
-
28
- ## Quick Reference
29
-
30
- **Load Full Reference below only when you need specific validation module code or rate limiting implementations.**
31
-
32
- Key rules:
33
- - NEVER trust the client. Every RemoteEvent arg is attacker-controlled.
34
- - Validate: type, range, ownership, cooldown on EVERY server handler.
35
- - Server-authoritative: server decides outcomes. Client is display-only.
36
- - Rate limit all remotes. Per-player cooldown table minimum.
37
- - Damage: server calculates from weapon stats + distance + cooldown. Never accept damage values from client.
38
- - Currency: all math server-side. Client displays only.
39
- - Movement: validate distance/speed against physics. Flag teleportation.
40
- - Use `t` library for composable type checks on remote args.
41
- - Suspicion scoring: accumulate violations, kick/ban at threshold. Don't instant-kick on first offense.
42
- - Exploiters can: fire any remote, read all client code, modify any client state, speed/fly/teleport.
43
-
44
- ---
45
-
46
- ## Full Reference
47
-
48
- ## Security Hardening
49
-
50
- ### Never Trust the Client
51
-
52
- Every RemoteEvent payload is attacker-controlled. Validate type, range, ownership, and cooldown on the server for every request.
53
-
54
- - **Modify any LocalScript** -- injecting code, changing variables, hooking functions.
55
- - **Fire any RemoteEvent with arbitrary arguments** -- types, values, and counts are all attacker-controlled.
56
- - **Speed hack, fly, and teleport** -- the character's physics can be overridden entirely on the client.
57
- - **See all client-accessible code** -- anything in `StarterPlayerScripts`, `StarterGui`, `ReplicatedStorage`, or `ReplicatedFirst` is fully readable.
58
- - **Read and modify any client-side state** -- health displays, cooldown timers, UI flags.
59
- - **Intercept and replay network traffic** -- RemoteSpy tools let exploiters see every remote call and replay or modify them.
60
-
61
- **The client is a display layer, not a trusted authority.** It renders the world and collects input. The server decides what actually happens.
62
-
63
- A useful mental model: treat every `RemoteEvent:FireServer()` call as if it were an HTTP request from an anonymous stranger on the internet. Validate everything. Assume nothing.
64
-
65
- ---
66
-
67
- ### RemoteEvent Validation Patterns
68
-
69
- > **For runtime type checking, the `t` library is vendored** at `vendor/t/t.lua` (osyrisrblx/t v3.1.1, MIT). It provides composable type checks (`t.string`, `t.number`, `t.interface({...})`) that are cleaner than manual typeof() chains. The agent can place it when relevant.
70
-
71
- ### The Problem
72
-
73
- A bare remote handler like this is exploitable:
74
-
75
- ```luau
76
- -- BAD: No validation at all
77
- DamageRemote.OnServerEvent:Connect(function(player, targetName, damage)
78
- local target = Players:FindFirstChild(targetName)
79
- target.Character.Humanoid:TakeDamage(damage)
80
- end)
81
- ```
82
-
83
- An exploiter can fire this with any target name and any damage value, instantly killing anyone.
84
-
85
- ### Production-Ready Validation Module
86
-
87
- Place this in `ServerScriptService`:
88
-
89
- ```luau
90
- -- ServerScriptService/Modules/RemoteValidator.luau
91
-
92
- local RemoteValidator = {}
93
-
94
- --[[ -----------------------------------------------------------------------
95
- Type Checking
96
- Validates that arguments match expected types.
97
- ----------------------------------------------------------------------- ]]
98
-
99
- type TypeSpec = string | (value: any) -> boolean
100
-
101
- function RemoteValidator.checkType(value: any, expected: TypeSpec): boolean
102
- if typeof(expected) == "function" then
103
- return expected(value)
104
- end
105
- return typeof(value) == expected
106
- end
107
-
108
- function RemoteValidator.validateArgs(
109
- args: { any },
110
- schema: { { name: string, type: TypeSpec, optional: boolean? } }
111
- ): (boolean, string?)
112
- for i, spec in schema do
113
- local value = args[i]
114
-
115
- if value == nil then
116
- if not spec.optional then
117
- return false, `Missing required argument: {spec.name}`
118
- end
119
- continue
120
- end
121
-
122
- if not RemoteValidator.checkType(value, spec.type) then
123
- return false, `Invalid type for {spec.name}: expected {tostring(spec.type)}, got {typeof(value)}`
124
- end
125
- end
126
-
127
- -- Reject extra arguments that were not declared in the schema
128
- if #args > #schema then
129
- return false, `Too many arguments: expected {#schema}, got {#args}`
130
- end
131
-
132
- return true, nil
133
- end
134
-
135
- --[[ -----------------------------------------------------------------------
136
- Range Checking
137
- Validates that numeric values fall within acceptable bounds.
138
- ----------------------------------------------------------------------- ]]
139
-
140
- function RemoteValidator.checkRange(value: number, min: number, max: number): boolean
141
- return typeof(value) == "number"
142
- and value == value -- NaN check
143
- and value >= min
144
- and value <= max
145
- end
146
-
147
- function RemoteValidator.checkIntegerRange(value: number, min: number, max: number): boolean
148
- return RemoteValidator.checkRange(value, min, max)
149
- and math.floor(value) == value
150
- end
151
-
152
- --[[ -----------------------------------------------------------------------
153
- Cooldown Tracking
154
- Per-player, per-action cooldown enforcement.
155
- ----------------------------------------------------------------------- ]]
156
-
157
- local cooldowns: { [Player]: { [string]: number } } = {}
158
-
159
- function RemoteValidator.checkCooldown(player: Player, action: string, cooldownSeconds: number): boolean
160
- local now = os.clock()
161
- local playerCooldowns = cooldowns[player]
162
-
163
- if not playerCooldowns then
164
- playerCooldowns = {}
165
- cooldowns[player] = playerCooldowns
166
- end
167
-
168
- local lastUsed = playerCooldowns[action]
169
- if lastUsed and (now - lastUsed) < cooldownSeconds then
170
- return false
171
- end
172
-
173
- playerCooldowns[action] = now
174
- return true
175
- end
176
-
177
- function RemoteValidator.clearPlayerCooldowns(player: Player)
178
- cooldowns[player] = nil
179
- end
180
-
181
- --[[ -----------------------------------------------------------------------
182
- Existence Checks
183
- Validates that targets, objects, and instances actually exist.
184
- ----------------------------------------------------------------------- ]]
185
-
186
- function RemoteValidator.playerExists(playerName: string): Player?
187
- local Players = game:GetService("Players")
188
- return Players:FindFirstChild(playerName) :: Player?
189
- end
190
-
191
- function RemoteValidator.characterAlive(player: Player): boolean
192
- local character = player.Character
193
- if not character then
194
- return false
195
- end
196
-
197
- local humanoid = character:FindFirstChildOfClass("Humanoid")
198
- if not humanoid then
199
- return false
200
- end
201
-
202
- return humanoid.Health > 0
203
- end
204
-
205
- function RemoteValidator.instanceExists(parent: Instance, name: string, className: string?): Instance?
206
- local child = parent:FindFirstChild(name)
207
- if not child then
208
- return nil
209
- end
210
-
211
- if className and not child:IsA(className) then
212
- return nil
213
- end
214
-
215
- return child
216
- end
217
-
218
- --[[ -----------------------------------------------------------------------
219
- Authorization
220
- Checks if a player is allowed to perform an action.
221
- ----------------------------------------------------------------------- ]]
222
-
223
- function RemoteValidator.playerOwnsItem(player: Player, itemId: string, inventoryFolder: Folder?): boolean
224
- local folder = inventoryFolder or player:FindFirstChild("Inventory") :: Folder?
225
- if not folder then
226
- return false
227
- end
228
-
229
- return folder:FindFirstChild(itemId) ~= nil
230
- end
231
-
232
- function RemoteValidator.playerHasAttribute(player: Player, attribute: string, expectedValue: any?): boolean
233
- local value = player:GetAttribute(attribute)
234
- if expectedValue ~= nil then
235
- return value == expectedValue
236
- end
237
- return value ~= nil
238
- end
239
-
240
- --[[ -----------------------------------------------------------------------
241
- Distance Check
242
- Validates that two positions are within an acceptable range.
243
- ----------------------------------------------------------------------- ]]
244
-
245
- function RemoteValidator.withinRange(posA: Vector3, posB: Vector3, maxDistance: number): boolean
246
- return (posA - posB).Magnitude <= maxDistance
247
- end
248
-
249
- function RemoteValidator.playerWithinRange(player: Player, targetPos: Vector3, maxDistance: number): boolean
250
- local character = player.Character
251
- if not character then
252
- return false
253
- end
254
-
255
- local root = character:FindFirstChild("HumanoidRootPart")
256
- if not root then
257
- return false
258
- end
259
-
260
- return RemoteValidator.withinRange(root.Position, targetPos, maxDistance)
261
- end
262
-
263
- --[[ -----------------------------------------------------------------------
264
- Cleanup
265
- ----------------------------------------------------------------------- ]]
266
-
267
- game:GetService("Players").PlayerRemoving:Connect(function(player)
268
- RemoteValidator.clearPlayerCooldowns(player)
269
- end)
270
-
271
- return RemoteValidator
272
- ```
273
-
274
- ### Using the Validation Module
275
-
276
- ```luau
277
- -- ServerScriptService/RemoteHandlers/DamageHandler.server.luau
278
-
279
- local ReplicatedStorage = game:GetService("ReplicatedStorage")
280
- local ServerScriptService = game:GetService("ServerScriptService")
281
-
282
- local Validator = require(ServerScriptService.Modules.RemoteValidator)
283
- local DamageRemote = ReplicatedStorage.Remotes.DealDamage
284
-
285
- local MAX_DAMAGE = 50
286
- local DAMAGE_COOLDOWN = 0.5 -- seconds
287
- local ATTACK_RANGE = 15 -- studs
288
-
289
- local ARG_SCHEMA = {
290
- { name = "targetPlayer", type = "Instance" },
291
- { name = "damage", type = "number" },
292
- }
293
-
294
- DamageRemote.OnServerEvent:Connect(function(player: Player, ...: any)
295
- local args = { ... }
296
-
297
- -- 1. Validate argument types
298
- local valid, err = Validator.validateArgs(args, ARG_SCHEMA)
299
- if not valid then
300
- warn(`[DamageHandler] {player.Name}: {err}`)
301
- return
302
- end
303
-
304
- local targetPlayer: Player = args[1]
305
- local damage: number = args[2]
306
-
307
- -- 2. Validate the target is actually a Player
308
- if not targetPlayer:IsA("Player") then
309
- return
310
- end
311
-
312
- -- 3. Validate damage range
313
- if not Validator.checkIntegerRange(damage, 1, MAX_DAMAGE) then
314
- warn(`[DamageHandler] {player.Name}: damage out of range ({damage})`)
315
- return
316
- end
317
-
318
- -- 4. Cooldown check
319
- if not Validator.checkCooldown(player, "DealDamage", DAMAGE_COOLDOWN) then
320
- return
321
- end
322
-
323
- -- 5. Verify attacker is alive
324
- if not Validator.characterAlive(player) then
325
- return
326
- end
327
-
328
- -- 6. Verify target is alive
329
- if not Validator.characterAlive(targetPlayer) then
330
- return
331
- end
332
-
333
- -- 7. Range check -- attacker must be near the target
334
- local targetRoot = targetPlayer.Character and targetPlayer.Character:FindFirstChild("HumanoidRootPart")
335
- if not targetRoot then
336
- return
337
- end
338
-
339
- if not Validator.playerWithinRange(player, targetRoot.Position, ATTACK_RANGE) then
340
- warn(`[DamageHandler] {player.Name}: target out of range`)
341
- return
342
- end
343
-
344
- -- 8. Authorization -- verify the player has a weapon equipped
345
- local character = player.Character
346
- local weapon = character and character:FindFirstChildOfClass("Tool")
347
- if not weapon or not weapon:GetAttribute("CanDealDamage") then
348
- warn(`[DamageHandler] {player.Name}: no valid weapon equipped`)
349
- return
350
- end
351
-
352
- -- 9. Server calculates actual damage (never trust client damage value directly)
353
- local serverDamage = math.min(damage, weapon:GetAttribute("MaxDamage") or MAX_DAMAGE)
354
-
355
- -- 10. Apply damage
356
- local targetHumanoid = targetPlayer.Character:FindFirstChildOfClass("Humanoid")
357
- if targetHumanoid then
358
- targetHumanoid:TakeDamage(serverDamage)
359
- end
360
- end)
361
- ```
362
-
363
- ---
364
-
365
- ### Server-Authoritative Design
366
-
367
- The server owns all game state. The client requests actions; the server decides outcomes.
368
-
369
- ### Movement Validation
370
-
371
- ```luau
372
- -- ServerScriptService/Security/MovementValidator.server.luau
373
-
374
- local Players = game:GetService("Players")
375
- local RunService = game:GetService("RunService")
376
-
377
- local MAX_SPEED = 50 -- studs per second (walk + sprint + tolerance)
378
- local MAX_VERTICAL_SPEED = 100 -- studs per second (jumping/falling tolerance)
379
- local VIOLATION_THRESHOLD = 5 -- strikes before action
380
- local CHECK_INTERVAL = 0.5 -- seconds between checks
381
-
382
- local playerData: { [Player]: {
383
- lastPosition: Vector3,
384
- lastCheck: number,
385
- violations: number,
386
- } } = {}
387
-
388
- Players.PlayerAdded:Connect(function(player)
389
- player.CharacterAdded:Connect(function(character)
390
- local root = character:WaitForChild("HumanoidRootPart")
391
- playerData[player] = {
392
- lastPosition = root.Position,
393
- lastCheck = os.clock(),
394
- violations = 0,
395
- }
396
- end)
397
- end)
398
-
399
- Players.PlayerRemoving:Connect(function(player)
400
- playerData[player] = nil
401
- end)
402
-
403
- RunService.Heartbeat:Connect(function()
404
- local now = os.clock()
405
-
406
- for player, data in playerData do
407
- if (now - data.lastCheck) < CHECK_INTERVAL then
408
- continue
409
- end
410
-
411
- local character = player.Character
412
- if not character then
413
- continue
414
- end
415
-
416
- local root = character:FindFirstChild("HumanoidRootPart")
417
- if not root then
418
- continue
419
- end
420
-
421
- local dt = now - data.lastCheck
422
- local displacement = root.Position - data.lastPosition
423
- local horizontalSpeed = Vector3.new(displacement.X, 0, displacement.Z).Magnitude / dt
424
- local verticalSpeed = math.abs(displacement.Y) / dt
425
-
426
- if horizontalSpeed > MAX_SPEED or verticalSpeed > MAX_VERTICAL_SPEED then
427
- data.violations += 1
428
- warn(`[MovementValidator] {player.Name}: speed violation #{data.violations} (h={math.floor(horizontalSpeed)}, v={math.floor(verticalSpeed)})`)
429
-
430
- if data.violations >= VIOLATION_THRESHOLD then
431
- -- Teleport player back to last valid position
432
- root.CFrame = CFrame.new(data.lastPosition)
433
- -- Or kick for persistent abuse:
434
- -- player:Kick("Movement anomaly detected.")
435
- end
436
- else
437
- -- Decay violations over time for legitimate edge cases
438
- data.violations = math.max(0, data.violations - 1)
439
- data.lastPosition = root.Position
440
- end
441
-
442
- data.lastCheck = now
443
- end
444
- end)
445
- ```
446
-
447
- ### Damage Validation
448
-
449
- ```luau
450
- -- Server decides damage, not the client.
451
-
452
- local function calculateDamage(attacker: Player, weapon: Tool, target: Player): number?
453
- local weaponConfig = WeaponDatabase[weapon.Name]
454
- if not weaponConfig then
455
- return nil
456
- end
457
-
458
- -- Server checks weapon cooldown
459
- local lastFire = weapon:GetAttribute("LastFired") or 0
460
- if os.clock() - lastFire < weaponConfig.Cooldown then
461
- return nil
462
- end
463
-
464
- -- Server checks range
465
- local attackerRoot = attacker.Character and attacker.Character:FindFirstChild("HumanoidRootPart")
466
- local targetRoot = target.Character and target.Character:FindFirstChild("HumanoidRootPart")
467
- if not attackerRoot or not targetRoot then
468
- return nil
469
- end
470
-
471
- local distance = (attackerRoot.Position - targetRoot.Position).Magnitude
472
- if distance > weaponConfig.Range then
473
- return nil
474
- end
475
-
476
- -- Server calculates damage
477
- weapon:SetAttribute("LastFired", os.clock())
478
- return weaponConfig.BaseDamage
479
- end
480
- ```
481
-
482
- ### Currency Transactions
483
-
484
- ```luau
485
- -- WRONG: Client tells server how much to add
486
- CurrencyRemote.OnServerEvent:Connect(function(player, amount)
487
- player.leaderstats.Gold.Value += amount -- exploiter sends 999999
488
- end)
489
-
490
- -- RIGHT: Server calculates the reward
491
- QuestCompleteRemote.OnServerEvent:Connect(function(player, questId)
492
- -- Validate quest ID type
493
- if typeof(questId) ~= "string" then
494
- return
495
- end
496
-
497
- -- Server checks quest state
498
- local questData = PlayerQuestData[player]
499
- if not questData or not questData[questId] then
500
- return
501
- end
502
-
503
- if questData[questId].completed then
504
- return -- already claimed
505
- end
506
-
507
- -- Server looks up the reward from its own data
508
- local questConfig = QuestDatabase[questId]
509
- if not questConfig then
510
- return
511
- end
512
-
513
- -- Server awards the reward
514
- questData[questId].completed = true
515
- player.leaderstats.Gold.Value += questConfig.Reward
516
- end)
517
- ```
518
-
519
- ### Inventory Operations
520
-
521
- ```luau
522
- -- Server-side trade validation
523
- local function executeTrade(playerA: Player, playerB: Player, itemIdA: string, itemIdB: string): boolean
524
- -- Both players must be alive and in range
525
- if not Validator.characterAlive(playerA) or not Validator.characterAlive(playerB) then
526
- return false
527
- end
528
-
529
- -- Verify ownership on the server
530
- local invA = playerA:FindFirstChild("Inventory")
531
- local invB = playerB:FindFirstChild("Inventory")
532
- if not invA or not invB then
533
- return false
534
- end
535
-
536
- ---
537
-
538
- ## Rate Limiting
539
-
540
- Roblox's built-in throttle (~500 req/sec per client) is NOT a substitute for custom rate limiting. Players can still spam remotes at hundreds of requests per second. You need application-level throttling.
541
-
542
- ### Pattern 1: Per-Player Cooldown Table
543
-
544
- Simple and effective for most games. Each remote has a minimum time between calls per player.
545
-
546
- ```luau
547
- local cooldowns: {[Player]: {[string]: number}} = {}
548
- local COOLDOWN = 0.2 -- seconds between calls
549
-
550
- local function isThrottled(player: Player, remoteName: string): boolean
551
- local now = os.clock()
552
- if not cooldowns[player] then
553
- cooldowns[player] = {}
554
- end
555
-
556
- local lastCall = cooldowns[player][remoteName]
557
- if lastCall and (now - lastCall) < COOLDOWN then
558
- return true -- throttled
559
- end
560
-
561
- cooldowns[player][remoteName] = now
562
- return false
563
- end
564
-
565
- -- Clean up when player leaves
566
- Players.PlayerRemoving:Connect(function(player)
567
- cooldowns[player] = nil
568
- end)
569
-
570
- -- Usage
571
- BuyItem.OnServerEvent:Connect(function(player, itemId)
572
- if isThrottled(player, "BuyItem") then return end
573
- -- process purchase
574
- end)
575
- ```
576
-
577
- ### Pattern 2: Declarative Remote Definitions
578
-
579
- Define all remotes in one place with rate limits, validation, and allowed states. Cleaner than scattered OnServerEvent handlers.
580
-
581
- ```luau
582
- type RemoteDef = {
583
- RateLimit: number?,
584
- Validate: (Player, ...any) -> boolean,
585
- Handler: (Player, ...any) -> (),
586
- }
587
-
588
- local Remotes: {[string]: RemoteDef} = {
589
- BuyItem = {
590
- RateLimit = 0.5,
591
- Validate = function(player, itemId)
592
- return typeof(itemId) == "string" and #itemId < 50
593
- end,
594
- Handler = function(player, itemId)
595
- -- process purchase
596
- end,
597
- },
598
- EquipTool = {
599
- RateLimit = 0.3,
600
- Validate = function(player, toolId)
601
- return typeof(toolId) == "string"
602
- end,
603
- Handler = function(player, toolId)
604
- -- equip tool
605
- end,
606
- },
607
- }
608
-
609
- -- Wire up automatically
610
- for name, def in Remotes do
611
- local remote = ReplicatedStorage:WaitForChild(name)
612
- remote.OnServerEvent:Connect(function(player, ...)
613
- if def.RateLimit and isThrottled(player, name) then return end
614
- if not def.Validate(player, ...) then return end
615
- def.Handler(player, ...)
616
- end)
617
- end
618
- ```
619
-
620
- ### Pattern 3: Suspicion Scoring
621
-
622
- For high-stakes games. Track suspicious behavior over time instead of hard-blocking.
623
-
624
- ```luau
625
- local suspicion: {[Player]: number} = {}
626
- local SUSPICION_THRESHOLD = 10
627
- local DECAY_RATE = 1 -- points lost per second
628
-
629
- local function addSuspicion(player: Player, amount: number, reason: string)
630
- suspicion[player] = (suspicion[player] or 0) + amount
631
- if suspicion[player] >= SUSPICION_THRESHOLD then
632
- warn(`High suspicion for {player.Name}: {reason}`)
633
- end
634
- end
635
-
636
- -- In remote handler
637
- BuyItem.OnServerEvent:Connect(function(player, itemId)
638
- if isThrottled(player, "BuyItem") then
639
- addSuspicion(player, 2, "rate limit exceeded")
640
- return
641
- end
642
- -- normal processing
643
- end)
644
-
645
- -- Decay suspicion over time
646
- task.spawn(function()
647
- while true do
648
- task.wait(1)
649
- for player, score in suspicion do
650
- suspicion[player] = math.max(0, score - DECAY_RATE)
651
- end
652
- end
653
- end)
654
- ```
655
-
656
- ### What NOT to Do
657
-
658
- ```luau
659
- -- BAD: no rate limiting at all
660
- BuyItem.OnServerEvent:Connect(function(player, itemId)
661
- -- exploiter can call this 1000 times/second
662
- grantItem(player, itemId)
663
- end)
664
-
665
- -- BAD: client-side rate limiting (exploiter bypasses)
666
- -- Rate limiting MUST be server-side
667
- ```
668
-
669
- Source: Roblox Server-Side Detection Guide (Roblox/creator-docs, MIT), DevForum rate limiting patterns
1
+ ---
2
+ name: roblox-networking
3
+ description: >
4
+ Server-authoritative networking, RemoteEvent validation, rate limiting, exploit prevention,
5
+ security hardening.
6
+ last_reviewed: 2026-05-22
7
+ ---
8
+
9
+ <!-- Source: brockmartin/roblox-game-skill (MIT) -->
10
+
11
+ # Roblox Networking & Security Reference
12
+
13
+ ---
14
+
15
+ ## Overview
16
+
17
+ **Load this reference when:**
18
+
19
+ - Validating RemoteEvent/RemoteFunction input on the server
20
+ - Implementing rate limiting or anti-exploit measures
21
+ - Designing server-authoritative systems (damage, currency, inventory)
22
+ - Hardening existing networking code against exploiters
23
+
24
+ This document covers server-side validation, rate limiting, suspicion scoring, and server-authoritative design patterns. For player lifecycle (PlayerAdded/Removing), see **roblox-architecture**.
25
+
26
+ ---
27
+
28
+ ## Quick Reference
29
+
30
+ **Load Full Reference below only when you need specific validation module code or rate limiting implementations.**
31
+
32
+ Key rules:
33
+ - NEVER trust the client. Every RemoteEvent arg is attacker-controlled.
34
+ - Validate: type, range, ownership, cooldown on EVERY server handler.
35
+ - Server-authoritative: server decides outcomes. Client is display-only.
36
+ - Rate limit all remotes. Per-player cooldown table minimum.
37
+ - Damage: server calculates from weapon stats + distance + cooldown. Never accept damage values from client.
38
+ - Currency: all math server-side. Client displays only.
39
+ - Movement: validate distance/speed against physics. Flag teleportation.
40
+ - Use `t` library for composable type checks on remote args.
41
+ - Suspicion scoring: accumulate violations, kick/ban at threshold. Don't instant-kick on first offense.
42
+ - Exploiters can: fire any remote, read all client code, modify any client state, speed/fly/teleport.
43
+
44
+ ---
45
+
46
+ ## Full Reference
47
+
48
+ ## Security Hardening
49
+
50
+ ### Never Trust the Client
51
+
52
+ Every RemoteEvent payload is attacker-controlled. Validate type, range, ownership, and cooldown on the server for every request.
53
+
54
+ - **Modify any LocalScript** -- injecting code, changing variables, hooking functions.
55
+ - **Fire any RemoteEvent with arbitrary arguments** -- types, values, and counts are all attacker-controlled.
56
+ - **Speed hack, fly, and teleport** -- the character's physics can be overridden entirely on the client.
57
+ - **See all client-accessible code** -- anything in `StarterPlayerScripts`, `StarterGui`, `ReplicatedStorage`, or `ReplicatedFirst` is fully readable.
58
+ - **Read and modify any client-side state** -- health displays, cooldown timers, UI flags.
59
+ - **Intercept and replay network traffic** -- RemoteSpy tools let exploiters see every remote call and replay or modify them.
60
+
61
+ **The client is a display layer, not a trusted authority.** It renders the world and collects input. The server decides what actually happens.
62
+
63
+ A useful mental model: treat every `RemoteEvent:FireServer()` call as if it were an HTTP request from an anonymous stranger on the internet. Validate everything. Assume nothing.
64
+
65
+ ---
66
+
67
+ ### RemoteEvent Validation Patterns
68
+
69
+ > **For runtime type checking, the `t` library is vendored** at `vendor/t/t.lua` (osyrisrblx/t v3.1.1, MIT). It provides composable type checks (`t.string`, `t.number`, `t.interface({...})`) that are cleaner than manual typeof() chains. The agent can place it when relevant.
70
+
71
+ ### The Problem
72
+
73
+ A bare remote handler like this is exploitable:
74
+
75
+ ```luau
76
+ -- BAD: No validation at all
77
+ DamageRemote.OnServerEvent:Connect(function(player, targetName, damage)
78
+ local target = Players:FindFirstChild(targetName)
79
+ target.Character.Humanoid:TakeDamage(damage)
80
+ end)
81
+ ```
82
+
83
+ An exploiter can fire this with any target name and any damage value, instantly killing anyone.
84
+
85
+ ### Production-Ready Validation Module
86
+
87
+ Place this in `ServerScriptService`:
88
+
89
+ ```luau
90
+ -- ServerScriptService/Modules/RemoteValidator.luau
91
+
92
+ local RemoteValidator = {}
93
+
94
+ --[[ -----------------------------------------------------------------------
95
+ Type Checking
96
+ Validates that arguments match expected types.
97
+ ----------------------------------------------------------------------- ]]
98
+
99
+ type TypeSpec = string | (value: any) -> boolean
100
+
101
+ function RemoteValidator.checkType(value: any, expected: TypeSpec): boolean
102
+ if typeof(expected) == "function" then
103
+ return expected(value)
104
+ end
105
+ return typeof(value) == expected
106
+ end
107
+
108
+ function RemoteValidator.validateArgs(
109
+ args: { any },
110
+ schema: { { name: string, type: TypeSpec, optional: boolean? } }
111
+ ): (boolean, string?)
112
+ for i, spec in schema do
113
+ local value = args[i]
114
+
115
+ if value == nil then
116
+ if not spec.optional then
117
+ return false, `Missing required argument: {spec.name}`
118
+ end
119
+ continue
120
+ end
121
+
122
+ if not RemoteValidator.checkType(value, spec.type) then
123
+ return false, `Invalid type for {spec.name}: expected {tostring(spec.type)}, got {typeof(value)}`
124
+ end
125
+ end
126
+
127
+ -- Reject extra arguments that were not declared in the schema
128
+ if #args > #schema then
129
+ return false, `Too many arguments: expected {#schema}, got {#args}`
130
+ end
131
+
132
+ return true, nil
133
+ end
134
+
135
+ --[[ -----------------------------------------------------------------------
136
+ Range Checking
137
+ Validates that numeric values fall within acceptable bounds.
138
+ ----------------------------------------------------------------------- ]]
139
+
140
+ function RemoteValidator.checkRange(value: number, min: number, max: number): boolean
141
+ return typeof(value) == "number"
142
+ and value == value -- NaN check
143
+ and value >= min
144
+ and value <= max
145
+ end
146
+
147
+ function RemoteValidator.checkIntegerRange(value: number, min: number, max: number): boolean
148
+ return RemoteValidator.checkRange(value, min, max)
149
+ and math.floor(value) == value
150
+ end
151
+
152
+ --[[ -----------------------------------------------------------------------
153
+ Cooldown Tracking
154
+ Per-player, per-action cooldown enforcement.
155
+ ----------------------------------------------------------------------- ]]
156
+
157
+ local cooldowns: { [Player]: { [string]: number } } = {}
158
+
159
+ function RemoteValidator.checkCooldown(player: Player, action: string, cooldownSeconds: number): boolean
160
+ local now = os.clock()
161
+ local playerCooldowns = cooldowns[player]
162
+
163
+ if not playerCooldowns then
164
+ playerCooldowns = {}
165
+ cooldowns[player] = playerCooldowns
166
+ end
167
+
168
+ local lastUsed = playerCooldowns[action]
169
+ if lastUsed and (now - lastUsed) < cooldownSeconds then
170
+ return false
171
+ end
172
+
173
+ playerCooldowns[action] = now
174
+ return true
175
+ end
176
+
177
+ function RemoteValidator.clearPlayerCooldowns(player: Player)
178
+ cooldowns[player] = nil
179
+ end
180
+
181
+ --[[ -----------------------------------------------------------------------
182
+ Existence Checks
183
+ Validates that targets, objects, and instances actually exist.
184
+ ----------------------------------------------------------------------- ]]
185
+
186
+ function RemoteValidator.playerExists(playerName: string): Player?
187
+ local Players = game:GetService("Players")
188
+ return Players:FindFirstChild(playerName) :: Player?
189
+ end
190
+
191
+ function RemoteValidator.characterAlive(player: Player): boolean
192
+ local character = player.Character
193
+ if not character then
194
+ return false
195
+ end
196
+
197
+ local humanoid = character:FindFirstChildOfClass("Humanoid")
198
+ if not humanoid then
199
+ return false
200
+ end
201
+
202
+ return humanoid.Health > 0
203
+ end
204
+
205
+ function RemoteValidator.instanceExists(parent: Instance, name: string, className: string?): Instance?
206
+ local child = parent:FindFirstChild(name)
207
+ if not child then
208
+ return nil
209
+ end
210
+
211
+ if className and not child:IsA(className) then
212
+ return nil
213
+ end
214
+
215
+ return child
216
+ end
217
+
218
+ --[[ -----------------------------------------------------------------------
219
+ Authorization
220
+ Checks if a player is allowed to perform an action.
221
+ ----------------------------------------------------------------------- ]]
222
+
223
+ function RemoteValidator.playerOwnsItem(player: Player, itemId: string, inventoryFolder: Folder?): boolean
224
+ local folder = inventoryFolder or player:FindFirstChild("Inventory") :: Folder?
225
+ if not folder then
226
+ return false
227
+ end
228
+
229
+ return folder:FindFirstChild(itemId) ~= nil
230
+ end
231
+
232
+ function RemoteValidator.playerHasAttribute(player: Player, attribute: string, expectedValue: any?): boolean
233
+ local value = player:GetAttribute(attribute)
234
+ if expectedValue ~= nil then
235
+ return value == expectedValue
236
+ end
237
+ return value ~= nil
238
+ end
239
+
240
+ --[[ -----------------------------------------------------------------------
241
+ Distance Check
242
+ Validates that two positions are within an acceptable range.
243
+ ----------------------------------------------------------------------- ]]
244
+
245
+ function RemoteValidator.withinRange(posA: Vector3, posB: Vector3, maxDistance: number): boolean
246
+ return (posA - posB).Magnitude <= maxDistance
247
+ end
248
+
249
+ function RemoteValidator.playerWithinRange(player: Player, targetPos: Vector3, maxDistance: number): boolean
250
+ local character = player.Character
251
+ if not character then
252
+ return false
253
+ end
254
+
255
+ local root = character:FindFirstChild("HumanoidRootPart")
256
+ if not root then
257
+ return false
258
+ end
259
+
260
+ return RemoteValidator.withinRange(root.Position, targetPos, maxDistance)
261
+ end
262
+
263
+ --[[ -----------------------------------------------------------------------
264
+ Cleanup
265
+ ----------------------------------------------------------------------- ]]
266
+
267
+ game:GetService("Players").PlayerRemoving:Connect(function(player)
268
+ RemoteValidator.clearPlayerCooldowns(player)
269
+ end)
270
+
271
+ return RemoteValidator
272
+ ```
273
+
274
+ ### Using the Validation Module
275
+
276
+ ```luau
277
+ -- ServerScriptService/RemoteHandlers/DamageHandler.server.luau
278
+
279
+ local ReplicatedStorage = game:GetService("ReplicatedStorage")
280
+ local ServerScriptService = game:GetService("ServerScriptService")
281
+
282
+ local Validator = require(ServerScriptService.Modules.RemoteValidator)
283
+ local DamageRemote = ReplicatedStorage.Remotes.DealDamage
284
+
285
+ local MAX_DAMAGE = 50
286
+ local DAMAGE_COOLDOWN = 0.5 -- seconds
287
+ local ATTACK_RANGE = 15 -- studs
288
+
289
+ local ARG_SCHEMA = {
290
+ { name = "targetPlayer", type = "Instance" },
291
+ { name = "damage", type = "number" },
292
+ }
293
+
294
+ DamageRemote.OnServerEvent:Connect(function(player: Player, ...: any)
295
+ local args = { ... }
296
+
297
+ -- 1. Validate argument types
298
+ local valid, err = Validator.validateArgs(args, ARG_SCHEMA)
299
+ if not valid then
300
+ warn(`[DamageHandler] {player.Name}: {err}`)
301
+ return
302
+ end
303
+
304
+ local targetPlayer: Player = args[1]
305
+ local damage: number = args[2]
306
+
307
+ -- 2. Validate the target is actually a Player
308
+ if not targetPlayer:IsA("Player") then
309
+ return
310
+ end
311
+
312
+ -- 3. Validate damage range
313
+ if not Validator.checkIntegerRange(damage, 1, MAX_DAMAGE) then
314
+ warn(`[DamageHandler] {player.Name}: damage out of range ({damage})`)
315
+ return
316
+ end
317
+
318
+ -- 4. Cooldown check
319
+ if not Validator.checkCooldown(player, "DealDamage", DAMAGE_COOLDOWN) then
320
+ return
321
+ end
322
+
323
+ -- 5. Verify attacker is alive
324
+ if not Validator.characterAlive(player) then
325
+ return
326
+ end
327
+
328
+ -- 6. Verify target is alive
329
+ if not Validator.characterAlive(targetPlayer) then
330
+ return
331
+ end
332
+
333
+ -- 7. Range check -- attacker must be near the target
334
+ local targetRoot = targetPlayer.Character and targetPlayer.Character:FindFirstChild("HumanoidRootPart")
335
+ if not targetRoot then
336
+ return
337
+ end
338
+
339
+ if not Validator.playerWithinRange(player, targetRoot.Position, ATTACK_RANGE) then
340
+ warn(`[DamageHandler] {player.Name}: target out of range`)
341
+ return
342
+ end
343
+
344
+ -- 8. Authorization -- verify the player has a weapon equipped
345
+ local character = player.Character
346
+ local weapon = character and character:FindFirstChildOfClass("Tool")
347
+ if not weapon or not weapon:GetAttribute("CanDealDamage") then
348
+ warn(`[DamageHandler] {player.Name}: no valid weapon equipped`)
349
+ return
350
+ end
351
+
352
+ -- 9. Server calculates actual damage (never trust client damage value directly)
353
+ local serverDamage = math.min(damage, weapon:GetAttribute("MaxDamage") or MAX_DAMAGE)
354
+
355
+ -- 10. Apply damage
356
+ local targetHumanoid = targetPlayer.Character:FindFirstChildOfClass("Humanoid")
357
+ if targetHumanoid then
358
+ targetHumanoid:TakeDamage(serverDamage)
359
+ end
360
+ end)
361
+ ```
362
+
363
+ ---
364
+
365
+ ### Server-Authoritative Design
366
+
367
+ The server owns all game state. The client requests actions; the server decides outcomes.
368
+
369
+ ### Movement Validation
370
+
371
+ ```luau
372
+ -- ServerScriptService/Security/MovementValidator.server.luau
373
+
374
+ local Players = game:GetService("Players")
375
+ local RunService = game:GetService("RunService")
376
+
377
+ local MAX_SPEED = 50 -- studs per second (walk + sprint + tolerance)
378
+ local MAX_VERTICAL_SPEED = 100 -- studs per second (jumping/falling tolerance)
379
+ local VIOLATION_THRESHOLD = 5 -- strikes before action
380
+ local CHECK_INTERVAL = 0.5 -- seconds between checks
381
+
382
+ local playerData: { [Player]: {
383
+ lastPosition: Vector3,
384
+ lastCheck: number,
385
+ violations: number,
386
+ } } = {}
387
+
388
+ Players.PlayerAdded:Connect(function(player)
389
+ player.CharacterAdded:Connect(function(character)
390
+ local root = character:WaitForChild("HumanoidRootPart")
391
+ playerData[player] = {
392
+ lastPosition = root.Position,
393
+ lastCheck = os.clock(),
394
+ violations = 0,
395
+ }
396
+ end)
397
+ end)
398
+
399
+ Players.PlayerRemoving:Connect(function(player)
400
+ playerData[player] = nil
401
+ end)
402
+
403
+ RunService.Heartbeat:Connect(function()
404
+ local now = os.clock()
405
+
406
+ for player, data in playerData do
407
+ if (now - data.lastCheck) < CHECK_INTERVAL then
408
+ continue
409
+ end
410
+
411
+ local character = player.Character
412
+ if not character then
413
+ continue
414
+ end
415
+
416
+ local root = character:FindFirstChild("HumanoidRootPart")
417
+ if not root then
418
+ continue
419
+ end
420
+
421
+ local dt = now - data.lastCheck
422
+ local displacement = root.Position - data.lastPosition
423
+ local horizontalSpeed = Vector3.new(displacement.X, 0, displacement.Z).Magnitude / dt
424
+ local verticalSpeed = math.abs(displacement.Y) / dt
425
+
426
+ if horizontalSpeed > MAX_SPEED or verticalSpeed > MAX_VERTICAL_SPEED then
427
+ data.violations += 1
428
+ warn(`[MovementValidator] {player.Name}: speed violation #{data.violations} (h={math.floor(horizontalSpeed)}, v={math.floor(verticalSpeed)})`)
429
+
430
+ if data.violations >= VIOLATION_THRESHOLD then
431
+ -- Teleport player back to last valid position
432
+ root.CFrame = CFrame.new(data.lastPosition)
433
+ -- Or kick for persistent abuse:
434
+ -- player:Kick("Movement anomaly detected.")
435
+ end
436
+ else
437
+ -- Decay violations over time for legitimate edge cases
438
+ data.violations = math.max(0, data.violations - 1)
439
+ data.lastPosition = root.Position
440
+ end
441
+
442
+ data.lastCheck = now
443
+ end
444
+ end)
445
+ ```
446
+
447
+ ### Damage Validation
448
+
449
+ ```luau
450
+ -- Server decides damage, not the client.
451
+
452
+ local function calculateDamage(attacker: Player, weapon: Tool, target: Player): number?
453
+ local weaponConfig = WeaponDatabase[weapon.Name]
454
+ if not weaponConfig then
455
+ return nil
456
+ end
457
+
458
+ -- Server checks weapon cooldown
459
+ local lastFire = weapon:GetAttribute("LastFired") or 0
460
+ if os.clock() - lastFire < weaponConfig.Cooldown then
461
+ return nil
462
+ end
463
+
464
+ -- Server checks range
465
+ local attackerRoot = attacker.Character and attacker.Character:FindFirstChild("HumanoidRootPart")
466
+ local targetRoot = target.Character and target.Character:FindFirstChild("HumanoidRootPart")
467
+ if not attackerRoot or not targetRoot then
468
+ return nil
469
+ end
470
+
471
+ local distance = (attackerRoot.Position - targetRoot.Position).Magnitude
472
+ if distance > weaponConfig.Range then
473
+ return nil
474
+ end
475
+
476
+ -- Server calculates damage
477
+ weapon:SetAttribute("LastFired", os.clock())
478
+ return weaponConfig.BaseDamage
479
+ end
480
+ ```
481
+
482
+ ### Currency Transactions
483
+
484
+ ```luau
485
+ -- WRONG: Client tells server how much to add
486
+ CurrencyRemote.OnServerEvent:Connect(function(player, amount)
487
+ player.leaderstats.Gold.Value += amount -- exploiter sends 999999
488
+ end)
489
+
490
+ -- RIGHT: Server calculates the reward
491
+ QuestCompleteRemote.OnServerEvent:Connect(function(player, questId)
492
+ -- Validate quest ID type
493
+ if typeof(questId) ~= "string" then
494
+ return
495
+ end
496
+
497
+ -- Server checks quest state
498
+ local questData = PlayerQuestData[player]
499
+ if not questData or not questData[questId] then
500
+ return
501
+ end
502
+
503
+ if questData[questId].completed then
504
+ return -- already claimed
505
+ end
506
+
507
+ -- Server looks up the reward from its own data
508
+ local questConfig = QuestDatabase[questId]
509
+ if not questConfig then
510
+ return
511
+ end
512
+
513
+ -- Server awards the reward
514
+ questData[questId].completed = true
515
+ player.leaderstats.Gold.Value += questConfig.Reward
516
+ end)
517
+ ```
518
+
519
+ ### Inventory Operations
520
+
521
+ ```luau
522
+ -- Server-side trade validation
523
+ local function executeTrade(playerA: Player, playerB: Player, itemIdA: string, itemIdB: string): boolean
524
+ -- Both players must be alive and in range
525
+ if not Validator.characterAlive(playerA) or not Validator.characterAlive(playerB) then
526
+ return false
527
+ end
528
+
529
+ -- Verify ownership on the server
530
+ local invA = playerA:FindFirstChild("Inventory")
531
+ local invB = playerB:FindFirstChild("Inventory")
532
+ if not invA or not invB then
533
+ return false
534
+ end
535
+
536
+ ---
537
+
538
+ ## Rate Limiting
539
+
540
+ Roblox's built-in throttle (~500 req/sec per client) is NOT a substitute for custom rate limiting. Players can still spam remotes at hundreds of requests per second. You need application-level throttling.
541
+
542
+ ### Pattern 1: Per-Player Cooldown Table
543
+
544
+ Simple and effective for most games. Each remote has a minimum time between calls per player.
545
+
546
+ ```luau
547
+ local cooldowns: {[Player]: {[string]: number}} = {}
548
+ local COOLDOWN = 0.2 -- seconds between calls
549
+
550
+ local function isThrottled(player: Player, remoteName: string): boolean
551
+ local now = os.clock()
552
+ if not cooldowns[player] then
553
+ cooldowns[player] = {}
554
+ end
555
+
556
+ local lastCall = cooldowns[player][remoteName]
557
+ if lastCall and (now - lastCall) < COOLDOWN then
558
+ return true -- throttled
559
+ end
560
+
561
+ cooldowns[player][remoteName] = now
562
+ return false
563
+ end
564
+
565
+ -- Clean up when player leaves
566
+ Players.PlayerRemoving:Connect(function(player)
567
+ cooldowns[player] = nil
568
+ end)
569
+
570
+ -- Usage
571
+ BuyItem.OnServerEvent:Connect(function(player, itemId)
572
+ if isThrottled(player, "BuyItem") then return end
573
+ -- process purchase
574
+ end)
575
+ ```
576
+
577
+ ### Pattern 2: Declarative Remote Definitions
578
+
579
+ Define all remotes in one place with rate limits, validation, and allowed states. Cleaner than scattered OnServerEvent handlers.
580
+
581
+ ```luau
582
+ type RemoteDef = {
583
+ RateLimit: number?,
584
+ Validate: (Player, ...any) -> boolean,
585
+ Handler: (Player, ...any) -> (),
586
+ }
587
+
588
+ local Remotes: {[string]: RemoteDef} = {
589
+ BuyItem = {
590
+ RateLimit = 0.5,
591
+ Validate = function(player, itemId)
592
+ return typeof(itemId) == "string" and #itemId < 50
593
+ end,
594
+ Handler = function(player, itemId)
595
+ -- process purchase
596
+ end,
597
+ },
598
+ EquipTool = {
599
+ RateLimit = 0.3,
600
+ Validate = function(player, toolId)
601
+ return typeof(toolId) == "string"
602
+ end,
603
+ Handler = function(player, toolId)
604
+ -- equip tool
605
+ end,
606
+ },
607
+ }
608
+
609
+ -- Wire up automatically
610
+ for name, def in Remotes do
611
+ local remote = ReplicatedStorage:WaitForChild(name)
612
+ remote.OnServerEvent:Connect(function(player, ...)
613
+ if def.RateLimit and isThrottled(player, name) then return end
614
+ if not def.Validate(player, ...) then return end
615
+ def.Handler(player, ...)
616
+ end)
617
+ end
618
+ ```
619
+
620
+ ### Pattern 3: Suspicion Scoring
621
+
622
+ For high-stakes games. Track suspicious behavior over time instead of hard-blocking.
623
+
624
+ ```luau
625
+ local suspicion: {[Player]: number} = {}
626
+ local SUSPICION_THRESHOLD = 10
627
+ local DECAY_RATE = 1 -- points lost per second
628
+
629
+ local function addSuspicion(player: Player, amount: number, reason: string)
630
+ suspicion[player] = (suspicion[player] or 0) + amount
631
+ if suspicion[player] >= SUSPICION_THRESHOLD then
632
+ warn(`High suspicion for {player.Name}: {reason}`)
633
+ end
634
+ end
635
+
636
+ -- In remote handler
637
+ BuyItem.OnServerEvent:Connect(function(player, itemId)
638
+ if isThrottled(player, "BuyItem") then
639
+ addSuspicion(player, 2, "rate limit exceeded")
640
+ return
641
+ end
642
+ -- normal processing
643
+ end)
644
+
645
+ -- Decay suspicion over time
646
+ task.spawn(function()
647
+ while true do
648
+ task.wait(1)
649
+ for player, score in suspicion do
650
+ suspicion[player] = math.max(0, score - DECAY_RATE)
651
+ end
652
+ end
653
+ end)
654
+ ```
655
+
656
+ ### What NOT to Do
657
+
658
+ ```luau
659
+ -- BAD: no rate limiting at all
660
+ BuyItem.OnServerEvent:Connect(function(player, itemId)
661
+ -- exploiter can call this 1000 times/second
662
+ grantItem(player, itemId)
663
+ end)
664
+
665
+ -- BAD: client-side rate limiting (exploiter bypasses)
666
+ -- Rate limiting MUST be server-side
667
+ ```
668
+
669
+ Source: Roblox Server-Side Detection Guide (Roblox/creator-docs, MIT), DevForum rate limiting patterns