@darkclaw921/umgt 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +161 -2
  3. package/architecture.md +105 -0
  4. package/bin/lm-game-hub.js +7 -0
  5. package/dist/server/activity.d.ts +17 -0
  6. package/dist/server/activity.js +55 -0
  7. package/dist/server/activity.js.map +1 -0
  8. package/dist/server/agents/activity.d.ts +36 -0
  9. package/dist/server/agents/activity.js +117 -0
  10. package/dist/server/agents/activity.js.map +1 -0
  11. package/dist/server/agents/claude.d.ts +5 -0
  12. package/dist/server/agents/claude.js +107 -0
  13. package/dist/server/agents/claude.js.map +1 -0
  14. package/dist/server/agents/codex.d.ts +5 -0
  15. package/dist/server/agents/codex.js +128 -0
  16. package/dist/server/agents/codex.js.map +1 -0
  17. package/dist/server/agents/core-tools.d.ts +23 -0
  18. package/dist/server/agents/core-tools.js +298 -0
  19. package/dist/server/agents/core-tools.js.map +1 -0
  20. package/dist/server/agents/core.d.ts +9 -0
  21. package/dist/server/agents/core.js +106 -0
  22. package/dist/server/agents/core.js.map +1 -0
  23. package/dist/server/agents/index.d.ts +5 -0
  24. package/dist/server/agents/index.js +41 -0
  25. package/dist/server/agents/index.js.map +1 -0
  26. package/dist/server/agents/kimi.d.ts +5 -0
  27. package/dist/server/agents/kimi.js +123 -0
  28. package/dist/server/agents/kimi.js.map +1 -0
  29. package/dist/server/agents/rpc.d.ts +19 -0
  30. package/dist/server/agents/rpc.js +102 -0
  31. package/dist/server/agents/rpc.js.map +1 -0
  32. package/dist/server/agents/types.d.ts +51 -0
  33. package/dist/server/agents/types.js +7 -0
  34. package/dist/server/agents/types.js.map +1 -0
  35. package/dist/server/app.d.ts +21 -0
  36. package/dist/server/app.js +38 -0
  37. package/dist/server/app.js.map +1 -0
  38. package/dist/server/asset-catalog.d.ts +5 -0
  39. package/dist/server/asset-catalog.js +525 -0
  40. package/dist/server/asset-catalog.js.map +1 -0
  41. package/dist/server/asset-media.d.ts +16 -0
  42. package/dist/server/asset-media.js +170 -0
  43. package/dist/server/asset-media.js.map +1 -0
  44. package/dist/server/asset-selection.d.ts +71 -0
  45. package/dist/server/asset-selection.js +273 -0
  46. package/dist/server/asset-selection.js.map +1 -0
  47. package/dist/server/assets.d.ts +69 -0
  48. package/dist/server/assets.js +669 -0
  49. package/dist/server/assets.js.map +1 -0
  50. package/dist/server/browser-gateway.d.ts +82 -0
  51. package/dist/server/browser-gateway.js +285 -0
  52. package/dist/server/browser-gateway.js.map +1 -0
  53. package/dist/server/chat-computer.d.ts +13 -0
  54. package/dist/server/chat-computer.js +29 -0
  55. package/dist/server/chat-computer.js.map +1 -0
  56. package/dist/server/chat-games.d.ts +38 -0
  57. package/dist/server/chat-games.js +202 -0
  58. package/dist/server/chat-games.js.map +1 -0
  59. package/dist/server/chat.d.ts +42 -0
  60. package/dist/server/chat.js +318 -0
  61. package/dist/server/chat.js.map +1 -0
  62. package/dist/server/cli.d.ts +14 -0
  63. package/dist/server/cli.js +117 -0
  64. package/dist/server/cli.js.map +1 -0
  65. package/dist/server/core-client.d.ts +69 -0
  66. package/dist/server/core-client.js +357 -0
  67. package/dist/server/core-client.js.map +1 -0
  68. package/dist/server/directory-picker.d.ts +14 -0
  69. package/dist/server/directory-picker.js +113 -0
  70. package/dist/server/directory-picker.js.map +1 -0
  71. package/dist/server/engine.d.ts +36 -0
  72. package/dist/server/engine.js +507 -0
  73. package/dist/server/engine.js.map +1 -0
  74. package/dist/server/events.d.ts +12 -0
  75. package/dist/server/events.js +137 -0
  76. package/dist/server/events.js.map +1 -0
  77. package/dist/server/game-launch.d.ts +16 -0
  78. package/dist/server/game-launch.js +143 -0
  79. package/dist/server/game-launch.js.map +1 -0
  80. package/dist/server/html5.d.ts +9 -0
  81. package/dist/server/html5.js +107 -0
  82. package/dist/server/html5.js.map +1 -0
  83. package/dist/server/interview.d.ts +241 -0
  84. package/dist/server/interview.js +190 -0
  85. package/dist/server/interview.js.map +1 -0
  86. package/dist/server/mcp.d.ts +27 -0
  87. package/dist/server/mcp.js +91 -0
  88. package/dist/server/mcp.js.map +1 -0
  89. package/dist/server/plugins.d.ts +8 -0
  90. package/dist/server/plugins.js +144 -0
  91. package/dist/server/plugins.js.map +1 -0
  92. package/dist/server/process.d.ts +15 -0
  93. package/dist/server/process.js +89 -0
  94. package/dist/server/process.js.map +1 -0
  95. package/dist/server/publication-materials.d.ts +18 -0
  96. package/dist/server/publication-materials.js +104 -0
  97. package/dist/server/publication-materials.js.map +1 -0
  98. package/dist/server/publication-mcp.d.ts +3 -0
  99. package/dist/server/publication-mcp.js +43 -0
  100. package/dist/server/publication-mcp.js.map +1 -0
  101. package/dist/server/publication.d.ts +54 -0
  102. package/dist/server/publication.js +334 -0
  103. package/dist/server/publication.js.map +1 -0
  104. package/dist/server/push.d.ts +81 -0
  105. package/dist/server/push.js +122 -0
  106. package/dist/server/push.js.map +1 -0
  107. package/dist/server/reports.d.ts +4 -0
  108. package/dist/server/reports.js +40 -0
  109. package/dist/server/reports.js.map +1 -0
  110. package/dist/server/roblox-assets.d.ts +41 -0
  111. package/dist/server/roblox-assets.js +131 -0
  112. package/dist/server/roblox-assets.js.map +1 -0
  113. package/dist/server/roblox-connection.d.ts +54 -0
  114. package/dist/server/roblox-connection.js +331 -0
  115. package/dist/server/roblox-connection.js.map +1 -0
  116. package/dist/server/roblox-install.d.ts +4 -0
  117. package/dist/server/roblox-install.js +170 -0
  118. package/dist/server/roblox-install.js.map +1 -0
  119. package/dist/server/roblox-mcp.d.ts +2 -0
  120. package/dist/server/roblox-mcp.js +34 -0
  121. package/dist/server/roblox-mcp.js.map +1 -0
  122. package/dist/server/roblox-play.d.ts +23 -0
  123. package/dist/server/roblox-play.js +84 -0
  124. package/dist/server/roblox-play.js.map +1 -0
  125. package/dist/server/roblox-project-tools.d.ts +11 -0
  126. package/dist/server/roblox-project-tools.js +119 -0
  127. package/dist/server/roblox-project-tools.js.map +1 -0
  128. package/dist/server/roblox-setup.d.ts +37 -0
  129. package/dist/server/roblox-setup.js +206 -0
  130. package/dist/server/roblox-setup.js.map +1 -0
  131. package/dist/server/roblox-skills.d.ts +15 -0
  132. package/dist/server/roblox-skills.js +15 -0
  133. package/dist/server/roblox-skills.js.map +1 -0
  134. package/dist/server/roblox.d.ts +30 -0
  135. package/dist/server/roblox.js +248 -0
  136. package/dist/server/roblox.js.map +1 -0
  137. package/dist/server/routes.d.ts +11 -0
  138. package/dist/server/routes.js +223 -0
  139. package/dist/server/routes.js.map +1 -0
  140. package/dist/server/security.d.ts +4 -0
  141. package/dist/server/security.js +64 -0
  142. package/dist/server/security.js.map +1 -0
  143. package/dist/server/skills.d.ts +4 -0
  144. package/dist/server/skills.js +123 -0
  145. package/dist/server/skills.js.map +1 -0
  146. package/dist/server/store.d.ts +25 -0
  147. package/dist/server/store.js +89 -0
  148. package/dist/server/store.js.map +1 -0
  149. package/dist/server/templates/roblox-coin-run.d.ts +2 -0
  150. package/dist/server/templates/roblox-coin-run.js +326 -0
  151. package/dist/server/templates/roblox-coin-run.js.map +1 -0
  152. package/dist/server/templates/roblox-skills.json +55 -0
  153. package/dist/server/templates/yandex-sdk.d.ts +3 -0
  154. package/dist/server/templates/yandex-sdk.js +177 -0
  155. package/dist/server/templates/yandex-sdk.js.map +1 -0
  156. package/dist/server/updates.d.ts +28 -0
  157. package/dist/server/updates.js +99 -0
  158. package/dist/server/updates.js.map +1 -0
  159. package/dist/server/video-media.d.ts +47 -0
  160. package/dist/server/video-media.js +224 -0
  161. package/dist/server/video-media.js.map +1 -0
  162. package/dist/server/video-routes.d.ts +7 -0
  163. package/dist/server/video-routes.js +91 -0
  164. package/dist/server/video-routes.js.map +1 -0
  165. package/dist/server/video.d.ts +214 -0
  166. package/dist/server/video.js +301 -0
  167. package/dist/server/video.js.map +1 -0
  168. package/dist/server/web-build.d.ts +16 -0
  169. package/dist/server/web-build.js +232 -0
  170. package/dist/server/web-build.js.map +1 -0
  171. package/dist/server/workflow.d.ts +221 -0
  172. package/dist/server/workflow.js +826 -0
  173. package/dist/server/workflow.js.map +1 -0
  174. package/dist/server/yandex-config.d.ts +33 -0
  175. package/dist/server/yandex-config.js +12 -0
  176. package/dist/server/yandex-config.js.map +1 -0
  177. package/dist/server/yandex-godot.d.ts +5 -0
  178. package/dist/server/yandex-godot.js +107 -0
  179. package/dist/server/yandex-godot.js.map +1 -0
  180. package/dist/shared/aspects.d.ts +48 -0
  181. package/dist/shared/aspects.js +22 -0
  182. package/dist/shared/aspects.js.map +1 -0
  183. package/dist/shared/assistant.d.ts +7 -0
  184. package/dist/shared/assistant.js +36 -0
  185. package/dist/shared/assistant.js.map +1 -0
  186. package/dist/shared/chat.d.ts +46 -0
  187. package/dist/shared/chat.js +2 -0
  188. package/dist/shared/chat.js.map +1 -0
  189. package/dist/shared/settings.d.ts +11 -0
  190. package/dist/shared/settings.js +25 -0
  191. package/dist/shared/settings.js.map +1 -0
  192. package/dist/shared/types.d.ts +510 -0
  193. package/dist/shared/types.js +2 -0
  194. package/dist/shared/types.js.map +1 -0
  195. package/dist/web/apple-touch-icon.png +0 -0
  196. package/dist/web/assets/AgentActivity-sYJeY5Yf.js +9 -0
  197. package/dist/web/assets/ArrowRight.es-9w41WqD8.js +1 -0
  198. package/dist/web/assets/ArrowUpRight.es-BmIrpjWN.js +1 -0
  199. package/dist/web/assets/AssetMediaPreview-D2cd6oQO.js +1 -0
  200. package/dist/web/assets/AssetSelectionWizard-a43_6NO7.js +1 -0
  201. package/dist/web/assets/CaretDown.es-Cq33NYip.js +1 -0
  202. package/dist/web/assets/CheckCircle.es-D5B3hdvz.js +1 -0
  203. package/dist/web/assets/CoreAccount-D4ZC8CWv.js +1 -0
  204. package/dist/web/assets/DownloadSimple.es-DwOWFbs2.js +1 -0
  205. package/dist/web/assets/FileText.es-D83IIiOG.js +1 -0
  206. package/dist/web/assets/ImageSquare.es-DxCFt99_.js +1 -0
  207. package/dist/web/assets/KidsApp-BXWaALoK.css +1 -0
  208. package/dist/web/assets/KidsApp-D-8t3HPy.js +2 -0
  209. package/dist/web/assets/MagnifyingGlass.es-rkdOU2rH.js +1 -0
  210. package/dist/web/assets/PaperPlaneRight.es-U0twumpP.js +1 -0
  211. package/dist/web/assets/PencilSimple.es-JVHwRvGx.js +1 -0
  212. package/dist/web/assets/PlainChat-BC4K_tmj.css +1 -0
  213. package/dist/web/assets/PlainChat-CMxYwKko.js +1 -0
  214. package/dist/web/assets/Sparkle.es-CmGpvnNm.js +1 -0
  215. package/dist/web/assets/Square.es-BjNN9Ypc.js +1 -0
  216. package/dist/web/assets/UploadSimple.es-DxIuxpCC.js +1 -0
  217. package/dist/web/assets/WarningCircle.es-D63H8cL9.js +1 -0
  218. package/dist/web/assets/WorkflowJournal-Did0sRta.js +1 -0
  219. package/dist/web/assets/YandexConfigFields-DDL22Bfw.js +1 -0
  220. package/dist/web/assets/YandexGamesView-R64RSJRL.js +2 -0
  221. package/dist/web/assets/bg-build-DIalyCDw.svg +3 -0
  222. package/dist/web/assets/bg-idea-DHcE-Dhs.svg +3 -0
  223. package/dist/web/assets/bg-materials-UDVFaueI.svg +3 -0
  224. package/dist/web/assets/bg-plan-DpfwIsqJ.svg +3 -0
  225. package/dist/web/assets/bg-play-CNc1QZFK.svg +3 -0
  226. package/dist/web/assets/dist-7vx7MTaJ.js +1 -0
  227. package/dist/web/assets/index-Izzbh11U.css +1 -0
  228. package/dist/web/assets/index-sNciOZYG.js +13 -0
  229. package/dist/web/assets/jsx-runtime-CdVFa8IP.js +1 -0
  230. package/dist/web/assets/manrope-cyrillic-wght-normal-Dvxsihut.woff2 +0 -0
  231. package/dist/web/assets/manrope-greek-wght-normal-DL7QRZyv.woff2 +0 -0
  232. package/dist/web/assets/manrope-latin-ext-wght-normal-Ch3YOpNY.woff2 +0 -0
  233. package/dist/web/assets/manrope-latin-wght-normal-DHIcAJRg.woff2 +0 -0
  234. package/dist/web/assets/manrope-vietnamese-wght-normal-usUDDRr7.woff2 +0 -0
  235. package/dist/web/assets/resources-DppXCagL.js +1 -0
  236. package/dist/web/assets/robot-happy-B6YvlU94.svg +24 -0
  237. package/dist/web/assets/robot-hello-CCsQgsS-.svg +24 -0
  238. package/dist/web/assets/robot-listen-Buuz9xII.svg +24 -0
  239. package/dist/web/assets/robot-sad-CRtiNVWs.svg +24 -0
  240. package/dist/web/assets/robot-think-CJ5fDBTm.svg +24 -0
  241. package/dist/web/assets/star-big-CSFDKjws.svg +22 -0
  242. package/dist/web/assets/star-empty-CC-5vioJ.svg +3 -0
  243. package/dist/web/assets/star-next-ztuDjOHU.svg +9 -0
  244. package/dist/web/assets/star-on-l_krO3aV.svg +10 -0
  245. package/dist/web/assets/ui-wHE_yBK0.js +41 -0
  246. package/dist/web/assets/umgt-logo-DAOkTN9u.png +0 -0
  247. package/dist/web/assets/unbounded-cyrillic-wght-normal-C627f1f8.woff2 +0 -0
  248. package/dist/web/assets/unbounded-latin-ext-wght-normal-D6H-S6MW.woff2 +0 -0
  249. package/dist/web/assets/unbounded-latin-wght-normal-D5afBTBx.woff2 +0 -0
  250. package/dist/web/assets/unbounded-vietnamese-wght-normal-rat7GD3o.woff2 +0 -0
  251. package/dist/web/assets/workflow-BhcU9uvG.css +1 -0
  252. package/dist/web/assets/workflow-BrIKN5ER.js +34 -0
  253. package/dist/web/favicon.ico +0 -0
  254. package/dist/web/icons/favicon-16.png +0 -0
  255. package/dist/web/icons/favicon-32.png +0 -0
  256. package/dist/web/icons/icon-192.png +0 -0
  257. package/dist/web/icons/icon-512.png +0 -0
  258. package/dist/web/icons/maskable-192.png +0 -0
  259. package/dist/web/icons/maskable-512.png +0 -0
  260. package/dist/web/index.html +28 -0
  261. package/dist/web/manifest.webmanifest +19 -0
  262. package/dist/web/sw.js +25 -0
  263. package/docs/agent-activity-ui.md +33 -0
  264. package/docs/agent-activity.md +43 -0
  265. package/docs/agent-permissions-ui.md +15 -0
  266. package/docs/api.md +145 -0
  267. package/docs/approval-journal.md +13 -0
  268. package/docs/asset-media.md +27 -0
  269. package/docs/asset-providers.md +34 -0
  270. package/docs/asset-selection.md +31 -0
  271. package/docs/automatic-approvals.md +15 -0
  272. package/docs/background-game-launch.md +17 -0
  273. package/docs/billing-validation.md +23 -0
  274. package/docs/core-isolation.md +37 -0
  275. package/docs/core.md +45 -0
  276. package/docs/generated-text-ui.md +41 -0
  277. package/docs/interactive-interview-ui.md +29 -0
  278. package/docs/interactive-interview.md +52 -0
  279. package/docs/local-codex.md +51 -0
  280. package/docs/local-meshy.md +13 -0
  281. package/docs/markdown.md +11 -0
  282. package/docs/permission-ui.md +11 -0
  283. package/docs/plan.md +47 -0
  284. package/docs/remote-core-deployment.md +47 -0
  285. package/docs/roblox.md +90 -0
  286. package/docs/validation.md +228 -0
  287. package/docs/video-reference.md +35 -0
  288. package/docs/workflow-journal-ui.md +21 -0
  289. package/docs/yandex-foundation.md +27 -0
  290. package/docs/yandex-games.md +45 -0
  291. package/package.json +95 -4
  292. package/scripts/create-roblox-demo.mjs +31 -0
  293. package/scripts/diagnose-roblox-mcp.mjs +45 -0
  294. package/scripts/open-hub-chrome.mjs +60 -0
  295. package/scripts/smoke-agent.mjs +91 -0
@@ -0,0 +1,55 @@
1
+ {
2
+ "source": "https://github.com/EL4CTEO/roblox-skills",
3
+ "revision": "11571e0622bb96c0d469e5c688bed7017c6b5cda",
4
+ "license": "MIT License\n\nCopyright (c) 2026 EL4CTEO\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
5
+ "files": {
6
+ "skills/roblox-analytics-liveops/SKILL.md": "---\nname: roblox-analytics-liveops\ndescription: Analytics and live ops - AnalyticsService economy/funnel/onboarding/progression/custom events, player segments, experience configs via ConfigService, A/B experiments, badges, experience notifications, timed events, daily rewards, season passes. Use when instrumenting a game, tuning economy or onboarding, running A/B tests or feature flags, or planning live events and re-engagement.\n---\n\n# Analytics and live operations\n\n## Analytics events (server-side, `AnalyticsService`)\n\nStandard events feed the Creator Hub dashboards automatically (economy, funnels, progression,\ncustom). The `Fire*` methods are deprecated; use the `Log*` family.\n\n```luau\n--!strict\nlocal AnalyticsService = game:GetService(\"AnalyticsService\")\n\nlocal Analytics = {}\n\n-- Currency sources and sinks → Economy dashboard.\nfunction Analytics.coinsEarned(player: Player, amount: number, balance: number, source: string)\n\tAnalyticsService:LogEconomyEvent(\n\t\tplayer,\n\t\tEnum.AnalyticsEconomyFlowType.Source,\n\t\t\"Coins\", -- currency type\n\t\tamount,\n\t\tbalance, -- ending balance\n\t\tEnum.AnalyticsEconomyTransactionType.Gameplay.Name, -- or a custom string like \"QuestReward\"\n\t\tsource -- item SKU / source id\n\t)\nend\n\nfunction Analytics.itemBought(player: Player, price: number, balance: number, itemSku: string)\n\tAnalyticsService:LogEconomyEvent(\n\t\tplayer,\n\t\tEnum.AnalyticsEconomyFlowType.Sink,\n\t\t\"Coins\",\n\t\tprice,\n\t\tbalance,\n\t\tEnum.AnalyticsEconomyTransactionType.Shop.Name,\n\t\titemSku\n\t)\nend\n\n-- First-session tutorial funnel (steps must be sequential, 1-based).\nfunction Analytics.onboarding(player: Player, step: number, stepName: string)\n\tAnalyticsService:LogOnboardingFunnelStepEvent(player, step, stepName)\nend\n\n-- Recurring funnels (e.g. shop purchase flow); share a session id per attempt.\nfunction Analytics.shopFunnel(player: Player, sessionId: string, step: number, stepName: string)\n\tAnalyticsService:LogFunnelStepEvent(player, \"ShopPurchase\", sessionId, step, stepName)\nend\n\n-- Level progression.\nfunction Analytics.levelStarted(player: Player, level: number)\n\tAnalyticsService:LogProgressionStartEvent(player, \"MainPath\", level, `Level{level}`)\nend\nfunction Analytics.levelCompleted(player: Player, level: number)\n\tAnalyticsService:LogProgressionCompleteEvent(player, \"MainPath\", level, `Level{level}`)\nend\n\n-- Anything else: numeric custom events, sliced by up to 3 custom fields.\nfunction Analytics.bossKilled(player: Player, bossId: string, seconds: number)\n\tAnalyticsService:LogCustomEvent(player, \"BossKillTime\", seconds, {\n\t\t[Enum.AnalyticsCustomFieldKeys.CustomField01.Name] = bossId,\n\t})\nend\n\nreturn Analytics\n```\n\nGuidelines:\n- Log from the **server** (trustworthy, and required by these APIs). Keep event names and custom-field\n values low-cardinality (IDs of categories, not free text or user IDs).\n- Economy events need accurate `endingBalance`; log every source and sink or the dashboard misleads.\n- Design funnels before building features (onboarding steps, shop flow, first purchase).\n- `AnalyticsService:GetPlayerSegmentsAsync(player)` returns segment info (payer status, account age in\n your game, ...) at runtime to tailor onboarding and offers — `pcall` it.\n\n## Experience configs (live values without republishing)\n\nCreate configs in Creator Hub (or Open Cloud) — balance numbers, feature flags, event toggles — and read them\non the server with `ConfigService`. Published changes arrive in running servers.\n\n```luau\n--!strict\nlocal ConfigService = game:GetService(\"ConfigService\")\n\nlocal ok, snapshot = pcall(function()\n\treturn ConfigService:GetConfigAsync()\nend)\n\nlocal function get(key: string, default: any): any\n\tif not ok then\n\t\treturn default\n\tend\n\tlocal value = snapshot:GetValue(key)\n\treturn if value == nil then default else value\nend\n\nlocal bossHealth = get(\"bossHealth\", 500)\nprint(\"boss health\", bossHealth)\n\nif ok then\n\tsnapshot.UpdateAvailable:Connect(function()\n\t\tsnapshot:Refresh() -- apply newly published values at a safe moment (e.g. between rounds)\n\t\tprint(\"new boss health\", get(\"bossHealth\", 500))\n\tend)\nend\n```\n\n- Always code a default: configs may fail to load.\n- **Experiments / targeted configs** evaluate per player: use `ConfigService:GetConfigForPlayerAsync(player)`\n (`GetConfigAsync` ignores targeting). Experiments run 14–60 days, up to two variants plus control; pick a\n goal metric and respect the minimum detectable effect. Manage via Creator Hub or the Open Cloud\n experimentation API (`roblox-open-cloud`).\n- Studio testing: `ConfigService:SetTestingValue(key, value)` / `ClearTestingValue`.\n\n## Badges\n\n`BadgeService:AwardBadgeAsync(userId, badgeId)` (server, `pcall`; skip if `UserHasBadgeAsync` or\n`CheckUserBadgesAsync` already says owned). `AwardBadge`/`UserHasBadge` are deprecated. Badges are\ngreat onboarding and progression milestones and show on players' profiles.\n\n## Experience notifications (re-engagement)\n\n1. Ask players to opt in at a meaningful moment: `ExperienceNotificationService:CanPromptOptInAsync()` then\n `PromptOptIn()` (client).\n2. Create notification strings in Creator Hub; send from your backend via Open Cloud\n `POST /cloud/v2/users/{userId}/notifications` (e.g. \"your crops are ready\", \"event starts now\").\n Notifications are rate-limited and moderated; send ones players value.\n\n## Live-ops patterns\n\n- **Timed events**: schedule with UTC timestamps in a config (`eventStart`, `eventEnd`); compare with\n `os.time()` on the server; broadcast state via attributes; clients show countdowns from\n `workspace:GetServerTimeNow()`.\n- **Season pass / rotating shops**: rotation seed = `math.floor(os.time() / 86400)` for daily shops; store\n claimed rewards per season id in player data.\n- **Daily rewards/streaks**: store `lastClaimDay` (UTC day number) and streak count; grant on the server.\n- Ship content behind config flags, validate with a small rollout/experiment, then roll out to 100%.\n- Watch Creator Hub dashboards: retention (D1/D7/D30), session length, conversion, ARPPU, funnels, and\n performance (crash rate, FPS, memory) after every release.\n\n## Related skills\n\n`roblox-monetization` (offers, pricing), `roblox-data-stores` (streaks, season data),\n`roblox-open-cloud` (configs/experiments/notifications APIs), `roblox-game-systems`.\n",
7
+ "skills/roblox-architecture/SKILL.md": "---\nname: roblox-architecture\ndescription: Roblox project structure and client/server model - replication, where code and assets belong (ServerScriptService, ReplicatedStorage, StarterPlayer), RunContext, service/controller bootstrapping, player/character lifecycle, CollectionService components, attributes, streaming-safe client code, library choices. Use when starting a project, deciding where code goes, or fixing replication/streaming bugs.\n---\n\n# Roblox game architecture\n\n## The model in one paragraph\n\nEvery Roblox game is a **server** (authoritative, runs `Script`s, sees everything, owns DataStores)\nplus one **client** per player (runs `LocalScript`s / client `Script`s, renders, reads input).\nThe server's DataModel **replicates** to clients automatically. Client changes do **not** replicate\nback, except: physics of parts the client has network ownership of (its character by default),\nits own `Humanoid` state/animations, and remote calls. Assume every client is compromised\n(see `roblox-security`). Design every feature as: *client requests → server validates and\ndecides → state replicates → clients render*.\n\n## Where things go\n\n| Container | Runs/visible on | Put here |\n| --- | --- | --- |\n| `ServerScriptService` | Server only | Server `Script`s and server-only ModuleScripts (game logic, data, anti-cheat). |\n| `ServerStorage` | Server only | Server-only assets: maps, NPC templates, tools to clone, secrets-adjacent config. |\n| `ReplicatedStorage` | Both | Shared modules (types, config, pure utils), `RemoteEvent`s, assets clients clone. |\n| `ReplicatedFirst` | Client, first | Loading screen only. Replicates before everything else. |\n| `StarterPlayer.StarterPlayerScripts` | Client (copied once to `PlayerScripts`) | Client entry point and controllers. |\n| `StarterPlayer.StarterCharacterScripts` | Client (copied per spawn into character) | Per-character client scripts (rarely needed). |\n| `StarterGui` | Client (copied to `PlayerGui`) | `ScreenGui`s. Set `ResetOnSpawn = false` unless you want reset on death. |\n| `Workspace` | Both (streamed to clients) | The 3D world only. Not a place for code or templates. |\n| `Lighting`, `SoundService`, `Teams`, `TextChatService` | Both | Their respective config objects. |\n\nRules:\n- Code in `ReplicatedStorage` is **downloadable by exploiters**. Never put server logic, secrets,\n admin lists, or anti-cheat thresholds there.\n- `Script.RunContext`: `Legacy` (default: behavior depends on the container), `Server`, `Client`, or\n `Plugin`. A non-Legacy RunContext makes a `Script` run **regardless of container** (e.g. a `Client`\n Script in `ReplicatedStorage` runs on every client) — so templates you clone must not contain such\n scripts unintentionally. Legacy `Script`s don't run in `ReplicatedStorage`/`ServerStorage`.\n\n## Project layout (Rojo-style, recommended)\n\n```text\nsrc/\n server/ -> ServerScriptService.Server (init.server.luau = entry point)\n Services/ -> one ModuleScript per system (DataService, CombatService, ...)\n client/ -> StarterPlayer.StarterPlayerScripts.Client (init.client.luau)\n Controllers/ -> one ModuleScript per client system (InputController, UIController, ...)\n shared/ -> ReplicatedStorage.Shared (types, config, pure logic, network definitions)\nPackages/ -> ReplicatedStorage.Packages (Wally/pesde dependencies)\nServerPackages/ -> ServerScriptService.ServerPackages\n```\n\nOne entry script per side, everything else is a ModuleScript. This gives deterministic load order,\ntestable modules, and a single place to wire dependencies. Roblox's own recommendation is equivalent:\none `Script` (`RunContext = Server`) in `ServerScriptService` and one `Script` (`RunContext = Client`) in\n`ReplicatedStorage`, each requiring modules and calling their `start()`. Tooling setup: `roblox-tooling`.\n\n## Bootstrapping services (no framework needed)\n\n```luau\n--!strict\n-- ServerScriptService/Server/init.server.luau\ntype Service = {\n\tinit: ((self: Service) -> ())?, -- sync setup; may reference other services; must not yield\n\tstart: ((self: Service) -> ())?, -- runs after every init; may yield / connect events\n}\n\nlocal servicesFolder = script:WaitForChild(\"Services\")\nlocal services: { [string]: Service } = {}\n\nfor _, module in servicesFolder:GetChildren() do\n\tif module:IsA(\"ModuleScript\") then\n\t\tservices[module.Name] = (require :: any)(module)\n\tend\nend\n\nfor name, service in services do\n\tif service.init then\n\t\tdebug.setmemorycategory(name)\n\t\tservice:init()\n\t\tdebug.resetmemorycategory()\n\tend\nend\n\nfor name, service in services do\n\tif service.start then\n\t\ttask.spawn(function()\n\t\t\tdebug.setmemorycategory(name)\n\t\t\tservice:start()\n\t\tend)\n\tend\nend\n```\n\n- Services reference each other with plain `require` (no cycles) or receive dependencies in `init`.\n- The client mirrors this with `Controllers`. Keep names symmetrical (`ShopService` / `ShopController`).\n- Frameworks like Knit are unnecessary for this; if a project already uses one, follow its conventions.\n\n## Player and character lifecycle\n\n```luau\n--!strict\nlocal Players = game:GetService(\"Players\")\n\nlocal function onCharacterAdded(player: Player, character: Model)\n\tlocal humanoid = character:WaitForChild(\"Humanoid\") :: Humanoid\n\thumanoid.Died:Once(function()\n\t\tprint(`{player.Name} died`)\n\tend)\nend\n\nlocal function onPlayerAdded(player: Player)\n\t-- Load data first (see roblox-data-stores), then set up character handling.\n\tif player.Character then\n\t\ttask.spawn(onCharacterAdded, player, player.Character)\n\tend\n\tplayer.CharacterAdded:Connect(function(character)\n\t\tonCharacterAdded(player, character)\n\tend)\nend\n\nlocal function onPlayerRemoving(player: Player)\n\t-- Save data, release session locks, clear every table keyed by this player.\n\tprint(`{player.Name} left`)\nend\n\nPlayers.PlayerAdded:Connect(onPlayerAdded)\nPlayers.PlayerRemoving:Connect(onPlayerRemoving)\nfor _, player in Players:GetPlayers() do -- players who joined before this script connected\n\ttask.spawn(onPlayerAdded, player)\nend\n\ngame:BindToClose(function()\n\t-- Server shutdown: flush saves for remaining players (max ~30 s total).\nend)\n```\n\n- Always handle players/characters that already exist when your code starts.\n- Characters respawn: anything bound to a character must be re-created on `CharacterAdded`.\n- On the client, character descendants may not exist yet (streaming): `WaitForChild` them.\n\n## Components with CollectionService tags\n\nTag instances (Studio **Tags** section or `CollectionService:AddTag`) and attach behavior in code.\nThis scales better than copying scripts into every model and works with streaming because the\nadded/removed signals fire as instances stream in and out.\n\n```luau\n--!strict\nlocal CollectionService = game:GetService(\"CollectionService\")\n\nlocal TAG = \"KillBrick\"\nlocal cleanups: { [Instance]: () -> () } = {}\n\nlocal function onAdded(instance: Instance)\n\tif not instance:IsA(\"BasePart\") then\n\t\treturn\n\tend\n\tlocal connection = instance.Touched:Connect(function(hit)\n\t\tlocal humanoid = hit.Parent and hit.Parent:FindFirstChildOfClass(\"Humanoid\")\n\t\tif humanoid then\n\t\t\thumanoid.Health = 0\n\t\tend\n\tend)\n\tcleanups[instance] = function()\n\t\tconnection:Disconnect()\n\tend\nend\n\nlocal function onRemoved(instance: Instance)\n\tlocal cleanup = cleanups[instance]\n\tif cleanup then\n\t\tcleanup()\n\t\tcleanups[instance] = nil\n\tend\nend\n\nCollectionService:GetInstanceAddedSignal(TAG):Connect(onAdded)\nCollectionService:GetInstanceRemovedSignal(TAG):Connect(onRemoved)\nfor _, instance in CollectionService:GetTagged(TAG) do\n\ttask.spawn(onAdded, instance)\nend\n```\n\nConfigure components with **attributes** (`instance:GetAttribute(\"Damage\")`,\n`GetAttributeChangedSignal`) instead of `IntValue`/`StringValue` children. Attributes replicate\nserver → client, are cheaper, and show in the Properties panel.\n\n## Sharing state\n\n| Need | Use |\n| --- | --- |\n| Small per-instance/per-player values visible to clients | Attributes on the instance / `Player` (server sets). |\n| Events and requests | `RemoteEvent` / `UnreliableRemoteEvent` (see `roblox-networking`). |\n| Large or per-player private state (inventory, quests) | Server-owned tables + remotes that send snapshots/diffs to that player only. |\n| Same-side communication | Module function calls or a typed Signal module — not `BindableEvent`, `_G`, or `shared`. |\n| Persistent data | `roblox-data-stores`. Cross-server: MessagingService / MemoryStore. |\n\n## Streaming (on by default for new places)\n\nWith `Workspace.StreamingEnabled`, clients only have nearby parts. Client code must:\n- `WaitForChild` (with a timeout when it may never arrive) or nil-check anything in `Workspace`.\n- Use `ModelStreamingMode = Atomic` for models whose parts scripts need together;\n `Persistent` only when a model must always be present (costs memory).\n- Treat `GetChildren`, raycasts, and property reads of streamed-out parts as partial/stale; do full-world\n queries on the server.\n- Pre-stream destinations on the server with `player:RequestStreamAroundAsync(position)` before teleporting a character.\n\nFull pattern catalog and recommended Workspace settings: [references/streaming.md](references/streaming.md).\n\n## Anti-patterns\n\n- Dozens of loose `Script`s inside parts/models (use tags + one service).\n- Polling loops (`while task.wait(0.1) do check() end`) instead of events or `Heartbeat` with `deltaTime`.\n- Trusting the client for game state, or firing remotes every frame.\n- `_G`/`shared` globals, circular requires, yielding at module top level.\n- Cloning maps into `Workspace` from the client (client-only copies never get server updates).\n- Storing per-player data in the character (it's destroyed on death).\n\nLibrary ecosystem (signals, cleanup, networking, UI, ECS, data): [references/ecosystem.md](references/ecosystem.md).\n\n## Related skills\n\n`roblox-networking`, `roblox-security`, `roblox-data-stores`, `roblox-tooling`, `roblox-performance`, `roblox-game-systems`.\n",
8
+ "skills/roblox-architecture/references/ecosystem.md": "# Roblox open-source ecosystem (2026)\n\nGuidance, not endorsements. **Always check the project's repository for its current status and API\nbefore adding it** — several popular libraries were archived or rewritten recently. If a project already\nuses a library, follow it instead of introducing an alternative. Install with Wally or pesde (see\n`roblox-tooling`).\n\n## Frameworks\n\n- **None (recommended)** — a single entry script per side that requires service/controller modules\n (see SKILL.md). Fully typed, no magic.\n- **Knit** — archived by its author in July 2024. Don't start new projects on it; maintain existing ones.\n- **Flamework** — decorator-based framework for roblox-ts projects.\n\n## Utilities\n\n| Need | Libraries |\n| --- | --- |\n| Signals | `sleitnick/signal`, GoodSignal, LemonSignal |\n| Cleanup | Trove (`sleitnick/trove`), Janitor, Maid |\n| Promises | `evaera/promise` (Promise-based async; optional — plain `task` + `pcall` is fine) |\n| Tables/functional | Sift, Dash |\n\n## Networking (buffer-packed, schema-driven)\n\n- **Zap** and **Blink** — IDL compilers: you write a schema, they generate typed, validated,\n buffer-serialized remote code for client and server. Both have had rewrites/forks; check which\n branch/fork is maintained.\n- **ByteNet**, **Packet** — runtime (non-codegen) buffer networking libraries.\n- Plain `RemoteEvent`s are fine for most games; adopt these when bandwidth or remote CPU shows up in profiling.\n\n## Data\n\n- **ProfileStore** (loleris) — session-locked player data, successor to ProfileService; uses\n MessagingService to resolve lock conflicts quickly. The default choice for player data.\n- **Lapis** — alternative DataStore wrapper with session locking and migrations.\n- **Replica** (loleris) — replicate server state tables to clients.\n- DataStore2 — legacy; don't use for new work.\n\n## UI\n\n- **React** (`jsdotlua/react` + `jsdotlua/react-roblox`, the Roblox port of React 17) — hooks, large apps.\n- **Fusion** — reactive state + declarative instances.\n- **Vide** — lightweight reactive UI library.\n- Plain instances + Studio's UI editor + StyleSheets for simple UIs.\n\n## ECS\n\n- **jecs** — fast archetype ECS with relationships and a typed Luau API.\n- **Matter** — earlier ECS with a debugger; less active.\n\n## Testing / tooling libs\n\n- **Jest Lua** (`jsdotlua/jest`) — Jest port; the modern test framework. TestEZ is legacy.\n- See `roblox-testing` and `roblox-tooling`.\n\n## roblox-ts\n\nTypeScript → Luau compiler (`roblox-ts`, npm `@rbxts/*` packages). If a project has `tsconfig.json` and\n`@rbxts/types`, write TypeScript, not Luau, and use `@rbxts/services` imports.\n",
9
+ "skills/roblox-architecture/references/streaming.md": "# Instance streaming — settings and client patterns\n\nSource: Roblox \"Instance streaming\" and \"Techniques and conversion\" docs (2026).\n\n## Recommended Workspace settings\n\n| Property | Value |\n| --- | --- |\n| `StreamingEnabled` | `true` (default for new places; required by server authority) |\n| `ModelStreamingBehavior` | `Improved` |\n| `StreamingIntegrityMode` | `PauseOutsideLoadedArea` |\n| `StreamingMinRadius` | `64` (default) |\n| `StreamingTargetRadius` | `1024` (default) |\n| `StreamOutBehavior` | `Opportunistic` (lowers client memory, prevents OOM crashes) |\n| `EnableSLIMAvatars` | `Enabled` (lightweight stand-ins for distant R15 avatars) |\n\n## Model settings\n\n- `Model.ModelStreamingMode`:\n - `Default`/`Nonatomic` — parts stream individually.\n - `Atomic` — all **initial** descendants arrive together; scripts can index children directly once the\n model exists. Instances added later are not atomic.\n - `Persistent` — always present on every client, never streams out. Use sparingly (memory).\n - `PersistentPerPlayer` — persistent only for players added via `Model:AddPersistentPlayer(player)`.\n- `Model.LevelOfDetail = SLIM` for static models: distant models render as optimized composite meshes.\n Not for animated or runtime-modified models.\n- Keep models spatially compact (≈64 studs extents); split huge \"container\" models; flatten nesting\n (a persistent model inside an atomic one forces the parent persistent).\n\n## Client script patterns\n\n| Pattern | Problem under streaming | Fix |\n| --- | --- | --- |\n| `workspace.House1.Door` | Errors if not streamed in. | `WaitForChild` (with timeout if optional), nil-check, or make `House1` Atomic. |\n| `character.Humanoid` in `CharacterAdded` | Character is parented before descendants replicate. | `character:WaitForChild(\"Humanoid\")`. |\n| Remote carries an `Instance` | Signal can arrive before (or without) the instance. | `WaitForChild` with timeout, pre-stream with `RequestStreamAroundAsync`, or tolerate `nil`. |\n| Reading `part.Position` of a far part | Returns the last replicated (stale) value. | Do distance logic on the server, or check `part:IsDescendantOf(workspace)`. |\n| `folder:GetChildren()` / `GetDescendants()` | Only streamed-in subset. | Enumerate on the server, or `PersistentPerPlayer`, or document that partial is fine. |\n| Client raycasts / `GetPartBoundsInBox` | Only streamed-in geometry. | Full-world queries on the server; local-radius queries are fine. |\n| `ChildAdded` / tag added signals | Fire again on every stream-in. | Mark first-time effects with an attribute; treat add/remove as stream in/out. |\n| Cloning map from ReplicatedStorage on client | Client copy never receives server updates; can stream out. | Build the world on the server. |\n| Loading screen waits for a map part | Hangs: no replication focus before spawn. | Wait for character + surroundings, or `player:AddReplicationFocus(part)` before spawn. |\n| `Sound`/`AudioPlayer` in a far part | Stops when the part streams out. | Parent ambient audio to a persistent model or `SoundService`. |\n| `Touched`, `ProximityPrompt`, `ClickDetector` | Don't work for clients without the part. | Expected; keep interactions local to the player. |\n| Client pathfinding | Sees only streamed geometry. | Compute paths on the server. |\n\n```luau\n--!strict\nlocal Players = game:GetService(\"Players\")\n\nlocal player = Players.LocalPlayer\n\nlocal function onCharacterAdded(character: Model)\n\tlocal humanoid = character:WaitForChild(\"Humanoid\", 10) :: Humanoid?\n\tif not humanoid then\n\t\twarn(\"Humanoid did not replicate in time\")\n\t\treturn\n\tend\n\tprint(\"walk speed\", humanoid.WalkSpeed)\nend\n\nif player.Character then\n\ttask.spawn(onCharacterAdded, player.Character)\nend\nplayer.CharacterAdded:Connect(onCharacterAdded)\n```\n\n## Proactive streaming (server)\n\n```luau\n--!strict\nlocal function teleportCharacter(player: Player, target: CFrame)\n\tlocal ok, err = pcall(function()\n\t\tplayer:RequestStreamAroundAsync(target.Position, 5) -- optional timeout in seconds\n\tend)\n\tif not ok then\n\t\twarn(err)\n\tend\n\tlocal character = player.Character\n\tif character and character.Parent then\n\t\tcharacter:PivotTo(target)\n\tend\nend\n\nreturn teleportCharacter\n```\n\n`Player:AddReplicationFocus(part)` / `RemoveReplicationFocus(part)` keep an extra area loaded for a player\n(e.g. a camera cutscene location). Keep the number of extra foci small.\n\n## Testing\n\n- Test with `StreamingTargetRadius = 64` to surface bugs.\n- Traverse the whole map, teleport between distant areas, revisit areas.\n- Watch Output/Developer Console for `attempt to index nil with ...`.\n- Roblox publishes an official AI **streaming conversion skill** (`/rbx-convert-to-streaming`) that runs\n through the Studio MCP server; see the `roblox-studio-mcp` skill.\n",
10
+ "skills/roblox-audio/SKILL.md": "---\nname: roblox-audio\ndescription: Sound and music - the Audio API (AudioPlayer, AudioEmitter, AudioListener, AudioDeviceOutput, Wire, effects, faders), classic Sound/SoundGroup, 2D vs 3D audio, music and SFX volume, client vs server playback, asset permissions, streaming caveats, voice routing, text-to-speech. Use when adding music, sound effects, spatial audio, mixing, or fixing sounds that don't play.\n---\n\n# Roblox audio\n\nTwo systems coexist:\n- **Audio API** (modular, current): `AudioPlayer` → `Wire` → (`AudioEmitter` … `AudioListener`) → `Wire`\n → `AudioDeviceOutput`, with effect objects inserted in the chain. Required for voice-chat processing,\n mixing graphs, analyzers, text-to-speech/speech-to-text.\n- **`Sound`** (classic, still supported): one object, parented to a part for 3D or to `SoundService`/UI for 2D;\n routed through `SoundGroup`s with `SoundEffect` children. Fine for simple games and existing code.\n\nDon't mix both for the same feature; follow what the project uses.\n\n## 2D audio (music, UI) with the Audio API\n\n```luau\n--!strict\n-- Client: build a music player once.\nlocal SoundService = game:GetService(\"SoundService\")\n\nlocal player = Instance.new(\"AudioPlayer\")\nplayer.Name = \"Music\"\nplayer.Asset = \"rbxassetid://1843463175\" -- AudioPlayer.AssetId is deprecated; use Asset\nplayer.Looping = true\nplayer.Volume = 0.5\nplayer.Parent = SoundService\n\nlocal fader = Instance.new(\"AudioFader\") -- one place to control music volume (settings menu)\nfader.Name = \"MusicVolume\"\nfader.Parent = SoundService\n\nlocal output = Instance.new(\"AudioDeviceOutput\")\noutput.Parent = SoundService\n\nlocal function wire(source: Instance, target: Instance)\n\tlocal w = Instance.new(\"Wire\")\n\tw.SourceInstance = source\n\tw.TargetInstance = target\n\tw.Parent = target\nend\n\nwire(player, fader)\nwire(fader, output)\nplayer:Play()\n```\n\n## 3D audio with the Audio API\n\n- Put an `AudioEmitter` on the sound's part/attachment; wire `AudioPlayer → AudioEmitter`.\n- Listening: `SoundService.ListenerLocation` controls the default `AudioListener` (camera or character);\n wire `AudioListener → AudioDeviceOutput` (done automatically in default setups).\n- Distance falloff and directionality are set on the emitter/listener (`AudioEmitter:SetDistanceAttenuation`,\n the `AudioInteractionGroup` string property on emitters and listeners scopes who hears what).\n\n## Classic Sound quick reference\n\n```luau\n--!strict\nlocal SoundService = game:GetService(\"SoundService\")\n\nlocal sfxGroup = Instance.new(\"SoundGroup\")\nsfxGroup.Name = \"SFX\"\nsfxGroup.Volume = 0.8\nsfxGroup.Parent = SoundService\n\nlocal function playAt(part: BasePart, soundId: string)\n\tlocal sound = Instance.new(\"Sound\")\n\tsound.SoundId = soundId\n\tsound.SoundGroup = sfxGroup\n\tsound.RollOffMaxDistance = 80\n\tsound.Parent = part -- 3D: plays from the part; parent to SoundService/UI for 2D\n\tsound:Play()\n\tsound.Ended:Once(function()\n\t\tsound:Destroy()\n\tend)\nend\n\nreturn playAt\n```\n\n- `SoundService:PlayLocalSound(sound)` for client-only UI clicks.\n- Preload critical sounds: `ContentProvider:PreloadAsync({ sound })` (not everything — slows joins).\n- Pool frequently played sounds instead of creating one per shot.\n\n## Where to play\n\n- Sounds started **on the client** are heard only by that client (UI, local feedback, music tracks\n the player chose). Sounds started **on the server** replicate to everyone (use sparingly; prefer a\n remote telling clients to play locally, which also lets each client apply its own volume settings).\n- With `SoundService.RespectFilteringEnabled = true` (default), client-played sounds don't replicate.\n- Streaming: a sound or emitter in a part that streams out stops. Keep ambient audio in persistent\n models or `SoundService`.\n\n## Assets and permissions\n\n- Use audio you own, uploaded to the same owner (user/group) as the experience, or Creator Store\n audio marked free-to-use. Private audio from other creators won't play unless shared with your\n experience (asset permissions). Check the Output for \"failed to load\" / permission errors.\n- Uploads are auto-classified as sound effects or songs; eligible songs can show on the game page.\n\n## Extras\n\n- Effects: `AudioEqualizer`, `AudioCompressor`, `AudioReverb`, `AudioEcho`, `AudioChorus`, `AudioDistortion`,\n `AudioFlanger`, `AudioPitchShifter`, `AudioTremolo`, `AudioFilter`, `AudioLimiter`, `AudioAnalyzer` (read\n loudness/spectrum for visualizers).\n- `AudioTextToSpeech` (artificial voices) and `AudioSpeechToText` (transcription).\n- Voice chat: each player's `AudioDeviceInput` can be wired through effects (radios, walkie-talkies,\n proximity) — see the voice chat docs; respect players' voice settings.\n- Give players separate Music/SFX volume sliders (faders or SoundGroups) and a mute option.\n\n## Related skills\n\n`roblox-ui` (settings menus), `roblox-architecture` (client vs server), `roblox-performance` (audio memory).\n",
11
+ "skills/roblox-characters-animation/SKILL.md": "---\nname: roblox-characters-animation\ndescription: Characters and animation - Humanoid properties/states, spawning (LoadCharacterAsync), health and damage, HumanoidDescription appearance, Animator/AnimationTrack playback, priorities and markers, default animation replacement, Tools, TweenService, IKControl, Character Controller Library (beta). Use when changing movement, abilities, damage, respawn, avatars, animations, tweens, or tools/weapons.\n---\n\n# Characters and animation\n\n## Character basics\n\n- A player character is a `Model` with a `Humanoid`, `HumanoidRootPart` (the root; move the character\n with `character:PivotTo(cframe)`), R15 body parts, and an `Animate` LocalScript.\n- Defaults (walk speed, jump, health, R6/R15, collisions, body scaling) are configured in Studio's\n **Avatar Settings** window and `StarterPlayer` (`CharacterWalkSpeed`, `CharacterJumpHeight` or\n `CharacterUseJumpPower` + `CharacterJumpPower`).\n- Custom character: put a `Model` named `StarterCharacter` in `StarterPlayer`. Custom per-player\n spawns: `Players.CharacterAutoLoads = false`, then `player:LoadCharacterAsync()` when ready\n (e.g. after data loads). `LoadCharacter` (non-Async) is deprecated.\n- Characters are destroyed and recreated on death/respawn: bind per-character logic in `CharacterAdded`.\n- Client-set `WalkSpeed`/`JumpHeight` are trivially exploitable; enforce limits on the server or use\n server authority (`roblox-security`, `roblox-physics`).\n\n## Health and damage (server)\n\n```luau\n--!strict\nlocal Players = game:GetService(\"Players\")\n\nlocal function damage(target: Model, amount: number, source: Player?)\n\tlocal humanoid = target:FindFirstChildOfClass(\"Humanoid\")\n\tif not humanoid or humanoid.Health <= 0 then\n\t\treturn\n\tend\n\thumanoid:SetAttribute(\"LastDamagedBy\", if source then source.UserId else 0)\n\thumanoid:TakeDamage(amount) -- respects ForceField; use Health -= amount to bypass\nend\n\nPlayers.PlayerAdded:Connect(function(player)\n\tplayer.CharacterAdded:Connect(function(character)\n\t\tlocal humanoid = character:WaitForChild(\"Humanoid\") :: Humanoid\n\t\thumanoid.MaxHealth = 150\n\t\thumanoid.Health = 150\n\t\thumanoid.Died:Once(function()\n\t\t\tlocal killerId = humanoid:GetAttribute(\"LastDamagedBy\")\n\t\t\tprint(`{player.Name} died (killer {killerId})`)\n\t\tend)\n\tend)\nend)\n\nreturn damage\n```\n\n- Disable default regen by deleting/replacing the `Health` script via `StarterCharacterScripts`.\n- `Humanoid.BreakJointsOnDeath = false` + ragdoll constraints for ragdolls.\n- States: `humanoid:ChangeState(Enum.HumanoidStateType.Jumping)`, `SetStateEnabled(state, false)`,\n `StateChanged`. `MoveDirection`, `FloorMaterial`, `RootPart`, `SeatPart` are read-only helpers.\n- `humanoid:MoveTo(position)` walks NPCs (times out after 8 s; re-issue for long paths — see `roblox-npc-ai`).\n\n## Appearance\n\n```luau\n--!strict\nlocal Players = game:GetService(\"Players\")\n\nlocal function applyUniform(player: Player)\n\tlocal character = player.Character\n\tlocal humanoid = character and character:FindFirstChildOfClass(\"Humanoid\")\n\tif not humanoid then\n\t\treturn\n\tend\n\tlocal ok, description = pcall(function()\n\t\treturn Players:GetHumanoidDescriptionFromUserIdAsync(player.UserId)\n\tend)\n\tif not ok then\n\t\treturn\n\tend\n\tdescription.Shirt = 123456 -- asset ids\n\tdescription.Pants = 654321\n\tdescription.HeightScale = 1.05\n\tpcall(function()\n\t\thumanoid:ApplyDescriptionAsync(description) -- server-side; replicates\n\tend)\nend\n\nreturn applyUniform\n```\n\n`Player:LoadCharacterWithHumanoidDescriptionAsync(description)` spawns directly with an outfit.\nAll the non-Async variants (`ApplyDescription`, `GetHumanoidDescriptionFromUserId`, ...) are deprecated.\n\n## Animation playback\n\n```luau\n--!strict\nlocal function playAttack(character: Model, animation: Animation): AnimationTrack?\n\tlocal humanoid = character:FindFirstChildOfClass(\"Humanoid\")\n\tlocal animator = humanoid and humanoid:FindFirstChildOfClass(\"Animator\")\n\tif not animator then\n\t\treturn nil\n\tend\n\tlocal track = animator:LoadAnimation(animation) -- Humanoid:LoadAnimation is deprecated\n\ttrack.Priority = Enum.AnimationPriority.Action\n\ttrack.Looped = false\n\ttrack:GetMarkerReachedSignal(\"Hit\"):Connect(function(param)\n\t\tprint(\"hit frame reached\", param) -- apply damage window, play sound, etc.\n\tend)\n\ttrack:Play(0.1) -- fade time\n\treturn track\nend\n\nreturn playAttack\n```\n\n- **Where to play**: the local player's own character → play on the client (replicates automatically via\n its Animator); NPCs → play on the server (or on clients for purely cosmetic, high-count NPCs).\n- Load each `Animation` once per Animator and reuse the track; don't call `LoadAnimation` every time an\n action happens (there's a per-Animator track limit). Exception: server authority (query live tracks).\n- Priorities: `Core < Idle < Movement < Action < Action2 < Action3 < Action4`. Blend with `AdjustWeight`,\n change speed with `AdjustSpeed`, stop with `Stop(fadeTime)`.\n- Events: `track.Stopped`, `track.Ended`, markers via `GetMarkerReachedSignal` (`KeyframeReached` is superseded).\n- Animation assets must be owned by (or shared with, via asset permissions) the experience's owner —\n otherwise they silently fail to play in live servers.\n- Replace defaults by editing the `Animate` script's `StringValue`/`Animation` children\n (`animate.run.RunAnim.AnimationId`) on `CharacterAdded`, or ship a custom `Animate` in\n `StarterCharacterScripts`.\n- Procedural: `IKControl` (look-at, foot planting, reaching) instead of hand-written joint math.\n\n## Tweening\n\n```luau\n--!strict\nlocal TweenService = game:GetService(\"TweenService\")\n\nlocal door = Instance.new(\"Part\")\ndoor.Anchored = true\ndoor.Parent = workspace\n\nlocal info = TweenInfo.new(0.6, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)\nlocal open = TweenService:Create(door, info, { CFrame = door.CFrame * CFrame.new(0, 8, 0) })\nopen.Completed:Once(function(state)\n\tprint(\"door tween finished:\", state)\nend)\nopen:Play()\n```\n\n- Tween **anchored** parts or UI. For unanchored/physics objects use constraints.\n- Tweening on the server replicates every frame and stutters: for visual-only motion, tween on each client\n (the server sets the final state or an attribute clients react to).\n- Models: tween a `CFrameValue` and call `model:PivotTo(value)` in its `Changed` handler, or tween the\n primary part with the rest welded to it.\n- `TweenService:GetValue(alpha, style, direction)` for custom interpolation; `SmoothDamp` for follow motion.\n\n## Tools\n\n`Tool` in `StarterPack`/`Backpack` with a `Handle` part (or `RequiresHandle = false`). `Equipped`,\n`Unequipped`, `Activated` fire on both client and server (`Activated` comes from the client: validate\nrate and state on the server). Keep weapon logic in modules and use tags rather than scripts inside\nevery tool clone.\n\n## Character Controller Library (beta)\n\nRoblox's CCL (`require(\"@rbx/AvatarAbilities\")`) replaces the fixed Humanoid state machine with\ncomposable **abilities** (Running, Jumping, Climbing, custom Dash...) coordinated by labels, sensors,\nconditions (`StartsWhen`/`RunsWhile` with `All`/`Any`/`Not`), conflicts (`Blocks`/`Stops`/`Suspends`),\nand lifecycle callbacks. Enable it via **File → Beta Features** and **Avatar Settings → Movement →\nAbilities**. Only use it when the project has opted in; APIs may change while in beta.\nSketch: [references/character-controller-library.md](references/character-controller-library.md).\n\n## Related skills\n\n`roblox-npc-ai` (NPC movement), `roblox-physics` (ragdolls, knockback), `roblox-input` (ability inputs),\n`roblox-security` (movement/damage validation), `roblox-game-systems` (combat).\n",
12
+ "skills/roblox-characters-animation/references/character-controller-library.md": "# Character Controller Library (CCL) — beta\n\nSource: Roblox \"Character Controller Library\" and \"Custom abilities\" docs (2026). Beta: enable\n**File → Beta Features → AvatarAbilities Character Controller Library**, then **Avatar Settings →\nMovement → Abilities → Character Controller Library**. APIs may change; re-check the docs.\n\n## Concepts\n\n- **Ability**: a table (`AvatarAbilities.AbilityDefinition`) in a ModuleScript shared by server and client.\n- **Labels**: named bits in a 64-bit world mask. Active abilities broadcast `Labels`; `TimedLabels`\n (`OnStart`, `OnStop` with durations in seconds, `Consumes` list) broadcast or remove labels temporarily.\n- **Sensors**: engine facts such as `Identifiers.Sensor.Ground`.\n- **Conditions**: `StartsWhen` / `RunsWhile` built from labels, sensors, and `Rule.All`, `Rule.Any`, `Rule.Not`\n (`Not` takes a single label). They compile to bitmask math.\n- **Conflicts**: `Blocks`, `Stops`, `Suspends`, `ExclusiveGroup`.\n- **Input**: `{ InputName = \"Dash\", Mode = \"Press\", ActionSlot = 5 }` — injected into `StartsWhen`.\n- **Callbacks**: `OnSetup`, `OnStart`, `OnUpdate`, `OnStop`, `OnTeardown` with\n `(managerCtx: AvatarAbilities.ManagerContext, abilityCtx: AvatarAbilities.AbilityContext)`.\n `managerCtx.AbilityOwner` is the character; `abilityCtx.Config` / `abilityCtx.State` hold per-ability config and replicated state.\n\n## Custom dash ability\n\n```luau\n-- ReplicatedStorage/CustomAbilities/Dash (ModuleScript)\nlocal AvatarAbilities = require(\"@rbx/AvatarAbilities\")\nlocal Identifiers = AvatarAbilities.Identifiers\nlocal Rule = AvatarAbilities.Rule\nlocal Sensor = Identifiers.Sensor\nlocal All, Not = Rule.All, Rule.Not\n\nlocal Dash: AvatarAbilities.AbilityDefinition = {\n\tName = \"Dash\",\n\tLabels = { \"Dashing\" },\n\tStartsWhen = All(Sensor.Ground, Not(\"DashCooldown\")),\n\tRunsWhile = \"DashWindow\",\n\tInput = { InputName = \"Dash\", Mode = \"Press\", ActionSlot = 5 },\n\tTimedLabels = {\n\t\tOnStart = { DashWindow = 0.2 }, -- active for 0.2 s\n\t\tOnStop = { DashCooldown = 1.0 }, -- then 1 s cooldown\n\t},\n}\n\nfunction Dash.OnStart(managerCtx: AvatarAbilities.ManagerContext, _abilityCtx: AvatarAbilities.AbilityContext)\n\tlocal rootPart = managerCtx.AbilityOwner.PrimaryPart\n\tif rootPart then\n\t\trootPart:ApplyImpulse(managerCtx.RootLookVector * 80 * rootPart.AssemblyMass)\n\tend\nend\n\nreturn Dash\n```\n\n## Registering abilities (server)\n\n```luau\n--!strict\nlocal Players = game:GetService(\"Players\")\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal AvatarAbilities = require(\"@rbx/AvatarAbilities\")\n\nlocal CUSTOM_ABILITIES = {\n\tReplicatedStorage.CustomAbilities.Dash,\n}\n\nlocal function onCharacterAdded(character: Model)\n\tlocal actor = character:WaitForChild(\"AbilityManagerActor\", 10)\n\tif not actor then\n\t\treturn\n\tend\n\twhile not actor:IsDescendantOf(game) do\n\t\tactor.AncestryChanged:Wait()\n\tend\n\tfor _, abilityModule in CUSTOM_ABILITIES do\n\t\tAvatarAbilities.addAbilityForCharacter(character, abilityModule) -- pass the module, don't require it\n\tend\nend\n\nlocal function onPlayerAdded(player: Player)\n\tplayer.CharacterAdded:Connect(onCharacterAdded)\n\tif player.Character then\n\t\ttask.spawn(onCharacterAdded, player.Character)\n\tend\nend\n\nPlayers.PlayerAdded:Connect(onPlayerAdded)\nfor _, player in Players:GetPlayers() do\n\tonPlayerAdded(player)\nend\n```\n\nCallbacks run in both the predicted client simulation and the authoritative server simulation: keep\nthem deterministic (no `math.random` without shared seeds, no client-only state).\n",
13
+ "skills/roblox-code-review/SKILL.md": "---\nname: roblox-code-review\ndescription: Review Roblox/Luau code for exploits, data loss, memory leaks, performance problems, lifecycle bugs, policy issues, and deprecated or superseded APIs, using a severity-ranked checklist and the full 2026 deprecation map. Use when reviewing a PR or script, auditing a game before launch, modernizing legacy code, or asked what is wrong with some Roblox code.\n---\n\n# Roblox code review\n\nReview in severity order. Report each finding with **file:line, severity, what breaks, and the fix**.\nVerify claims against the code (and the engine reference when unsure) — don't pattern-match blindly.\n\n## Severity guide\n\n| Severity | Examples |\n| --- | --- |\n| **Critical** | Client-authoritative rewards/damage/currency; unvalidated remote args (NaN, instance spoofing); purchases granted outside `ProcessReceipt` or non-idempotent; data saved with `SetAsync` from multiple servers without session locking; secrets/API keys in replicated code; unfiltered player text shown to others. |\n| **High** | Data loss paths (no `BindToClose`, saving default data after a failed load, no retry); memory leaks (connections, player-keyed tables); `InvokeClient`; per-frame remotes; infinite yields (`WaitForChild` without timeout on optional instances, `RemoteFunction` handlers that can yield forever); streaming-unsafe client indexing. |\n| **Medium** | Deprecated/superseded APIs; `wait`/`spawn`/`delay`; polling loops; server-side tweens; missing `pcall` on network/yielding APIs; missing late-joiner handling; race conditions on `CharacterAdded`. |\n| **Low** | Style, naming, missing types, magic numbers, dead code, minor inefficiencies. |\n\n## Checklist\n\n**Security** (`roblox-security`)\n- Every `OnServerEvent`/`OnServerInvoke`: types checked with `typeof`, numbers `math.isfinite`, strings\n length/UTF-8 checked, instances `IsA` + `IsDescendantOf(trusted)`, rate limited, context checked.\n- No server-only logic, admin lists, or secrets in `ReplicatedStorage`/`StarterPlayer`/`Workspace`.\n- `ProximityPrompt`/`ClickDetector`/`Touched` handlers re-validate distance/state on the server.\n- Gameplay-critical unanchored parts are server-owned.\n\n**Data** (`roblox-data-stores`)\n- Session locking (ProfileStore or equivalent); `UpdateAsync` for read-modify-write; retries with backoff.\n- `BindToClose` flushes saves in parallel; no saves of `Instance`/`Vector3`/mixed tables.\n- Failed loads never overwritten with defaults; player-keyed tables cleared on leave.\n- Keys stable (`User_{UserId}`), Studio uses a separate store/universe.\n\n**Monetization** (`roblox-monetization`)\n- Single `ProcessReceipt`; idempotent by `PurchaseId`; returns `PurchaseGranted` only after persisting.\n- Pass perks checked on join **and** after in-session purchase; prices not hardcoded; paid random items\n disclose odds and respect `PolicyService`.\n\n**Correctness & lifecycle** (`roblox-architecture`)\n- Handles players already in game (`for _, p in Players:GetPlayers()`) and characters already spawned.\n- Per-character state rebuilt on respawn; per-player state cleaned on `PlayerRemoving`.\n- No yields at module top level; no circular requires; one owner per piece of state.\n- Deferred-event safe (doesn't rely on handlers running synchronously).\n- Client code streaming-safe (`WaitForChild`/nil checks for `Workspace` content).\n\n**Performance** (`roblox-performance`)\n- No per-frame `FindFirstChild`/`GetDescendants`/`Instance.new`; params objects reused.\n- Event-driven instead of polling; think loops throttled and staggered.\n- Tweens and cosmetic effects on clients; remotes batched/throttled; payloads minimal.\n- Connections disconnected / instances destroyed; no unbounded caches.\n\n**Text & safety** (`roblox-text-chat`)\n- All player-authored text shown to others filtered server-side (`FilterStringAsync`), fail closed.\n- Chat uses `TextChatService`; public editable text rate-limited (≥ 1 minute).\n\n**Modern API usage**\n- `task.*`, `*Async` variants, `Raycast`, mover constraints, `PivotTo`, `Animator:LoadAnimation`,\n `TextChatService`, `UIDragDetector`, `workspace` collision-group APIs, `CreatePath`+`ComputeAsync`.\n- Full map of deprecated and superseded members: [references/deprecated-apis.md](references/deprecated-apis.md).\n\n**Code quality** (`roblox-luau`)\n- `--!strict` with typed public APIs; services via `GetService`; no `_G`/`shared`.\n- Clear module boundaries; config separated from logic; tests for pure logic (`roblox-testing`).\n\n## Output format\n\n```text\n## Summary\n<2–3 sentences: overall risk, the most important fixes>\n\n## Findings\n1. [Critical] src/server/Shop.server.luau:42 — Price read from client argument.\n Impact: exploiters buy anything for 0 coins. Fix: look up ITEMS[itemId].price on the server.\n2. [High] ...\n\n## Modernization (optional)\n- Replace `wait()` → `task.wait()` (12 occurrences) ...\n```\n\nOffer to apply fixes; when fixing, keep behavior identical except for the bug, and re-run the relevant\nchecks (type check, tests, Studio playtest via `roblox-studio-mcp`).\n",
14
+ "skills/roblox-code-review/references/deprecated-apis.md": "# Deprecated and superseded Roblox APIs (engine reference, Studio 0.740, September 2026)\n\n\"Superseded\" members still work but have a documented replacement — treat them as deprecated in new\ncode. Search this file for the member you're looking at. Camel-case aliases (`:findFirstChild`,\n`:remove`, `.className`, ...) are all deprecated in favor of their PascalCase versions.\n\n## Globals and libraries\n\n| Deprecated | Use |\n| --- | --- |\n| `wait()` / `spawn()` / `delay()` | `task.wait()` / `task.spawn()` / `task.delay()` |\n| `ypcall`, `elapsedTime`, `version`, `printidentity`, `stats()` | `pcall`, `os.clock`/`time()`, — , — , `game:GetService(\"Stats\")` |\n| `getfenv` / `setfenv` | nothing (disables optimizations) |\n| `table.getn`, `table.foreach`, `table.foreachi` | `#t`, `for k, v in t do` |\n| `Ray.new` + `FindPartOnRay*` | `workspace:Raycast(origin, direction, RaycastParams)` |\n| `Region3` + `FindPartsInRegion3*`, `IsRegion3Empty*` | `workspace:GetPartBoundsInBox(cframe, size, OverlapParams)` |\n\n## Players and characters\n\n| Deprecated | Use |\n| --- | --- |\n| `Player:LoadCharacter()` | `Player:LoadCharacterAsync()` |\n| `Player:LoadCharacterWithHumanoidDescription()` | `LoadCharacterWithHumanoidDescriptionAsync()` |\n| `Player:LoadCharacterAppearance()`, `Player.CharacterAppearance`, `Players:GetCharacterAppearanceAsync()` | `HumanoidDescription` APIs |\n| `Player:GetRankInGroup(Async)`, `Player:GetRoleInGroup(Async)` | `GroupService:GetRolesInGroupAsync()` |\n| `Player:IsInGroup()` | `Player:IsInGroupAsync()` |\n| `Player:IsFriendsWith()`, `IsBestFriendsWith()` | `Player:IsFriendsWithAsync()` |\n| `Player:GetFriendsOnline()` | `Player:GetFriendsOnlineAsync()` |\n| `Player.MembershipType` | `Player.HasRobloxSubscription` |\n| `Player:Save*/Load*`, `DataReady`, `WaitForDataReady`, `DataComplexity` | `DataStoreService` |\n| `Players.NumPlayers` | `#Players:GetPlayers()` |\n| `Players:GetHumanoidDescriptionFromUserId/OutfitId()` | `...Async()` variants |\n| `Players:CreateHumanoidModelFromDescription/UserId()` | `...Async()` variants |\n| `Humanoid:LoadAnimation()`, `AnimationController:LoadAnimation()` | `Animator:LoadAnimation()` |\n| `Humanoid:ApplyDescription()` / `ApplyDescriptionReset()` / `PlayEmote()` | `...Async()` variants |\n| `Humanoid:*Status*` (RbxStatus), `Humanoid.Torso/LeftLeg/RightLeg` (R6-only) | attributes/tags; R15 body parts |\n| `AnimationTrack.KeyframeReached` | `AnimationTrack:GetMarkerReachedSignal(name)` |\n| `KeyframeSequenceProvider:GetKeyframeSequence*`, `AnimationClipProvider:GetAnimationClip*` | `AnimationClipProvider:GetAnimationClipAsync()` |\n| `PoseBase.Weight`, `Pose.MaskWeight` | `AnimationTrack:AdjustWeight()` |\n\n## Physics and parts\n\n| Deprecated | Use |\n| --- | --- |\n| `BodyVelocity`, `BodyPosition`, `BodyGyro`, `BodyForce`, `BodyThrust`, `BodyAngularVelocity`, `RocketPropulsion` | `LinearVelocity`, `AlignPosition`, `AlignOrientation`, `VectorForce`, `AngularVelocity`, `LineForce` |\n| `BasePart.Velocity` / `RotVelocity` | `AssemblyLinearVelocity` / `AssemblyAngularVelocity` (or `ApplyImpulse`) |\n| `BasePart.Friction/Elasticity/SpecificGravity` | `CustomPhysicalProperties` |\n| `BasePart:MakeJoints/BreakJoints`, `Model:MakeJoints`, surface joints (`Glue`, `Snap`, `Rotate*`, `Motor`, `ManualWeld`), `JointsService` | `WeldConstraint`, `HingeConstraint`, `GetJoints()` + `Destroy()` |\n| `BasePart:GetRenderCFrame()` | `BasePart.CFrame` |\n| `BasePart.StoppedTouching`, `LocalSimulationTouched` | `TouchEnded`, `Touched` |\n| `PhysicsService:CreateCollisionGroup/RemoveCollisionGroup/GetCollisionGroups` | `workspace:RegisterCollisionGroup/UnregisterCollisionGroup/GetRegisteredCollisionGroups` |\n| `PhysicsService:RegisterCollisionGroup`, `CollisionGroupSetCollidable`, `CollisionGroupsAreCollidable`, `IsCollisionGroupRegistered`, `RenameCollisionGroup`, `UnregisterCollisionGroup`, `GetMaxCollisionGroups` (superseded) | same methods on `workspace` (WorldRoot) |\n| `PhysicsService:SetPartCollisionGroup`, `GetCollisionGroupId/Name`, `CollisionGroupContainsPart`, `BasePart.CollisionGroupId` | `BasePart.CollisionGroup` (string) |\n| `Model:SetPrimaryPartCFrame()` / `GetPrimaryPartCFrame()` | `Model:PivotTo()` / `Model:GetPivot()` |\n| `Model:GetModelCFrame()` / `GetModelSize()` | `GetPivot()`/`GetBoundingBox()` / `GetExtentsSize()` |\n| `AlignOrientation.PrimaryAxisOnly` | `AlignOrientation.AlignType` |\n| `Attachment.Rotation/WorldRotation`, `Get/SetAxis`, `Get/SetSecondaryAxis` | `Orientation`/`WorldOrientation`, `Axis`/`SecondaryAxis` properties / `CFrame` |\n| `TorsionSpringConstraint.LimitEnabled` | `LimitsEnabled` |\n| `VehicleSeat.Steer` / `Throttle` | `SteerFloat` / `ThrottleFloat` |\n| `SkateboardPlatform`, `Flag`, `FlagStand` | custom systems |\n| `FormFactorPart.FormFactor` | `Size` directly |\n\n## Workspace, world, rendering\n\n| Deprecated | Use |\n| --- | --- |\n| `Workspace.FilteringEnabled` | nothing (always on) |\n| `game.Workspace`, `game.lighting` | `workspace`, `game:GetService(\"Lighting\")` |\n| `Lighting.Technology` (superseded) | `Lighting.LightingStyle` + `PrioritizeLightingQuality` |\n| `Lighting.Outlines`, `ShadowColor`, `GetMoonPhase` | nothing |\n| `Decal.Texture`, `Decal.TextureContent` (superseded) | `Decal.ColorMapContent` |\n| `Decal.Shiny/Specular` | `SurfaceAppearance` / materials |\n| `Terrain:SetCell/GetCell/SetCells/AutowedgeCell(s)/GetWaterCell/SetWaterCell`, `ConvertToSmooth`, `IsSmooth` | `FillBlock/FillBall/ReadVoxels/WriteVoxels` |\n| `Sparkles.Color`, `ParticleEmitter.VelocitySpread`, `SelectionBox/Sphere.SurfaceColor`, `GuiBase3d.Color` | `SparkleColor`, `SpreadAngle`, `SurfaceColor3`, `Color3` |\n| `Camera.CoordinateFrame`, `Camera:Interpolate`, `InterpolationFinished`, `SetRoll/GetRoll`, `PanUnits/TiltUnits`, `SetCameraPanMode` | `Camera.CFrame`, `TweenService`, CFrame math |\n| `Hint`, `Message` | `TextLabel` in a `ScreenGui` |\n| `Hat` | `Accessory` |\n| `Skin` | `BodyColors` |\n| `HopperBin`, `Hopper` | `Tool`, `StarterPack` |\n| `FloorWire`, `SelectionPartLasso`, `SelectionPointLasso` | `Beam` |\n| `CustomEvent`, `CustomEventReceiver` | `BindableEvent` (or a Luau signal module) |\n| `IntConstrainedValue`, `DoubleConstrainedValue` | `IntValue`/`NumberValue` + `math.clamp`, or attributes |\n| `Debris.MaxItems` | nothing |\n| `Instance:Remove()` | `Instance:Destroy()` / `Parent = nil` |\n| `Instance.RobloxLocked` | nothing |\n| `DataModel.OnClose` | `game:BindToClose()` |\n| `DataModel.VIPServerId/VIPServerOwnerId` | `PrivateServerId` / `PrivateServerOwnerId` |\n| `DataModel:GetRemoteBuildMode()` | `RunService:IsServer()` |\n| `DataModel.ItemChanged` | `Instance.Changed` / `GetPropertyChangedSignal` |\n| `BaseScript.LinkedSource`, `ModuleScript.LinkedSource` | Packages |\n| `CollectionService.ItemAdded/ItemRemoved`, `GetCollection` | `GetInstanceAddedSignal/GetInstanceRemovedSignal`, `GetTagged` |\n\n## UI and input\n\n| Deprecated | Use |\n| --- | --- |\n| `GuiObject:TweenPosition/TweenSize/TweenSizeAndPosition` | `TweenService:Create()` |\n| `GuiObject.Draggable`, `DragBegin`, `DragStopped` | `UIDragDetector` |\n| `GuiObject.BackgroundColor/BorderColor/Transparency` | `BackgroundColor3`/`BorderColor3`/`BackgroundTransparency` |\n| `TextLabel/TextButton/TextBox.FontSize`, `TextColor`, `TextWrap` | `TextSize`, `TextColor3`, `TextWrapped` |\n| `StarterGui.ResetPlayerGuiOnSpawn` | `ScreenGui.ResetOnSpawn` |\n| `GuiService:IsTenFootInterface()` (superseded) | `GuiService.ViewportDisplaySize` |\n| `GuiService.IsModalDialog`, `IsWindows`, `AddSelectionParent/Tuple`, `RemoveSelectionGroup` | `SelectionGroup`, `UserInputService.PreferredInput` |\n| `UIGridStyleLayout:SetCustomSortFunction` | `SortOrder = LayoutOrder`/`Name` |\n| `Mouse.KeyDown/KeyUp` | `UserInputService` / Input Action System |\n| `UserInputService.ModalEnabled` | `GuiService.TouchControlsEnabled` |\n| `UserInputService.VREnabled`, `UserHeadCFrame`, `GetUserCFrame`, `UserCFrameChanged` | `VRService` equivalents |\n| `ContextActionService:BindActionToInputTypes` | `BindAction` (or Input Action System) |\n| `InputAction:Fire()` | `InputBinding:Fire()` |\n| `UserGameSettings.ControlMode` | `UserInputService.MouseBehavior` for shift-lock style control |\n\n## Services\n\n| Deprecated | Use |\n| --- | --- |\n| `MarketplaceService:GetProductInfo()` | `GetProductInfoAsync()` |\n| `MarketplaceService:PlayerOwnsAsset()` / `PlayerOwnsBundle()` | `...Async()` variants |\n| `MarketplaceService:PromptPremiumPurchase()` | `PromptRobloxSubscriptionPurchase()` |\n| `GamePassService:PlayerHasPass()` | `MarketplaceService:UserOwnsGamePassAsync()` |\n| `BadgeService:AwardBadge()` / `UserHasBadge()` / `IsDisabled()` / `IsLegal()` | `AwardBadgeAsync()` / `UserHasBadgeAsync()` / `GetBadgeInfoAsync().IsEnabled` / — |\n| `TeleportService:Teleport`, `TeleportToPlaceInstance`, `TeleportToPrivateServer`, `TeleportPartyAsync`, `TeleportToSpawnByName` | `TeleportService:TeleportAsync()` + `TeleportOptions` |\n| `TeleportService:ReserveServer()` | `ReserveServerAsync()` |\n| `TeleportService.CustomizedTeleportUI` | `SetTeleportGui` |\n| `GlobalDataStore:OnUpdate()` | `MessagingService` |\n| `DataStore:RemoveVersionAsync()` | versions expire automatically; `RemoveAsync` for keys |\n| `PathfindingService:FindPathAsync()` (superseded), `ComputeRawPathAsync`, `ComputeSmoothPathAsync`, `Path:GetPointCoordinates`, `Path:CheckOcclusionAsync` | `CreatePath()` + `Path:ComputeAsync()`, `GetWaypoints()`, `Path.Blocked` |\n| `AnalyticsService:FireEvent/FireCustomEvent/FireInGameEconomyEvent/FirePlayerProgressionEvent/FireLogEvent` | `LogCustomEvent`, `LogEconomyEvent`, `LogProgressionEvent` (+ Start/Complete/Fail), `LogFunnelStepEvent` |\n| `Chat` service, `Chat:FilterStringForPlayerAsync`, legacy ChatService modules, `TextChatService.ChatVersion` | `TextChatService`; `TextService:FilterStringAsync` |\n| `TextFilterResult:GetChatForUserAsync()` (returns empty string) | `TextChannel`s for chat; `GetNonChatStringForUserAsync/ForBroadcastAsync` for other text |\n| `TextService:FilterAndTranslateStringAsync()` | `FilterStringAsync` |\n| `LocalizationService:GetTranslatorForPlayer()` | `GetTranslatorForPlayerAsync()` |\n| `LocalizationTable:GetString/GetContents/SetContents/SetEntry/RemoveKey`, `DevelopmentLanguage` | `GetTranslator`, `GetEntries/SetEntries`, `RemoveEntry`, `SourceLocaleId` |\n| `InsertService:Insert/GetFreeModels/GetFreeDecals`, sets APIs | `LoadAsset`, `GetFreeModelsAsync`, `GetFreeDecalsAsync` |\n| `AssetService:SearchAudio/GetAssetIdsForPackage` | `SearchAudioAsync`/`GetAssetIdsForPackageAsync` |\n| `AudioPlayer.AssetId` | `AudioPlayer.Asset` |\n| `Sound.Pitch`, `Sound.MinDistance/MaxDistance/EmitterSize` | `PlaybackSpeed`, `RollOffMinDistance/RollOffMaxDistance` |\n| `SocialService:PromptLinkSharing()` | `PromptLinkSharingAsync()` |\n| `OpenCloudService`, `OpenCloudApiV1` | `HttpService:RequestAsync` with an API key secret |\n| `ContentProvider:Preload()` | `PreloadAsync()` |\n| `Stats.HeartbeatTimeMs/PhysicsStepTimeMs` | `HeartbeatTime`/`PhysicsStepTime` |\n| `TestService:Run()`, `FunctionalTest` | `TestService:RunAsync()` / a test framework (Jest Lua) |\n| `Teams:RebalanceTeams()`, `Team.Score`, `Team.AutoColorCharacters` | custom logic |\n| `PointsService` | badges / your own progression |\n| `AdService:ShowVideoAd`, `AdGui.OnAdEvent` | Rewarded video via `AdService:ShowRewardedVideoAdAsync` |\n| `GenerationService:GenerateMeshAsync` (scheduled for deprecation) | `GenerateModelAsync` |\n",
15
+ "skills/roblox-data-stores/SKILL.md": "---\nname: roblox-data-stores\ndescription: Persistence and cross-server data - DataStoreService (UpdateAsync, retries, budgets, limits), session locking with ProfileStore, schema migrations, OrderedDataStore leaderboards, MemoryStore queues/sorted maps/hash maps, MessagingService, right-to-be-forgotten. Use when saving player data, building leaderboards or queues, or fixing data loss, duplication, or throttling.\n---\n\n# Roblox data stores and cloud services\n\n## Choose the service\n\n| Need | Service | Notes |\n| --- | --- | --- |\n| Player progress, inventory, settings | **DataStore** via ProfileStore (session-locked) | One key per player (`User_{UserId}`), ≤ 4 MB. |\n| Global leaderboards | `OrderedDataStore` | Integer values only; periodic updates, cached reads. |\n| Fast, temporary, cross-server state | `MemoryStoreService` | Sorted maps, queues, hash maps; everything expires (TTL ≤ 45 days). |\n| Matchmaking queues, live events, server lists | MemoryStore queue / sorted map | Not durable — never the only copy of valuable data. |\n| Broadcast to all servers | `MessagingService` | ≤ 1 kB per message, best effort, no delivery guarantee. |\n| Values changed by live-ops without publishing | Experience configs (`ConfigService`) | See `roblox-analytics-liveops`. |\n\n## Player data: use session locking\n\nTwo servers must never own the same player's data at once (teleports, fast rejoins), or items duplicate\nand progress is lost. **ProfileStore** (loleris) implements session locking, auto-save, and\n`BindToClose` flushing, and is the recommended default. Install it from Wally/the Creator Store and put\nit in `ServerScriptService`.\n\n```luau\n-- ServerScriptService/Server/Services/DataService.luau (ProfileStore)\nlocal Players = game:GetService(\"Players\")\nlocal ServerScriptService = game:GetService(\"ServerScriptService\")\n\nlocal ProfileStore = require(ServerScriptService.ServerPackages.ProfileStore)\n\nlocal TEMPLATE = {\n\tversion = 1,\n\tcoins = 0,\n\tinventory = {} :: { [string]: number },\n}\ntype PlayerData = typeof(TEMPLATE)\n\nlocal PlayerStore = ProfileStore.New(\"PlayerData\", TEMPLATE)\nlocal profiles: { [Player]: typeof(PlayerStore:StartSessionAsync(\"\")) } = {}\n\nlocal function onPlayerAdded(player: Player)\n\tlocal profile = PlayerStore:StartSessionAsync(`User_{player.UserId}`, {\n\t\tCancel = function()\n\t\t\treturn player.Parent ~= Players -- stop waiting if the player left\n\t\tend,\n\t})\n\tif profile == nil then\n\t\tplayer:Kick(\"Could not load your data. Please rejoin.\")\n\t\treturn\n\tend\n\tprofile:AddUserId(player.UserId) -- GDPR / right-to-be-forgotten linkage\n\tprofile:Reconcile() -- fill in fields added to TEMPLATE since this profile was created\n\tprofile.OnSessionEnd:Connect(function()\n\t\tprofiles[player] = nil\n\t\tplayer:Kick(\"Your data was loaded on another server. Please rejoin.\")\n\tend)\n\tif player.Parent == Players then\n\t\tprofiles[player] = profile\n\telse\n\t\tprofile:EndSession() -- left while loading\n\tend\nend\n\nfor _, player in Players:GetPlayers() do\n\ttask.spawn(onPlayerAdded, player)\nend\nPlayers.PlayerAdded:Connect(onPlayerAdded)\nPlayers.PlayerRemoving:Connect(function(player)\n\tlocal profile = profiles[player]\n\tif profile then\n\t\tprofile:EndSession() -- saves and releases the lock\n\tend\nend)\n\nlocal DataService = {}\nfunction DataService.get(player: Player): PlayerData?\n\tlocal profile = profiles[player]\n\treturn profile and profile.Data\nend\nreturn DataService\n```\n\nMutate `profile.Data` directly on the server (it is cached in memory and auto-saved). Don't\n`SetAsync` per change. Check the ProfileStore docs for the exact current API before relying on less\ncommon features (global updates, `MessageAsync`, mock stores for Studio).\n\n## Raw DataStoreService rules (when not using a wrapper)\n\n- **Buffer in memory**: load once on join, mutate the in-memory table, save periodically (e.g. every\n 2–5 minutes with random jitter), on leave, at checkpoints (purchases), and in `game:BindToClose`.\n- **`UpdateAsync` over `SetAsync`** whenever the write depends on the old value or multiple servers may\n write the key. The transform function must be pure and fast (no yields); return `nil` to cancel.\n- **Retry transient errors** with exponential backoff + jitter, capped attempts; process retries for\n a key in order (an old retry must not overwrite newer data). Treat a failed write as \"outcome unknown\".\n- Never save `Instance`s, `Vector3`, `CFrame`, functions, or mixed/sparse tables: store plain tables,\n numbers, strings, booleans, and `buffer`s. Serialize datatypes (`{ x, y, z }`). Validate UTF-8 strings.\n- Keys and data store names ≤ 50 characters; value ≤ 4,194,304 characters after JSON encoding.\n- Stable key patterns (`User_{UserId}`), never display names. Few data stores, few keys per player.\n- Studio needs **File → Experience Settings → Security → Enable Studio Access to API Services**; use a separate test\n universe or a different data store name so testing never touches production data.\n\nComplete raw implementation (session lock, autosave, BindToClose, retries):\n[references/raw-datastore.md](references/raw-datastore.md).\n\n## Budgets and limits (2026)\n\n| Limit | Value |\n| --- | --- |\n| Experience-wide standard reads / writes | `300 + 40 × CCU` / `300 + 20 × CCU` per minute (shared with Open Cloud) |\n| Default per-server reads/writes | `60 + 40 × players` per minute each (configurable) |\n| Ordered writes per server | `30 + 5 × players` per minute |\n| Per-key throughput | 25 MB/min read, 4 MB/min write |\n| Storage (latest versions, compressed) | `500 MB + 1 MB × lifetime users` per experience |\n| Queue per request type | 30 pending requests, then errors 301–306 |\n\n- `UpdateAsync` consumes both read and write budget.\n- Check `DataStoreService:GetRequestBudgetForRequestType(Enum.DataStoreRequestType.StandardWrite)` before\n bulk work; tune per-server limits once at startup with `SetRateLimitForRequestType`.\n- Don't pre-compress data; Roblox compresses automatically. Shard only genuinely hot keys.\n- Use versions (`ListVersionsAsync`, `GetVersionAtTimeAsync`) to restore data instead of writing backup keys.\n\n## Schema versioning and migrations\n\nStore a `version` field. On load, run ordered migration functions until the data reaches the current\nversion, then reconcile defaults. Never delete fields in the same release that stops writing them;\nnever reuse field names with different meanings.\n\n```luau\n--!strict\ntype Data = { [string]: any }\n\nlocal MIGRATIONS: { (Data) -> () } = {\n\t[1] = function(data) -- v1 -> v2: \"gold\" renamed to \"coins\"\n\t\tdata.coins = data.gold or 0\n\t\tdata.gold = nil\n\tend,\n\t[2] = function(data) -- v2 -> v3: inventory list -> counts\n\t\tlocal counts: { [string]: number } = {}\n\t\tfor _, itemId in data.inventory or {} do\n\t\t\tcounts[itemId] = (counts[itemId] or 0) + 1\n\t\tend\n\t\tdata.inventory = counts\n\tend,\n}\nlocal CURRENT_VERSION = #MIGRATIONS + 1\n\nlocal function migrate(data: Data): Data\n\tlocal version = data.version or 1\n\twhile version < CURRENT_VERSION do\n\t\tMIGRATIONS[version](data)\n\t\tversion += 1\n\tend\n\tdata.version = CURRENT_VERSION\n\treturn data\nend\n\nprint(migrate({ gold = 5, inventory = { \"Sword\", \"Sword\" } }))\n```\n\n## Leaderboards (OrderedDataStore)\n\n- Values must be integers. Write on a schedule or on meaningful change, not every increment.\n- Read top N with `GetSortedAsync(false, 100)` on **one** server loop (e.g. every 60 s), cache the\n result, and replicate to clients. Don't let every server/client read constantly.\n- For \"this week\" boards, include the period in the store name (`Wins_2026W39`) or use a MemoryStore sorted map.\n\n## MemoryStore essentials\n\n- Quotas are experience-wide: memory `64 KB + 1.2 KB × users`; requests `1000 + 120 × CCU` units/min.\n- `MemoryStoreSortedMap`: ranked data, server browsers, live leaderboards (`SetAsync` with expiration,\n `GetRangeAsync`, `UpdateAsync`).\n- `MemoryStoreQueue`: matchmaking/work queues (`AddAsync`, `ReadAsync` + `RemoveAsync` with the returned id\n after successful processing, invisibility timeout for crash safety).\n- `MemoryStoreHashMap`: high-volume key-value without sorting (`SetAsync`, `GetAsync`, `UpdateAsync`, `ListItemsAsync`).\n- Always set expirations; handle throttling (`pcall` + retry); keep items small.\n\n## MessagingService essentials\n\n- Message ≤ 1 kB. Per server: `600 + 240 × players` sends/min; `20 + 8 × players` subscriptions.\n- Delivery is **best effort** and not ordered: use it for notifications (\"refresh your cache\",\n global announcements), never as the source of truth. Pair with DataStore/MemoryStore for state.\n- Wrap `SubscribeAsync`/`PublishAsync` in `pcall`; they can fail or yield.\n\nMore code (MemoryStore queue matchmaking, cross-server announcement, RTBF): [references/memory-and-messaging.md](references/memory-and-messaging.md).\n\n## Right to be forgotten\n\nUse static key patterns so **automated RTBF** (configured in Creator Dashboard) can delete player data\ntemplates like `PlayerData/User_{userId}`. Otherwise handle the right-to-erasure webhook (see\n`roblox-open-cloud`). Link keys to user IDs: `SetAsync(key, value, { userId })`, return them as the second value from an\n`UpdateAsync` transform, or call ProfileStore's `AddUserId`.\n\n## Related skills\n\n`roblox-security` (duplication and trade exploits), `roblox-monetization` (receipts),\n`roblox-open-cloud` (external access, bulk operations), `roblox-teleport-matchmaking`.\n",
16
+ "skills/roblox-data-stores/references/memory-and-messaging.md": "# MemoryStore and MessagingService recipes\n\n## Matchmaking queue (MemoryStoreQueue)\n\n```luau\n--!strict\nlocal MemoryStoreService = game:GetService(\"MemoryStoreService\")\n\nlocal queue = MemoryStoreService:GetQueue(\"Ranked1v1\", 30) -- 30 s invisibility timeout\nlocal EXPIRATION = 300\n\nlocal function enqueue(player: Player)\n\tlocal ok, err = pcall(function()\n\t\tqueue:AddAsync({ userId = player.UserId, rating = 1000 }, EXPIRATION)\n\tend)\n\tif not ok then\n\t\twarn(\"enqueue failed:\", err)\n\tend\nend\n\n-- One matchmaker loop (e.g. on every server, or a dedicated lobby server).\nlocal function matchmakeOnce()\n\tlocal ok, items, readId = pcall(function()\n\t\treturn queue:ReadAsync(2, true, 5) -- exactly 2 items or none; wait up to 5 s\n\tend)\n\tif not ok or not items or #items < 2 then\n\t\treturn\n\tend\n\t-- Reserve a server and teleport both players (see roblox-teleport-matchmaking) ...\n\t-- Only after success, remove the items; otherwise they reappear after the invisibility timeout.\n\tpcall(function()\n\t\tqueue:RemoveAsync(readId)\n\tend)\nend\n\nreturn { enqueue = enqueue, matchmakeOnce = matchmakeOnce }\n```\n\nFor many game modes and skill-based matching, prefer Roblox's built-in **matchmaking** (configurable\nscoring for which server a player joins) where it fits, and see `roblox-teleport-matchmaking`.\n\n## Live leaderboard / server list (MemoryStoreSortedMap)\n\n```luau\n--!strict\nlocal MemoryStoreService = game:GetService(\"MemoryStoreService\")\n\nlocal servers = MemoryStoreService:GetSortedMap(\"ActiveServers\")\n\nlocal function heartbeat(playerCount: number)\n\tpcall(function()\n\t\t-- value, expiration (s), sort key: sorts by player count\n\t\tservers:SetAsync(game.JobId, { players = playerCount, placeId = game.PlaceId }, 60, playerCount)\n\tend)\nend\n\nlocal function topServers(): { any }\n\tlocal ok, entries = pcall(function()\n\t\treturn servers:GetRangeAsync(Enum.SortDirection.Descending, 20)\n\tend)\n\treturn if ok then entries else {}\nend\n\nreturn { heartbeat = heartbeat, topServers = topServers }\n```\n\nEntries are `{ key, value, sortKey }`. Refresh with a TTL shorter than the heartbeat interval × 2 so\ndead servers disappear on their own.\n\n## Atomic counters (MemoryStoreHashMap)\n\n```luau\n--!strict\nlocal MemoryStoreService = game:GetService(\"MemoryStoreService\")\n\nlocal counters = MemoryStoreService:GetHashMap(\"EventCounters\")\n\nlocal function increment(name: string, by: number): number?\n\tlocal ok, result = pcall(function()\n\t\treturn counters:UpdateAsync(name, function(old: number?)\n\t\t\treturn (old or 0) + by\n\t\tend, 86400)\n\tend)\n\treturn if ok then result else nil\nend\n\nreturn increment\n```\n\n## Cross-server announcements (MessagingService)\n\n```luau\n--!strict\nlocal MessagingService = game:GetService(\"MessagingService\")\nlocal Players = game:GetService(\"Players\")\n\nlocal TOPIC = \"Announcements\"\n\nlocal function publish(text: string)\n\tlocal ok, err = pcall(function()\n\t\tMessagingService:PublishAsync(TOPIC, { text = text, from = game.JobId }) -- ≤ 1 kB\n\tend)\n\tif not ok then\n\t\twarn(\"publish failed:\", err)\n\tend\nend\n\ntask.spawn(function()\n\tlocal ok, err = pcall(function()\n\t\tMessagingService:SubscribeAsync(TOPIC, function(message)\n\t\t\tlocal payload = message.Data -- message.Sent is the publish timestamp\n\t\t\tif typeof(payload) == \"table\" and typeof(payload.text) == \"string\" then\n\t\t\t\tfor _, player in Players:GetPlayers() do\n\t\t\t\t\tprint(`announce to {player.Name}: {payload.text}`)\n\t\t\t\tend\n\t\t\tend\n\t\tend)\n\tend)\n\tif not ok then\n\t\twarn(\"subscribe failed:\", err)\n\tend\nend)\n\nreturn publish\n```\n\n- Any text shown to players that originated from a player must be filtered (`roblox-text-chat`).\n- Messages can be missed (server starting, throttling). For state that must converge, store it in a\n DataStore/MemoryStore and use messages only as a \"go re-read\" hint.\n\n## Data store vs memory store\n\n| | DataStore | MemoryStore |\n| --- | --- | --- |\n| Durability | Persistent, versioned | Expires (≤ 45 days); evicted at quota |\n| Latency | Higher | Low |\n| Use | Player progress, purchases, anything valuable | Queues, live rankings, sessions, locks, caches |\n",
17
+ "skills/roblox-data-stores/references/raw-datastore.md": "# Player data with raw DataStoreService (session-locked)\n\nUse this when a project can't use ProfileStore. It shows every moving part: session lock, retries,\nautosave with jitter, and shutdown flushing. Keep it in `ServerScriptService`.\n\n```luau\n--!strict\nlocal DataStoreService = game:GetService(\"DataStoreService\")\nlocal HttpService = game:GetService(\"HttpService\")\nlocal Players = game:GetService(\"Players\")\nlocal RunService = game:GetService(\"RunService\")\n\nlocal STORE = DataStoreService:GetDataStore(\"PlayerData\")\nlocal AUTOSAVE_INTERVAL = 180\nlocal LOCK_EXPIRY = 600 -- a lock older than this is considered abandoned (crashed server)\nlocal SERVER_ID = if RunService:IsStudio() then \"studio-\" .. HttpService:GenerateGUID(false) else game.JobId\n\ntype PlayerData = { version: number, coins: number, inventory: { [string]: number } }\ntype Record = { data: PlayerData, lock: { server: string, time: number }? }\n\nlocal function defaultData(): PlayerData\n\treturn { version = 1, coins = 0, inventory = {} }\nend\n\nlocal sessions: { [Player]: PlayerData } = {}\n\nlocal function withRetries<T>(label: string, fn: () -> T): (boolean, T?)\n\tlocal waitTime = 1\n\tfor attempt = 1, 5 do\n\t\tlocal ok, result = pcall(fn)\n\t\tif ok then\n\t\t\treturn true, result\n\t\tend\n\t\twarn(`{label} failed (attempt {attempt}): {result}`)\n\t\ttask.wait(waitTime + math.random())\n\t\twaitTime = math.min(waitTime * 2, 16)\n\tend\n\treturn false, nil\nend\n\nlocal function keyFor(player: Player): string\n\treturn `User_{player.UserId}`\nend\n\n-- Acquire the session lock and return the data, or nil if another live server holds it.\nlocal function load(player: Player): PlayerData?\n\tlocal loaded: PlayerData? = nil\n\tlocal ok = withRetries(\"load\", function()\n\t\tSTORE:UpdateAsync(keyFor(player), function(record: Record?): Record?\n\t\t\tlocal now = os.time()\n\t\t\tlocal current: Record = record or { data = defaultData(), lock = nil }\n\t\t\tlocal lock = current.lock\n\t\t\tif lock and lock.server ~= SERVER_ID and now - lock.time < LOCK_EXPIRY then\n\t\t\t\tloaded = nil\n\t\t\t\treturn nil -- cancel the write: someone else owns this session\n\t\t\tend\n\t\t\tcurrent.lock = { server = SERVER_ID, time = now }\n\t\t\tloaded = current.data\n\t\t\treturn current\n\t\tend)\n\t\treturn true\n\tend)\n\treturn if ok then loaded else nil\nend\n\nlocal function save(player: Player, release: boolean)\n\tlocal data = sessions[player]\n\tif not data then\n\t\treturn\n\tend\n\twithRetries(\"save\", function()\n\t\tSTORE:UpdateAsync(keyFor(player), function(record: Record?): (Record?, { number }?)\n\t\t\tif record and record.lock and record.lock.server ~= SERVER_ID then\n\t\t\t\treturn nil, nil -- we lost the lock; never overwrite another server's session\n\t\t\tend\n\t\t\tlocal newRecord: Record = {\n\t\t\t\tdata = data,\n\t\t\t\tlock = if release then nil else { server = SERVER_ID, time = os.time() },\n\t\t\t}\n\t\t\treturn newRecord, { player.UserId } -- user IDs link the key for right-to-be-forgotten\n\t\tend)\n\t\treturn true\n\tend)\nend\n\nlocal function onPlayerAdded(player: Player)\n\tlocal data = nil\n\tfor _ = 1, 3 do -- the previous server may still be releasing the lock after a teleport\n\t\tdata = load(player)\n\t\tif data or player.Parent ~= Players then\n\t\t\tbreak\n\t\tend\n\t\ttask.wait(5)\n\tend\n\tif player.Parent ~= Players then\n\t\treturn\n\tend\n\tif not data then\n\t\tplayer:Kick(\"Your data is still in use on another server. Please rejoin shortly.\")\n\t\treturn\n\tend\n\tsessions[player] = data\nend\n\nPlayers.PlayerAdded:Connect(onPlayerAdded)\nfor _, player in Players:GetPlayers() do\n\ttask.spawn(onPlayerAdded, player)\nend\n\nPlayers.PlayerRemoving:Connect(function(player)\n\tsave(player, true)\n\tsessions[player] = nil\nend)\n\ntask.spawn(function()\n\ttask.wait(math.random() * AUTOSAVE_INTERVAL) -- stagger servers\n\twhile true do\n\t\tfor player in sessions do\n\t\t\ttask.spawn(save, player, false)\n\t\tend\n\t\ttask.wait(AUTOSAVE_INTERVAL)\n\tend\nend)\n\ngame:BindToClose(function()\n\tif RunService:IsStudio() then\n\t\ttask.wait(1) -- let PlayerRemoving saves start in Studio\n\tend\n\tlocal pending = 0\n\tfor player in sessions do\n\t\tpending += 1\n\t\ttask.spawn(function()\n\t\t\tsave(player, true)\n\t\t\tpending -= 1\n\t\tend)\n\tend\n\twhile pending > 0 do\n\t\ttask.wait()\n\tend\nend)\n```\n\n## Notes\n\n- `UpdateAsync`'s transform can run more than once (on conflicts); keep it side-effect free apart from\n capturing the result as above.\n- `BindToClose` has about 30 seconds in total. Save all players in parallel, not sequentially.\n- A kicked or crashed server leaves a stale lock; `LOCK_EXPIRY` recovers after 10 minutes. ProfileStore\n resolves conflicts faster by messaging the lock holder.\n- In Studio, use a separate data store name (e.g. `PlayerData_Studio`) or a separate test universe.\n- Log failures to analytics; if a save ultimately fails on leave, the next server loads the last\n good version — never write partial or default data over a failed load.\n",
18
+ "skills/roblox-game-systems/SKILL.md": "---\nname: roblox-game-systems\ndescription: Recipes for common systems built on server authority - currency service and leaderstats, XP/levels, round loops with intermission and teams, validated melee/ranged combat, inventory and tools, soft-currency shops, obby checkpoints, daily rewards, pets. Use when implementing or reviewing gameplay systems such as rounds, combat, weapons, inventories, shops, currencies, or progression.\n---\n\n# Game systems recipes\n\nEvery recipe follows the same contract:\n1. **State lives on the server** in a service module; persistent parts go through the data layer\n (`roblox-data-stores`).\n2. **Clients send intents** through validated, rate-limited remotes (`roblox-security`,\n `roblox-networking`) and render state they receive (attributes, snapshots/deltas).\n3. **Config is data**: tunables in a shared/server config module (or live `ConfigService` configs).\n4. **Lifecycle-safe**: handles late joiners, respawns, leaving mid-action, and server shutdown.\n\n| System | Reference |\n| --- | --- |\n| Leaderstats, currency service, XP/levels, daily rewards, obby checkpoints | [references/progression.md](references/progression.md) |\n| Round loop (intermission → match → results), teams, spawning | [references/round-system.md](references/round-system.md) |\n| Melee and ranged combat with server validation, cooldowns, damage attribution | [references/combat.md](references/combat.md) |\n| Inventory, equipping tools, shop purchases (soft currency), pets that follow | [references/inventory-shop.md](references/inventory-shop.md) |\n\n## Currency service (the pattern every economy uses)\n\n```luau\n--!strict\n-- ServerScriptService/Server/Services/CurrencyService.luau\nlocal Players = game:GetService(\"Players\")\n\ntype Balances = { [Player]: number }\n\nlocal MAX_BALANCE = 1e12\n\nlocal CurrencyService = {}\nlocal balances: Balances = {}\n\nlocal function publish(player: Player)\n\tplayer:SetAttribute(\"Coins\", balances[player]) -- replicated to every client; UI listens\nend\n\nfunction CurrencyService.load(player: Player, saved: number?)\n\tbalances[player] = math.clamp(saved or 0, 0, MAX_BALANCE)\n\tpublish(player)\nend\n\nfunction CurrencyService.get(player: Player): number\n\treturn balances[player] or 0\nend\n\n-- Positive amount only; returns the new balance.\nfunction CurrencyService.add(player: Player, amount: number, _source: string): number?\n\tlocal balance = balances[player]\n\tif balance == nil or not math.isfinite(amount) or amount <= 0 then\n\t\treturn nil\n\tend\n\tbalances[player] = math.min(balance + math.floor(amount), MAX_BALANCE)\n\tpublish(player)\n\treturn balances[player]\nend\n\n-- Atomic check-and-spend; false if insufficient.\nfunction CurrencyService.spend(player: Player, amount: number, _sink: string): boolean\n\tlocal balance = balances[player]\n\tif balance == nil or not math.isfinite(amount) or amount <= 0 or balance < amount then\n\t\treturn false\n\tend\n\tbalances[player] = balance - math.floor(amount)\n\tpublish(player)\n\treturn true\nend\n\nPlayers.PlayerRemoving:Connect(function(player)\n\tbalances[player] = nil -- after the data layer has saved it\nend)\n\nreturn CurrencyService\n```\n\n- One module owns each resource; nothing else writes it. Log sources/sinks to analytics\n (`roblox-analytics-liveops`).\n- The in-memory balance is saved by the data layer (ProfileStore `profile.Data.coins`) — or make the\n profile table the balance store directly.\n- For display, clients read `player:GetAttribute(\"Coins\")` and `GetAttributeChangedSignal(\"Coins\")`.\n `leaderstats` (a Folder with `IntValue`s under the player) shows values in the player list.\n\n## Design checklist for any new system\n\n- [ ] Where does authoritative state live, and how is it persisted?\n- [ ] What can the client request, and how is each request validated and rate-limited?\n- [ ] What happens on join mid-session, respawn, leave, teleport, and server shutdown?\n- [ ] How is it tuned (config) and measured (analytics)?\n- [ ] How does it behave with 1 player, full servers, and high latency?\n- [ ] Is it streaming-safe on the client?\n\n## Related skills\n\n`roblox-architecture`, `roblox-security`, `roblox-data-stores`, `roblox-networking`,\n`roblox-characters-animation`, `roblox-monetization`, `roblox-ui`.\n",
19
+ "skills/roblox-game-systems/references/combat.md": "# Server-validated combat\n\nClients predict (play the swing/shot instantly) and send **intent**; the server owns cooldowns, hit\ndetection or hit validation, and damage.\n\n## Weapon config (shared)\n\n```luau\n--!strict\n-- ReplicatedStorage/Shared/WeaponConfig.luau\nexport type Weapon = {\n\tkind: \"Melee\" | \"Ranged\",\n\tdamage: number,\n\tcooldown: number,\n\trange: number,\n\thitboxSize: Vector3?, -- melee\n}\n\nreturn table.freeze({\n\tSword = { kind = \"Melee\", damage = 25, cooldown = 0.6, range = 7, hitboxSize = Vector3.new(6, 5, 7) },\n\tBlaster = { kind = \"Ranged\", damage = 18, cooldown = 0.25, range = 300 },\n} :: { [string]: Weapon })\n```\n\n## Server combat service\n\n```luau\n--!strict\nlocal Players = game:GetService(\"Players\")\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal WeaponConfig = require(ReplicatedStorage.Shared.WeaponConfig)\n\nlocal attackRemote = Instance.new(\"RemoteEvent\")\nattackRemote.Name = \"Attack\"\nattackRemote.Parent = ReplicatedStorage\n\nlocal lastAttack: { [Player]: number } = {}\nlocal equipped: { [Player]: string } = {} -- set by the inventory/tool system, never by the client\n\nlocal overlap = OverlapParams.new()\noverlap.FilterType = Enum.RaycastFilterType.Exclude\nlocal rayParams = RaycastParams.new()\nrayParams.FilterType = Enum.RaycastFilterType.Exclude\n\nlocal function rootOf(player: Player): BasePart?\n\tlocal character = player.Character\n\tlocal humanoid = character and character:FindFirstChildOfClass(\"Humanoid\")\n\tif not humanoid or humanoid.Health <= 0 then\n\t\treturn nil\n\tend\n\treturn humanoid.RootPart\nend\n\nlocal function damageModel(model: Model, attacker: Player, amount: number)\n\tlocal humanoid = model:FindFirstChildOfClass(\"Humanoid\")\n\tlocal victim = Players:GetPlayerFromCharacter(model)\n\tif not humanoid or humanoid.Health <= 0 or victim == attacker then\n\t\treturn\n\tend\n\tif victim and victim.Team and victim.Team == attacker.Team and not victim.Neutral then\n\t\treturn -- no friendly fire\n\tend\n\thumanoid:SetAttribute(\"LastAttacker\", attacker.UserId)\n\thumanoid:TakeDamage(amount)\nend\n\nlocal function meleeAttack(player: Player, root: BasePart, damage: number, range: number, hitboxSize: Vector3)\n\t-- Server-side hitbox in front of the attacker (uses server positions: no client-reported targets).\n\tlocal boxCFrame = root.CFrame * CFrame.new(0, 0, -range / 2)\n\toverlap.FilterDescendantsInstances = { player.Character :: Model }\n\tlocal hitModels: { [Model]: boolean } = {}\n\tfor _, part in workspace:GetPartBoundsInBox(boxCFrame, hitboxSize, overlap) do\n\t\tlocal model = part:FindFirstAncestorOfClass(\"Model\")\n\t\tif model and not hitModels[model] then\n\t\t\thitModels[model] = true\n\t\t\tdamageModel(model, player, damage)\n\t\tend\n\tend\nend\n\nlocal function rangedAttack(player: Player, root: BasePart, damage: number, range: number, direction: Vector3)\n\t-- Client sends only an aim direction; the server casts from its own muzzle position.\n\tlocal origin = root.Position + Vector3.new(0, 1.5, 0)\n\tlocal offset = direction.Unit * range\n\trayParams.FilterDescendantsInstances = { player.Character :: Model }\n\tlocal result = workspace:Raycast(origin, offset, rayParams)\n\tif result then\n\t\tlocal model = result.Instance:FindFirstAncestorOfClass(\"Model\")\n\t\tif model then\n\t\t\tdamageModel(model, player, damage)\n\t\tend\n\tend\n\tattackRemote:FireAllClients(player, origin, if result then result.Position else origin + offset)\nend\n\nattackRemote.OnServerEvent:Connect(function(player: Player, direction: unknown)\n\tlocal weaponName = equipped[player]\n\tlocal weapon = weaponName and WeaponConfig[weaponName]\n\tlocal root = rootOf(player)\n\tif not weapon or not root then\n\t\treturn\n\tend\n\tlocal now = os.clock()\n\tif now - (lastAttack[player] or 0) < weapon.cooldown * 0.9 then -- small tolerance for jitter\n\t\treturn\n\tend\n\tlastAttack[player] = now\n\n\tif weapon.kind == \"Melee\" then\n\t\tmeleeAttack(player, root, weapon.damage, weapon.range, weapon.hitboxSize or Vector3.one * 5)\n\telseif\n\t\ttypeof(direction) == \"Vector3\"\n\t\tand direction.Magnitude > 0.01\n\t\tand direction.Magnitude == direction.Magnitude\n\tthen\n\t\trangedAttack(player, root, weapon.damage, weapon.range, direction) -- Magnitude == Magnitude rejects NaN\n\tend\nend)\n\nPlayers.PlayerRemoving:Connect(function(player)\n\tlastAttack[player] = nil\n\tequipped[player] = nil\nend)\n\nreturn { equipped = equipped }\n```\n\n## Lag compensation (fast shooters)\n\nServer-side raycasts against **current** server positions miss targets the shooter saw earlier.\nOptions:\n- **Hit claims + validation**: client reports `(targetCharacter, hitPosition, clientTime)`; server checks\n the target was within tolerance of that position recently (keep a short position history per\n character, ~1 s, sampled on Heartbeat, rewound by `player:GetNetworkPing()`), line of sight against\n static geometry, range, and fire rate.\n- **Server authority** mode handles prediction/rollback for you (`roblox-physics`).\n\n## Client side\n\n- On input: play the animation/sound/muzzle flash immediately, then `Attack:FireServer(aimDirection)`.\n- On `Attack.OnClientEvent(shooter, from, to)`: draw tracers for other players' shots (skip your own if\n already predicted).\n- Health bars read `Humanoid.Health`; damage numbers from a server event carrying `(target, amount)`.\n- Use `GetMarkerReachedSignal` on swing animations to time client VFX; the server uses its own timing.\n",
20
+ "skills/roblox-game-systems/references/inventory-shop.md": "# Inventory, equipment, shop, pets\n\n## Item catalog (server-authoritative data)\n\n```luau\n--!strict\n-- ReplicatedStorage/Shared/Items.luau — shared so clients can render names/icons/prices.\n-- The server still re-reads prices from here; never from client messages.\nexport type Item = {\n\tid: string,\n\tname: string,\n\tprice: number?, -- nil = not sold in the shop\n\tmaxStack: number,\n\ttoolTemplate: string?, -- name of a Tool in ServerStorage.Tools\n}\n\nlocal Items: { [string]: Item } = {\n\tSword = { id = \"Sword\", name = \"Iron Sword\", price = 100, maxStack = 1, toolTemplate = \"Sword\" },\n\tPotion = { id = \"Potion\", name = \"Health Potion\", price = 25, maxStack = 20 },\n}\n\nreturn table.freeze(Items)\n```\n\n## Inventory service (server)\n\n```luau\n--!strict\nlocal Players = game:GetService(\"Players\")\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal ServerStorage = game:GetService(\"ServerStorage\")\n\nlocal Items = require(ReplicatedStorage.Shared.Items)\n\ntype Inventory = { [string]: number }\n\nlocal inventories: { [Player]: Inventory } = {} -- point this at profile.Data.inventory on load\nlocal changed = Instance.new(\"RemoteEvent\")\nchanged.Name = \"InventoryChanged\"\nchanged.Parent = ReplicatedStorage\n\nlocal InventoryService = {}\n\nfunction InventoryService.load(player: Player, saved: Inventory)\n\tinventories[player] = saved\n\tchanged:FireClient(player, \"snapshot\", saved)\nend\n\nfunction InventoryService.count(player: Player, itemId: string): number\n\tlocal inventory = inventories[player]\n\treturn inventory and inventory[itemId] or 0\nend\n\nfunction InventoryService.add(player: Player, itemId: string, amount: number): boolean\n\tlocal inventory = inventories[player]\n\tlocal item = Items[itemId]\n\tif not inventory or not item or amount <= 0 then\n\t\treturn false\n\tend\n\tlocal current = inventory[itemId] or 0\n\tif current + amount > item.maxStack then\n\t\treturn false\n\tend\n\tinventory[itemId] = current + amount\n\tchanged:FireClient(player, \"delta\", itemId, inventory[itemId])\n\treturn true\nend\n\nfunction InventoryService.remove(player: Player, itemId: string, amount: number): boolean\n\tlocal inventory = inventories[player]\n\tlocal current = inventory and inventory[itemId] or 0\n\tif not inventory or amount <= 0 or current < amount then\n\t\treturn false\n\tend\n\tlocal remaining = current - amount\n\tinventory[itemId] = if remaining > 0 then remaining else nil\n\tchanged:FireClient(player, \"delta\", itemId, remaining)\n\treturn true\nend\n\n-- Equip = clone the Tool template into the backpack; the tool's behavior lives in a tagged service.\nfunction InventoryService.equip(player: Player, itemId: string): boolean\n\tlocal item = Items[itemId]\n\tlocal backpack = player:FindFirstChildOfClass(\"Backpack\")\n\tif not item or not item.toolTemplate or not backpack or InventoryService.count(player, itemId) < 1 then\n\t\treturn false\n\tend\n\tlocal template = ServerStorage:WaitForChild(\"Tools\"):FindFirstChild(item.toolTemplate)\n\tif not template or backpack:FindFirstChild(template.Name) then\n\t\treturn false\n\tend\n\ttemplate:Clone().Parent = backpack\n\treturn true\nend\n\nPlayers.PlayerRemoving:Connect(function(player)\n\tinventories[player] = nil\nend)\n\nreturn InventoryService\n```\n\nTools in the backpack are lost on respawn unless re-granted: re-equip owned items on `CharacterAdded`\n(or clone into `player.StarterGear` so Roblox restores them).\n\n## Shop purchase with soft currency (server)\n\n```luau\n--!strict\nlocal Players = game:GetService(\"Players\")\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal ServerScriptService = game:GetService(\"ServerScriptService\")\n\nlocal Items = require(ReplicatedStorage.Shared.Items)\nlocal CurrencyService = require(ServerScriptService.Server.Services.CurrencyService)\nlocal InventoryService = require(ServerScriptService.Server.Services.InventoryService)\n\nlocal buy = Instance.new(\"RemoteFunction\")\nbuy.Name = \"BuyItem\"\nbuy.Parent = ReplicatedStorage\n\nlocal lastBuy: { [Player]: number } = {}\n\nbuy.OnServerInvoke = function(player: Player, itemId: unknown): (boolean, string)\n\tlocal now = os.clock()\n\tif now - (lastBuy[player] or 0) < 0.3 then\n\t\treturn false, \"Too fast\"\n\tend\n\tlastBuy[player] = now\n\n\tif typeof(itemId) ~= \"string\" then\n\t\treturn false, \"Invalid item\"\n\tend\n\tlocal item = Items[itemId]\n\tif not item or not item.price then\n\t\treturn false, \"Not for sale\"\n\tend\n\tif InventoryService.count(player, itemId) >= item.maxStack then\n\t\treturn false, \"Inventory full\"\n\tend\n\tif not CurrencyService.spend(player, item.price, `Shop:{itemId}`) then\n\t\treturn false, \"Not enough coins\"\n\tend\n\tif not InventoryService.add(player, itemId, 1) then\n\t\tCurrencyService.add(player, item.price, \"Refund\") -- keep the operation atomic\n\t\treturn false, \"Could not add item\"\n\tend\n\treturn true, \"Purchased\"\nend\n\nPlayers.PlayerRemoving:Connect(function(player)\n\tlastBuy[player] = nil\nend)\n```\n\nRobux purchases go through developer products and `ProcessReceipt` instead (`roblox-monetization`).\n\n## Pets that follow the player\n\n- Server spawns the pet model (cosmetic ownership is server-side data), sets\n `pet.PrimaryPart:SetNetworkOwner(player)` so the owner's client moves it smoothly, and replicates\n `PetOwner` as an attribute.\n- The owner's client moves the pet each frame toward an offset behind the character with\n `AlignPosition`/`AlignOrientation` (see `roblox-physics` recipes) — or all clients animate every pet\n locally from the owner's position (no physics, cheapest for many pets).\n- Hatching/random pets bought with Robux or Robux-bought currency are **paid random items**: disclose\n odds and respect `PolicyService` restrictions (`roblox-monetization`).\n",
21
+ "skills/roblox-game-systems/references/progression.md": "# Progression recipes\n\n## leaderstats (player list display)\n\n```luau\n--!strict\nlocal Players = game:GetService(\"Players\")\n\nlocal function setupLeaderstats(player: Player)\n\tlocal leaderstats = Instance.new(\"Folder\")\n\tleaderstats.Name = \"leaderstats\" -- exact lowercase name is required\n\n\tlocal coins = Instance.new(\"IntValue\")\n\tcoins.Name = \"Coins\"\n\tlocal function sync()\n\t\tlocal value = player:GetAttribute(\"Coins\")\n\t\tcoins.Value = if typeof(value) == \"number\" then value else 0\n\tend\n\tsync()\n\tcoins.Parent = leaderstats\n\tplayer:GetAttributeChangedSignal(\"Coins\"):Connect(sync)\n\n\tleaderstats.Parent = player\nend\n\nPlayers.PlayerAdded:Connect(setupLeaderstats)\nfor _, player in Players:GetPlayers() do\n\ttask.spawn(setupLeaderstats, player)\nend\n```\n\n`leaderstats` values are display only — the source of truth stays in the service/data layer.\n\n## XP and levels\n\n```luau\n--!strict\nlocal Levels = {}\n\n-- XP needed to go from `level` to `level + 1`: gentle exponential curve.\nfunction Levels.xpForLevel(level: number): number\n\treturn math.floor(100 * level ^ 1.5)\nend\n\n-- Adds XP and returns the new (level, xp, levelsGained). Pure: easy to unit test.\nfunction Levels.addXp(level: number, xp: number, amount: number, maxLevel: number): (number, number, number)\n\tlocal gained = 0\n\txp += amount\n\twhile level < maxLevel and xp >= Levels.xpForLevel(level) do\n\t\txp -= Levels.xpForLevel(level)\n\t\tlevel += 1\n\t\tgained += 1\n\tend\n\tif level >= maxLevel then\n\t\txp = 0\n\tend\n\treturn level, xp, gained\nend\n\nprint(Levels.addXp(1, 0, 450, 100))\nreturn Levels\n```\n\n## Daily rewards with streaks (server)\n\n```luau\n--!strict\ntype RewardState = { lastClaimDay: number?, streak: number }\n\nlocal REWARDS = { 50, 75, 100, 150, 200, 300, 500 } -- day 1..7, then repeat day 7\n\nlocal function utcDay(): number\n\treturn os.time() // 86400\nend\n\n-- Returns (granted amount or nil, reason). Mutates `state`.\nlocal function claimDaily(state: RewardState): (number?, string)\n\tlocal today = utcDay()\n\tif state.lastClaimDay == today then\n\t\treturn nil, \"already claimed\"\n\tend\n\tif state.lastClaimDay == today - 1 then\n\t\tstate.streak += 1\n\telse\n\t\tstate.streak = 1 -- missed a day (or first claim)\n\tend\n\tstate.lastClaimDay = today\n\treturn REWARDS[math.min(state.streak, #REWARDS)], \"ok\"\nend\n\nlocal state: RewardState = { streak = 0 }\nprint(claimDaily(state))\nreturn claimDaily\n```\n\nStore `RewardState` in the player's profile; call from a rate-limited remote; save immediately after.\nShow the next claim time using `(utcDay() + 1) * 86400` and `workspace:GetServerTimeNow()` on the client.\n\n## Obby checkpoints (server-validated)\n\n```luau\n--!strict\nlocal CollectionService = game:GetService(\"CollectionService\")\nlocal Players = game:GetService(\"Players\")\n\nlocal stageOf: { [Player]: number } = {}\n\nlocal function onCheckpoint(checkpoint: BasePart)\n\tlocal stage = checkpoint:GetAttribute(\"Stage\")\n\tif typeof(stage) ~= \"number\" then\n\t\treturn\n\tend\n\tcheckpoint.Touched:Connect(function(hit)\n\t\tlocal character = hit:FindFirstAncestorOfClass(\"Model\")\n\t\tlocal player = character and Players:GetPlayerFromCharacter(character)\n\t\tif not player then\n\t\t\treturn\n\t\tend\n\t\tlocal current = stageOf[player] or 0\n\t\tif stage == current + 1 then -- only the next stage counts (no skipping ahead)\n\t\t\tstageOf[player] = stage\n\t\t\tplayer:SetAttribute(\"Stage\", stage)\n\t\tend\n\tend)\nend\n\nfor _, checkpoint in CollectionService:GetTagged(\"Checkpoint\") do\n\tif checkpoint:IsA(\"BasePart\") then\n\t\tonCheckpoint(checkpoint)\n\tend\nend\n\nPlayers.PlayerAdded:Connect(function(player)\n\tplayer.CharacterAdded:Connect(function(character)\n\t\tlocal stage = stageOf[player] or 0\n\t\tfor _, checkpoint in CollectionService:GetTagged(\"Checkpoint\") do\n\t\t\tif checkpoint:IsA(\"BasePart\") and checkpoint:GetAttribute(\"Stage\") == stage then\n\t\t\t\ttask.defer(function()\n\t\t\t\t\tcharacter:PivotTo(checkpoint.CFrame + Vector3.new(0, 4, 0))\n\t\t\t\tend)\n\t\t\tend\n\t\tend\n\tend)\nend)\n\nPlayers.PlayerRemoving:Connect(function(player)\n\tstageOf[player] = nil\nend)\n```\n\nAdd timing heuristics (minimum plausible time per stage) to flag teleport exploits (`roblox-security`).\n",
22
+ "skills/roblox-game-systems/references/round-system.md": "# Round-based game loop\n\nState machine on the server; clients render from replicated attributes on a shared instance.\nTimers use absolute end times so clients count down locally without per-second remotes.\n\n```luau\n--!strict\n-- ServerScriptService/Server/Services/RoundService.luau\nlocal Players = game:GetService(\"Players\")\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal ServerStorage = game:GetService(\"ServerStorage\")\nlocal Teams = game:GetService(\"Teams\")\n\ntype Phase = \"Waiting\" | \"Intermission\" | \"Playing\" | \"Results\"\n\nlocal MIN_PLAYERS = 2\nlocal INTERMISSION = 15\nlocal ROUND_LENGTH = 180\nlocal RESULTS = 8\n\n-- Shared state instance: clients read attributes (Phase, PhaseEndsAt, Map, Winner).\nlocal state = Instance.new(\"Folder\")\nstate.Name = \"RoundState\"\nstate.Parent = ReplicatedStorage\n\nlocal maps = ServerStorage:WaitForChild(\"Maps\")\nlocal lobbySpawn = workspace:WaitForChild(\"LobbySpawn\") :: BasePart\n\nlocal participants: { [Player]: boolean } = {}\nlocal currentMap: Model? = nil\n\nlocal function setPhase(phase: Phase, duration: number?)\n\tstate:SetAttribute(\"Phase\", phase)\n\tstate:SetAttribute(\"PhaseEndsAt\", if duration then workspace:GetServerTimeNow() + duration else nil)\nend\n\nlocal function aliveParticipants(): { Player }\n\tlocal alive = {}\n\tfor player in participants do\n\t\tlocal character = player.Character\n\t\tlocal humanoid = character and character:FindFirstChildOfClass(\"Humanoid\")\n\t\tif player.Parent == Players and humanoid and humanoid.Health > 0 then\n\t\t\ttable.insert(alive, player)\n\t\tend\n\tend\n\treturn alive\nend\n\nlocal function loadMap(): Model\n\tlocal choices = maps:GetChildren()\n\tlocal template = choices[math.random(1, #choices)] :: Model\n\tlocal map = template:Clone()\n\tmap.Parent = workspace\n\tstate:SetAttribute(\"Map\", template.Name)\n\treturn map\nend\n\nlocal function teleportTo(player: Player, target: CFrame)\n\tlocal character = player.Character\n\tif character then\n\t\tcharacter:PivotTo(target + Vector3.new(0, 4, 0))\n\tend\nend\n\nlocal function startRound()\n\tcurrentMap = loadMap()\n\tlocal spawns = (currentMap :: Model):WaitForChild(\"Spawns\"):GetChildren()\n\tlocal red, blue = Teams:FindFirstChild(\"Red\") :: Team?, Teams:FindFirstChild(\"Blue\") :: Team?\n\tfor index, player in Players:GetPlayers() do\n\t\tparticipants[player] = true\n\t\tif red and blue then\n\t\t\tplayer.Team = if index % 2 == 0 then red else blue\n\t\tend\n\t\tlocal spawnPart = spawns[(index - 1) % #spawns + 1] :: BasePart\n\t\tteleportTo(player, spawnPart.CFrame)\n\tend\nend\n\nlocal function endRound(winner: string)\n\tstate:SetAttribute(\"Winner\", winner)\n\tfor player in participants do\n\t\tteleportTo(player, lobbySpawn.CFrame)\n\t\tplayer.Neutral = true\n\tend\n\ttable.clear(participants)\n\tif currentMap then\n\t\tcurrentMap:Destroy()\n\t\tcurrentMap = nil\n\tend\nend\n\nPlayers.PlayerRemoving:Connect(function(player)\n\tparticipants[player] = nil -- leaving mid-round must not break win checks\nend)\n\nlocal function run()\n\twhile true do\n\t\tif #Players:GetPlayers() < MIN_PLAYERS then\n\t\t\tsetPhase(\"Waiting\")\n\t\t\trepeat\n\t\t\t\ttask.wait(1)\n\t\t\tuntil #Players:GetPlayers() >= MIN_PLAYERS\n\t\tend\n\n\t\tsetPhase(\"Intermission\", INTERMISSION)\n\t\ttask.wait(INTERMISSION)\n\t\tif #Players:GetPlayers() < MIN_PLAYERS then\n\t\t\tcontinue\n\t\tend\n\n\t\tstartRound()\n\t\tsetPhase(\"Playing\", ROUND_LENGTH)\n\t\tlocal deadline = os.clock() + ROUND_LENGTH\n\t\tlocal winner = \"Nobody\"\n\t\twhile os.clock() < deadline do\n\t\t\tlocal alive = aliveParticipants()\n\t\t\tif #alive <= 1 then\n\t\t\t\twinner = if alive[1] then alive[1].Name else \"Nobody\"\n\t\t\t\tbreak\n\t\t\tend\n\t\t\ttask.wait(0.5)\n\t\tend\n\n\t\tsetPhase(\"Results\", RESULTS)\n\t\tendRound(winner)\n\t\ttask.wait(RESULTS)\n\tend\nend\n\ntask.spawn(run)\nreturn {}\n```\n\n## Client countdown\n\n```luau\n--!strict\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal RunService = game:GetService(\"RunService\")\n\nlocal state = ReplicatedStorage:WaitForChild(\"RoundState\")\n\nRunService.Heartbeat:Connect(function()\n\tlocal endsAt = state:GetAttribute(\"PhaseEndsAt\")\n\tlocal phase = state:GetAttribute(\"Phase\")\n\tif typeof(endsAt) == \"number\" then\n\t\tlocal remaining = math.max(0, math.ceil(endsAt - workspace:GetServerTimeNow()))\n\t\t-- update a TextLabel only when `remaining` changes\n\t\tlocal _text = `{phase}: {remaining}s`\n\tend\nend)\n```\n\n## Variations\n\n- **Teams**: create `Team` objects in `Teams` (`AutoAssignable = false` when you assign manually); set\n `player.Team`; team spawns via `SpawnLocation.TeamColor`.\n- **Late joiners** during `Playing`: spectate (camera on a random alive player) until next round.\n- **Rewards**: grant through the currency service at `Results` for participants still in the server.\n- **Round per reserved server** (competitive): the lobby teleports a full match to a reserved server\n that runs exactly one round, then teleports everyone back (`roblox-teleport-matchmaking`).\n",
23
+ "skills/roblox-input/SKILL.md": "---\nname: roblox-input\ndescription: Player input and cameras across keyboard/mouse, touch, gamepad, VR - Input Action System (InputContext, InputAction, InputBinding), UserInputService (PreferredInput, gameProcessedEvent, mouse lock), ContextActionService, reserved keys, custom cameras with BindToRenderStep. Use when adding controls, keybinds, abilities, vehicles, on-screen buttons, or custom cameras.\n---\n\n# Roblox input and camera\n\n## Choose an approach\n\n| Approach | Use when |\n| --- | --- |\n| **Input Action System** (`InputContext` → `InputAction` → `InputBinding`) | New gameplay controls. Cross-device bindings configured as instances, contexts you enable/disable, built-in on-screen button binding, prompt display. Recommended. |\n| `UserInputService` events | Raw input, text-free hotkeys, mouse position/delta, touch gestures, device detection. |\n| `ContextActionService:BindAction` | Legacy/simple bind-with-mobile-button; existing code. |\n\nAll input handling is **client-side**. Send *intent* to the server via remotes; the server validates.\n\n## Input Action System\n\nStructure (typically in `ReplicatedStorage.Inputs`, or created from a client script):\n\n```text\nInputs (Folder)\n PlayContext (InputContext, Priority 1000) -- gameplay controls\n Sprint (InputAction, Type = Bool)\n Keyboard (InputBinding, KeyCode = LeftShift)\n Gamepad (InputBinding, KeyCode = ButtonL3)\n Touch (InputBinding, UIButton = <on-screen TextButton>)\n MenuContext (InputContext, Priority 2000, Sink = true, Enabled = false)\n```\n\n- `InputAction.Type`: `Bool` (press/release), `Direction1D` (triggers, zoom), `Direction2D` (move, camera),\n `Direction3D` (flying), `ViewportPosition` (pointer position).\n- Events: `Pressed`, `Released` (Bool only), `StateChanged(value)` for every type. `action:GetState()` reads the current value.\n- `InputBinding` fields: `KeyCode`; composite directions `Up/Down/Left/Right/Forward/Backward`; `Scale`\n (e.g. `0.01` for `MouseDelta`/`TouchDelta`); `PressedThreshold`/`ReleasedThreshold` for analog triggers;\n `PrimaryModifier`/`SecondaryModifier` for chords (Ctrl+S); `UIButton` to drive the action from a GUI button;\n `DisplayName`/`DisplayImage` for prompts.\n- `InputContext.Enabled` switches whole control schemes (gameplay ↔ menu ↔ vehicle); `Priority` + `Sink`\n lets a higher context consume keys so lower contexts don't also fire.\n- Set `Workspace.PlayerScriptsUseInputActionSystem = Enabled` to have Roblox's default player/camera\n scripts use the system too (required for server authority).\n\n```luau\n--!strict\n-- Client: build a Sprint action in code and react to it.\nlocal Players = game:GetService(\"Players\")\n\nlocal player = Players.LocalPlayer\n\nlocal context = Instance.new(\"InputContext\")\ncontext.Name = \"PlayContext\"\ncontext.Priority = 1000\n\nlocal sprint = Instance.new(\"InputAction\")\nsprint.Name = \"Sprint\"\nsprint.Type = Enum.InputActionType.Bool\nsprint.Parent = context\n\nlocal keyboard = Instance.new(\"InputBinding\")\nkeyboard.KeyCode = Enum.KeyCode.LeftShift\nkeyboard.Parent = sprint\n\nlocal gamepad = Instance.new(\"InputBinding\")\ngamepad.KeyCode = Enum.KeyCode.ButtonL3\ngamepad.Parent = sprint\n\ncontext.Parent = player:WaitForChild(\"PlayerScripts\")\n\nlocal function setSprinting(on: boolean)\n\tlocal character = player.Character\n\tlocal humanoid = character and character:FindFirstChildOfClass(\"Humanoid\")\n\tif humanoid then\n\t\thumanoid.WalkSpeed = if on then 24 else 16 -- server should validate/own real speed limits\n\tend\nend\n\nsprint.Pressed:Connect(function()\n\tsetSprinting(true)\nend)\nsprint.Released:Connect(function()\n\tsetSprinting(false)\nend)\n```\n\nShow the right key/button: an `InputActionLabel` (beta) auto-renders the binding for the current device,\nor read `action.PreferredBinding` (+ its `DisplayName`/`KeyCode`) and `UserInputService:GetStringForKeyCode`\n/ `GetImageForKeyCode`, updating on `GetPropertyChangedSignal(\"PreferredBinding\")`.\n\n## UserInputService essentials\n\n```luau\n--!strict\nlocal UserInputService = game:GetService(\"UserInputService\")\n\nUserInputService.InputBegan:Connect(function(input: InputObject, gameProcessedEvent: boolean)\n\tif gameProcessedEvent then\n\t\treturn -- typing in a TextBox, clicking UI, or a core-script action consumed it\n\tend\n\tif input.KeyCode == Enum.KeyCode.E then\n\t\tprint(\"interact\")\n\telseif input.UserInputType == Enum.UserInputType.MouseButton1 then\n\t\tprint(\"click at\", input.Position)\n\tend\nend)\n\nlocal function onPreferredInputChanged()\n\tlocal preferred = UserInputService.PreferredInput -- KeyboardAndMouse | Touch | Gamepad\n\tprint(\"show prompts for\", preferred)\nend\nUserInputService:GetPropertyChangedSignal(\"PreferredInput\"):Connect(onPreferredInputChanged)\nonPreferredInputChanged()\n```\n\n- **Always honor `gameProcessedEvent`** or players trigger abilities while typing in chat.\n- `PreferredInput` reflects what the player is actually using (phone + Bluetooth gamepad → `Gamepad`);\n don't branch on `TouchEnabled`/`KeyboardEnabled` alone.\n- Mouse: `UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter` for FPS (set every frame while\n in first person if other scripts may reset it), `GetMouseLocation()` (includes top-bar inset),\n `MouseDeltaSensitivity`, `MouseIconEnabled`.\n- Gamepad: `GetConnectedGamepads`, `GamepadConnected/Disconnected`, thumbstick `input.Position` in\n `InputChanged` (apply a dead zone ≈ 0.15). Haptics: `HapticService` / `HapticEffect`.\n- Touch: `TouchTap`, `TouchLongPress`, `TouchPinch`, `TouchSwipe`; hide default controls only if you\n replace them (`GuiService.TouchControlsEnabled`).\n- Reserved keys you can't override: Esc/Start (menu), F9 (console), F11, F12, PrintScreen; chat `/`,\n Tab (player list), backpack and tool-number keys unless you disable those core features.\n\n## ContextActionService (legacy-compatible)\n\n`ContextActionService:BindAction(name, handler, createTouchButton, ...inputs)` with handler\n`(actionName, inputState, inputObject) -> Enum.ContextActionResult?`; return `Pass` to let other\nbindings see the input. Use `BindActionAtPriority` for ordering and `UnbindAction` when the context\nends (e.g. tool unequipped). Prefer the Input Action System for new work.\n\n## Custom cameras\n\n```luau\n--!strict\nlocal Players = game:GetService(\"Players\")\nlocal RunService = game:GetService(\"RunService\")\n\nlocal camera = workspace.CurrentCamera\nlocal OFFSET = Vector3.new(0, 30, 20)\n\ncamera.CameraType = Enum.CameraType.Scriptable\n\nRunService:BindToRenderStep(\"TopDownCamera\", Enum.RenderPriority.Camera.Value + 1, function()\n\tlocal character = Players.LocalPlayer.Character\n\tlocal root = character and character:FindFirstChild(\"HumanoidRootPart\") :: BasePart?\n\tif root then\n\t\tlocal target = root.Position\n\t\tcamera.CFrame = CFrame.lookAt(target + OFFSET, target)\n\tend\nend)\n\n-- Restore: RunService:UnbindFromRenderStep(\"TopDownCamera\"); camera.CameraType = Enum.CameraType.Custom\n```\n\n- Update cameras in `BindToRenderStep` (runs before render, ordered), not `Heartbeat`/`task.wait` loops.\n- Smooth with `camera.CFrame:Lerp(goal, 1 - math.exp(-speed * deltaTime))` (frame-rate independent).\n- First person: `player.CameraMode = Enum.CameraMode.LockFirstPerson`; zoom limits with\n `player.CameraMinZoomDistance`/`CameraMaxZoomDistance`. Shake: offset the CFrame, don't move parts.\n- Screen → world: `camera:ViewportPointToRay(x, y)` / `ScreenPointToRay` + `workspace:Raycast`.\n\n## Cross-platform checklist\n\n- Every action has keyboard, gamepad, **and** touch bindings (touch via `UIButton` or on-screen buttons).\n- Prompts/hints update when `PreferredInput` changes. Console UI is navigable with D-pad and closes with B.\n- No hover-only interactions (touch has no hover). Hold-to-confirm for destructive actions.\n- Test in Studio's Device Emulator and Controller Emulator.\n\n## Related skills\n\n`roblox-ui` (buttons, gamepad selection), `roblox-characters-animation` (movement, abilities),\n`roblox-networking` (sending intents), `roblox-physics` (vehicles).\n",
24
+ "skills/roblox-luau/SKILL.md": "---\nname: roblox-luau\ndescription: Modern, type-safe Luau for Roblox, covering --!strict and the new type solver, generics, string interpolation, if-expressions, task library, require-by-string, module/OOP patterns, error handling with retries, buffer/vector, and legacy APIs to avoid. Use when writing, refactoring, or explaining any Roblox script, fixing type errors, or modernizing old Lua code.\n---\n\n# Modern Luau for Roblox\n\nLuau is Roblox's gradually-typed Lua 5.1 derivative. Engine and language move fast; many\npatterns from older tutorials (and older training data) are deprecated. Apply these defaults\nto **every** script you write unless the project clearly uses another convention.\n\n## Defaults for all generated code\n\n1. `--!strict` at the top of new ModuleScripts (and Scripts when practical). Type public functions.\n2. Get services with `game:GetService(\"Name\")` at the top of the file, never `game.Name`.\n3. Use the `task` library: `task.wait`, `task.spawn`, `task.defer`, `task.delay`, `task.cancel`.\n Never `wait`, `spawn`, `delay` (deprecated, throttled, imprecise).\n4. Yielding engine calls that can fail (DataStore, HTTP, Marketplace, Teleport, `*Async`) go inside `pcall`.\n5. Prefer the `*Async` variants of APIs; Roblox renamed most yielding methods\n (`LoadCharacterAsync`, `GetProductInfoAsync`, `UserHasBadgeAsync`, ...). See the table below.\n6. Disconnect connections / destroy instances you create (see \"Cleanup\" below).\n7. Naming: `PascalCase` for services, modules, classes, types, and instance names;\n `camelCase` for locals and functions; `SCREAMING_SNAKE_CASE` for constants.\n8. Tabs for indentation (StyLua default in the Roblox ecosystem), double quotes.\n\n## Modern syntax cheat sheet\n\n```luau\nlocal Players = game:GetService(\"Players\")\n\nlocal count = 0\ncount += 1 -- compound assignment: += -= *= /= //= %= ^= ..=\n\nlocal label = if count > 1 then \"many\" else \"one\" -- if-expression (no `and/or` pitfalls)\nprint(`Player count: {#Players:GetPlayers()} ({label})`) -- string interpolation\n\nfor index, player in Players:GetPlayers() do -- generalized iteration: no pairs/ipairs needed\n\tif player.AccountAge < 1 then\n\t\tcontinue -- continue is a keyword\n\tend\n\tprint(index, player.Name)\nend\n\nlocal half = 7 // 2 -- floor division → 3\nlocal frozen = table.freeze({ Speed = 16 }) -- read-only table; writes throw\nlocal list = table.create(100, 0) -- preallocated array\nlocal copy = table.clone(frozen) -- shallow copy (unfrozen)\nlocal mid = math.lerp(0, 10, 0.5) -- also: math.map, math.clamp, math.round, math.sign\nprint(half, list[1], copy.Speed, mid)\n```\n\n- `pairs`/`ipairs` still work; generalized iteration is preferred and iterates arrays in order.\n- `#t` is only defined for arrays without holes. Setting `t[i] = nil` in the middle creates holes.\n- Numbers are doubles: integers are exact to 2^53. Use `math.floor`/`//` for integer math.\n- `tick()` is discouraged: use `os.clock()` for benchmarks, `workspace:GetServerTimeNow()` for\n synced time across clients/server, `os.time()`/`DateTime.now()` for wall-clock UTC.\n\n## Types (new type solver)\n\nRoblox's **new type solver** is generally available (Nov 2025) and `Workspace.UseNewLuauTypeSolver`\ncontrols it; `Workspace.LuauTypeCheckMode` sets the default mode. Write code that checks cleanly\nunder `--!strict`.\n\n```luau\n--!strict\nexport type Item = {\n\tid: string,\n\tname: string,\n\tstack: number,\n\ttags: { string }?, -- optional field\n}\n\ntype Rarity = \"Common\" | \"Rare\" | \"Legendary\" -- singleton (literal) union\n\nlocal function totalStack(items: { Item }, filter: ((Item) -> boolean)?): number\n\tlocal total = 0\n\tfor _, item in items do\n\t\tif filter == nil or filter(item) then\n\t\t\ttotal += item.stack\n\t\tend\n\tend\n\treturn total\nend\n\nlocal function first<T>(list: { T }): T? -- generics\n\treturn list[1]\nend\n\nlocal rarity: Rarity = \"Rare\"\nlocal part = workspace:FindFirstChild(\"Door\") :: BasePart? -- cast with ::\nprint(totalStack({}, nil), first({ 1, 2 }), rarity, part)\n```\n\n- Refine before use: `if part then ... end`, `if typeof(x) == \"Instance\" and x:IsA(\"BasePart\") then`.\n- `typeof(v)` knows Roblox types (`\"Vector3\"`, `\"Instance\"`, ...); `type(v)` only knows Lua types.\n- Instance types come from `IsA` refinement or `FindFirstChildOfClass`/`FindFirstChildWhichIsA`.\n- Avoid `any` except at trust boundaries (remote payloads, decoded JSON) — validate there, then narrow.\n\nDetails: generics, type packs, `read`/`write` properties, user-defined type functions, typing\nOOP classes, and migrating legacy code to strict: [references/type-system.md](references/type-system.md).\n\n## Modules and require\n\nModuleScripts run once per environment (server or each client) and return one value, cached for every\nlater `require`. A module in `ReplicatedStorage` required by both sides runs twice, once per side, with\nseparate state.\n\n```luau\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal Signal = require(ReplicatedStorage.Packages.Signal) -- instance path (always works)\nlocal Config = require(\"./Config\") -- require-by-string: sibling of this script (script.Parent)\nlocal Utils = require(\"@self/Utils\") -- child of this script\nlocal Shared = require(\"@game/ReplicatedStorage/Shared\") -- absolute from game\nprint(Signal, Config, Utils, Shared)\n```\n\n- String paths: `./` = `script.Parent`, `../` = `script.Parent.Parent`, `@self/` = `script`,\n `@game/` = `game`. They **do not wait** for instances to exist. Use instance paths with\n `WaitForChild` when the module may not have replicated yet.\n- Never create circular requires; they deadlock/error. Break cycles with dependency injection or events.\n- Don't yield at module top level (no `WaitForChild` chains on things that may never exist, no\n DataStore calls): every requirer blocks until the module returns.\n\n### Module / class pattern\n\n```luau\n--!strict\nlocal Counter = {}\nCounter.__index = Counter\n\nexport type Counter = typeof(setmetatable({} :: { value: number, step: number }, Counter))\n\nfunction Counter.new(step: number?): Counter\n\treturn setmetatable({ value = 0, step = step or 1 }, Counter)\nend\n\nfunction Counter.increment(self: Counter): number\n\tself.value += self.step\n\treturn self.value\nend\n\nreturn Counter\n```\n\nPrefer plain modules of functions + data tables over deep inheritance. Use composition.\n\n## Errors and retries\n\n```luau\nlocal DataStoreService = game:GetService(\"DataStoreService\")\nlocal store = DataStoreService:GetDataStore(\"PlayerData\")\n\nlocal function retry<T...>(attempts: number, fn: () -> T...): (boolean, T...)\n\tlocal delaySeconds = 1\n\tfor attempt = 1, attempts do\n\t\tlocal result = table.pack(pcall(fn))\n\t\tif result[1] then\n\t\t\treturn table.unpack(result, 1, result.n)\n\t\tend\n\t\tif attempt < attempts then\n\t\t\twarn(`attempt {attempt} failed: {result[2]}`)\n\t\t\ttask.wait(delaySeconds)\n\t\t\tdelaySeconds *= 2 -- exponential backoff\n\t\tend\n\tend\n\treturn false\nend\n\nlocal ok, data = retry(3, function()\n\treturn store:GetAsync(\"Player_1\")\nend)\nprint(ok, data)\n```\n\n- `error(\"message\", 2)` blames the caller; `error({ code = \"X\" })` throws a table for structured errors.\n- `xpcall(fn, debug.traceback)` keeps the stack trace.\n- Don't wrap everything in `pcall` — only calls that can fail for reasons outside your code.\n\n## Concurrency: the task library\n\n| Call | Behavior |\n| --- | --- |\n| `task.spawn(fn, ...)` | Run now (until first yield) on a new thread. |\n| `task.defer(fn, ...)` | Run at the end of the current resumption cycle. Good for \"after this finishes\". |\n| `task.delay(t, fn, ...)` | Run after `t` seconds. Returns a thread you can `task.cancel`. |\n| `task.wait(t?)` | Yield ≥ `t` seconds (one frame if omitted); returns elapsed time. |\n| `task.cancel(thread)` | Stop a spawned/delayed thread. |\n| `task.desynchronize()` / `task.synchronize()` | Parallel Luau (inside Actors) — see `roblox-performance`. |\n\n- Per-frame work: `RunService.Heartbeat` (after physics), `RunService.PreSimulation`/`PostSimulation`,\n `RunService:BindToRenderStep` (client, before render, ordered). Don't `while true do task.wait() end` for\n per-frame logic; connect to an event and use its `deltaTime`.\n- Write handlers that work under **deferred** signal behavior (`Workspace.SignalBehavior`): handlers run\n later in the frame, not inline. New template places use `Deferred`, `Default` will switch to it, and\n server authority requires it. Don't rely on handler side effects being visible right after `:Fire()`.\n- Never yield inside metamethods or `Changed`-style callbacks that must be synchronous.\n\n## Cleanup (memory leaks are the #1 Roblox bug)\n\n```luau\nlocal Players = game:GetService(\"Players\")\n\nlocal connections: { RBXScriptConnection } = {}\n\ntable.insert(\n\tconnections,\n\tPlayers.PlayerRemoving:Connect(function(player)\n\t\tprint(`{player.Name} left`)\n\tend)\n)\n\nlocal function cleanup()\n\tfor _, connection in connections do\n\t\tconnection:Disconnect()\n\tend\n\ttable.clear(connections)\nend\n\ncleanup()\n```\n\n- `Instance:Destroy()` disconnects that instance's connections and locks `Parent`; it does **not**\n clear your Lua references — nil out tables that hold players/instances on `PlayerRemoving`.\n- Use `:Once()` for one-shot connections. Use a Trove/Janitor/Maid-style helper for groups.\n- Keys that are Player/Instance objects in long-lived tables leak unless removed on leave/destroy.\n\n## Buffers and vectors (performance-sensitive data)\n\n```luau\nlocal packet = buffer.create(10)\nbuffer.writeu16(packet, 0, 513) -- item id\nbuffer.writef32(packet, 2, 12.5) -- amount\nbuffer.writei32(packet, 6, -4)\nprint(buffer.readu16(packet, 0), buffer.readf32(packet, 2), buffer.len(packet))\n\nlocal v = vector.create(1, 2, 3) -- native 3-float vector (same type as Vector3 at runtime)\nprint(vector.magnitude(v), vector.normalize(v), vector.dot(v, vector.one))\n```\n\n`buffer` is the right tool for compact network payloads, large grids, and binary serialization.\nStandard-library quick reference: [references/stdlib-cheatsheet.md](references/stdlib-cheatsheet.md).\n\n## Legacy → current (most common mistakes)\n\n| Don't write | Write instead |\n| --- | --- |\n| `wait()`, `spawn()`, `delay()` | `task.wait()`, `task.spawn()`, `task.delay()` |\n| `game.Workspace`, `game.Players` | `workspace`, `game:GetService(\"Players\")` |\n| `Instance.new(\"Part\", parent)` | create, set properties, **then** set `.Parent` last |\n| `Humanoid:LoadAnimation()` | `Animator:LoadAnimation()` |\n| `Model:SetPrimaryPartCFrame()` | `Model:PivotTo()` / `Model:GetPivot()` |\n| `workspace:FindPartOnRay(Ray.new(...))` | `workspace:Raycast(origin, direction, params)` |\n| `BodyVelocity`, `BodyPosition`, `BodyGyro` | `LinearVelocity`, `AlignPosition`, `AlignOrientation` |\n| `player:LoadCharacter()` | `player:LoadCharacterAsync()` |\n| `MarketplaceService:GetProductInfo()` | `:GetProductInfoAsync()` |\n| `BadgeService:AwardBadge()` | `:AwardBadgeAsync()` |\n| `player:GetRankInGroup()` | `GroupService:GetRolesInGroupAsync()` / `player:IsInGroupAsync()` |\n| `TeleportService:Teleport*()` variants | `TeleportService:TeleportAsync()` |\n| `Chat` service / `ChatService` modules | `TextChatService` |\n| `GuiObject.Draggable` | `UIDragDetector` |\n| `table.getn`, `table.foreach`, `getfenv` | `#t`, `for ... in`, nothing |\n\nThe full deprecation map lives in the `roblox-code-review` skill.\n\n## Performance idioms (only where it matters)\n\n- Measure first (`debug.profilebegin/profileend`, MicroProfiler, Script Profiler).\n- `--!native` at the top of a script, or `@native` on a function, compiles hot numeric code natively.\n Use it on math-heavy modules, not everywhere (compile time + memory limits). Type annotations help it.\n- Cache `Instance` lookups and service references outside loops; avoid `FindFirstChild` chains per frame.\n- Build large strings with `table.concat` or `buffer`, not repeated `..` in loops.\n- Reuse `RaycastParams`/`OverlapParams` objects instead of creating them per call.\n\n## Related skills\n\n`roblox-architecture` (project structure, client/server), `roblox-networking` (remotes),\n`roblox-performance` (profiling, Parallel Luau), `roblox-code-review` (review checklist, full deprecation map).\n",
25
+ "skills/roblox-luau/references/stdlib-cheatsheet.md": "# Luau standard library on Roblox — cheat sheet\n\nOnly libraries and functions available in Roblox are listed. Deprecated: `table.getn`, `table.foreach`,\n`table.foreachi`, `getfenv`, `setfenv`, globals `wait`, `spawn`, `delay`, `ypcall`, `version`, `elapsedTime`.\n\n## task\n\n`spawn(fnOrThread, ...)`, `defer(fnOrThread, ...)`, `delay(seconds, fnOrThread, ...)`, `wait(seconds?)`,\n`cancel(thread)`, `desynchronize()`, `synchronize()` (Parallel Luau only).\n\n## table\n\n| Function | Notes |\n| --- | --- |\n| `table.create(n, value?)` | Preallocate an array of `n` slots. |\n| `table.insert(t, v)` / `table.insert(t, i, v)` | Append / insert (shifts). |\n| `table.remove(t, i?)` | Remove (shifts). For unordered removal: swap with last, then remove last. |\n| `table.find(t, v, init?)` | Linear search in an array; returns index or nil. |\n| `table.clear(t)` | Empties in place, keeps capacity (good for reused buffers). |\n| `table.clone(t)` | Shallow copy. Deep copy must be written manually. |\n| `table.freeze(t)` / `table.isfrozen(t)` | Shallow immutability. |\n| `table.move(a, i, j, k, b?)` | Bulk copy ranges. |\n| `table.concat(t, sep?)` | Join strings — use for building large strings. |\n| `table.sort(t, lt?)` | In-place, not stable. Comparator must be a strict weak ordering (`<`, never `<=`). |\n| `table.pack(...)` / `table.unpack(t, i?, j?)` | `pack` sets `n` (handles nils). |\n\n## string\n\n`split(s, sep)`, `format` (`%d %s %q %.2f %x`), `find/match/gmatch/gsub` (Lua patterns, not regex),\n`sub`, `rep`, `lower/upper`, `byte/char`, `pack/unpack/packsize` (binary), `len`, `reverse`.\nInterpolation: `` `Hi {name}` `` (use `\\{` for a literal brace). `utf8` library for Unicode:\n`utf8.len`, `utf8.codes`, `utf8.char`, `utf8.offset`, and Roblox additions `utf8.graphemes`,\n`utf8.nfcnormalize`, `utf8.nfdnormalize` (use graphemes when counting/truncating user-visible text).\n\n## math\n\n`clamp`, `round`, `sign`, `floor`, `ceil`, `abs`, `min/max`, `lerp(a, b, t)`, `map(x, inMin, inMax, outMin, outMax)`,\n`noise(x, y?, z?)` (Perlin, returns about -1..1), `random(m?, n?)`, `isnan`, `isinf`, `isfinite`,\n`huge`, `pi`, `tau`. Prefer `Random.new(seed)` objects (`:NextInteger`, `:NextNumber`, `:Shuffle`) over\nglobal `math.random` for reproducible or per-system RNG.\n\n## buffer\n\nFixed-size mutable byte arrays. `create(size)`, `fromstring`, `tostring`, `len`, `copy`, `fill`,\n`read/write` + `i8 u8 i16 u16 i32 u32 f32 f64`, `readstring/writestring(b, offset, str, count?)`,\n`readbits/writebits(b, bitOffset, bitCount, value)`. Little-endian. Buffers can be sent through remotes\nand stored in DataStores (serialized), and are the fastest way to pack network data.\n\n## vector\n\n`vector.create(x, y, z)`, `zero`, `one`, `magnitude`, `normalize`, `cross`, `dot`, `angle(a, b, axis?)`,\n`floor`, `ceil`, `abs`, `sign`, `clamp`, `lerp`, `max`, `min`. Operates on the same immutable 3-component\nvalue type as `Vector3`, with faster, native-codegen-friendly calls.\n\n## bit32\n\n`band`, `bor`, `bxor`, `bnot`, `lshift`, `rshift`, `arshift`, `extract`, `replace`, `btest`, `countlz`, `countrz`, `byteswap`.\n\n## coroutine\n\n`create`, `resume`, `yield`, `wrap`, `status`, `running`, `isyieldable`, `close`. Use coroutines for\ngenerators/state machines; use `task.spawn`/`task.defer` to run code concurrently (errors inside a\n`coroutine.resume` are returned, not reported to Output, and are easy to lose).\n\n## os / time\n\n`os.clock()` (high-res CPU time, benchmarking), `os.time()` (UTC seconds), `os.date(fmt, t)`,\n`os.difftime`. Roblox datatypes: `DateTime.now()`, `DateTime.fromUnixTimestamp`, `:ToIsoDate()`,\n`:FormatLocalTime(fmt, locale)`. Synced server clock: `workspace:GetServerTimeNow()`.\n\n## debug\n\n`traceback(msg?, level?)`, `info(level|fn, \"slnaf\")`, `profilebegin(label)` / `profileend()` (MicroProfiler\nlabels), `setmemorycategory(name)` / `resetmemorycategory()` (Developer Console memory tags),\n`dumpcodesize()` (native codegen size in Studio).\n\n## Roblox globals\n\n`game`, `workspace`, `script`, `plugin` (plugins only), `shared` (avoid: untyped global table — use\nmodules), `typeof`, `require`, `settings()`, `UserSettings()`, `warn`, `print`, `error`, `tick` (avoid).\nDatatypes: `Vector3`, `Vector2`, `CFrame`, `Color3`, `UDim`, `UDim2`, `Rect`, `NumberRange`,\n`NumberSequence`, `ColorSequence`, `BrickColor`, `Enum`, `Instance`, `RaycastParams`, `OverlapParams`,\n`TweenInfo`, `Random`, `DateTime`, `Font`, `Content`, `Path2DControlPoint`, `SharedTable`.\n",
26
+ "skills/roblox-luau/references/type-system.md": "# Luau type system (new solver) — reference\n\nAuthoritative spec: <https://luau.org/typecheck>. Everything here type-checks under the new solver in `--!strict`.\n\n## Modes\n\n| Hot comment | Behavior |\n| --- | --- |\n| `--!nocheck` | No type errors reported. |\n| `--!nonstrict` | Infers types, reports only definite errors. Unannotated values are `any`-ish. |\n| `--!strict` | Full inference and checking. Use for all new modules. |\n\n`Workspace.LuauTypeCheckMode` sets the default for scripts without a hot comment;\n`Workspace.UseNewLuauTypeSolver` (a `RolloutState`) selects the solver. With Rojo + luau-lsp, set\n`\"luau-lsp.fflags.enableNewSolver\": true` (or the equivalent in your editor) so editor and Studio agree.\n\n## Building blocks\n\n```luau\n--!strict\ntype Id = string\ntype Maybe<T> = T? -- same as T | nil\ntype Vec = { x: number, y: number } -- table (record) type\ntype List = { number } -- array\ntype Map = { [string]: number } -- dictionary\ntype Callback = (player: Player, amount: number) -> boolean\ntype Varargs = (...number) -> () -- variadic, returns nothing\ntype Pair<K, V = string> = { key: K, value: V } -- generic with a default\ntype Shape = { kind: \"circle\", radius: number } | { kind: \"rect\", w: number, h: number } -- tagged union\n\nlocal function area(shape: Shape): number\n\tif shape.kind == \"circle\" then\n\t\treturn math.pi * shape.radius ^ 2 -- refined to the circle variant\n\telse\n\t\treturn shape.w * shape.h\n\tend\nend\n\nlocal lookup: Map = { a = 1 }\nlocal pair: Pair<number> = { key = 1, value = \"one\" }\nprint(area({ kind = \"rect\", w = 2, h = 3 }), lookup, pair)\n```\n\n- Tagged unions + `if x.kind == ...` give exhaustive, refined handling — prefer them over loose optional fields.\n- Intersections `A & B` combine table types; overloaded function types are intersections of function types.\n\n## Generics and type packs\n\n```luau\n--!strict\nlocal function map<T, U>(list: { T }, fn: (T) -> U): { U }\n\tlocal out = table.create(#list)\n\tfor i, v in list do\n\t\tout[i] = fn(v)\n\tend\n\treturn out\nend\n\nlocal function wrap<A..., R...>(fn: (A...) -> R...): (A...) -> R...\n\treturn function(...: A...): R...\n\t\treturn fn(...)\n\tend\nend\n\nlocal names = map({ 1, 2, 3 }, function(n)\n\treturn `#{n}`\nend)\nprint(names, wrap(math.max)(1, 5))\n```\n\n`T...` is a type pack (a list of types), used for varargs and multiple returns.\n\n## Refinement\n\n```luau\n--!strict\nlocal function describe(value: unknown): string\n\tif typeof(value) == \"Instance\" then\n\t\tif value:IsA(\"BasePart\") then\n\t\t\treturn `part at {value.Position}` -- refined to BasePart\n\t\tend\n\t\treturn value:GetFullName()\n\telseif type(value) == \"number\" then\n\t\treturn string.format(\"%.2f\", value)\n\tend\n\treturn \"unknown\"\nend\nprint(describe(workspace))\n```\n\n- Use `unknown` (not `any`) for values you haven't validated: the checker forces you to refine.\n- `assert(x, \"msg\")` refines `x` to non-nil after the call.\n- `FindFirstChild` returns `Instance?`; use `FindFirstChildOfClass(\"Humanoid\")` or `FindFirstChildWhichIsA`\n to get a typed result, or refine with `IsA`.\n\n## Casts\n\n`expr :: T` asserts a type. It's only allowed when the types are related (e.g. `Instance` → `Part`).\nTo force an unrelated cast, go through `any`: `(x :: any) :: T` — a code smell; prefer refinement.\n\n## Read-only / write-only properties\n\n```luau\n--!strict\ntype Settings = { read version: number, volume: number }\n\nlocal settings: Settings = { version = 2, volume = 0.5 }\nsettings.volume = 1 -- ok\n-- settings.version = 3 -- type error: property is read-only\nprint(settings.version)\n```\n\nPair with `table.freeze` for runtime immutability.\n\n## Built-in type functions\n\n`keyof<T>` (union of a table's key literals), `index<T, K>` (type of a property), `typeof(expr)`,\nplus `rawkeyof`, `rawget`, `getmetatable`, `setmetatable` type functions.\n\n```luau\n--!strict\nlocal DEFAULTS = { walkSpeed = 16, jumpPower = 50 }\ntype SettingName = keyof<typeof(DEFAULTS)> -- \"walkSpeed\" | \"jumpPower\"\n\nlocal function get(name: SettingName): number\n\treturn DEFAULTS[name]\nend\nprint(get(\"walkSpeed\"))\n```\n\n## User-defined type functions\n\nType functions run at analysis time and build types programmatically with the `types` library.\n\n```luau\n--!strict\ntype function Partial(t)\n\tlocal result = types.newtable()\n\tfor key, prop in t:properties() do\n\t\tresult:setproperty(key, types.optional(prop.read))\n\tend\n\treturn result\nend\n\ntype Stats = { health: number, mana: number }\nlocal patch: Partial<Stats> = { mana = 10 }\nprint(patch)\n```\n\nUse sparingly: they improve library APIs but make code harder for teammates to read.\n\n## Typing classes (OOP)\n\nPattern A — infer from the constructor (least boilerplate):\n\n```luau\n--!strict\nlocal Account = {}\nAccount.__index = Account\n\ntype AccountData = { owner: Player, balance: number }\nexport type Account = typeof(setmetatable({} :: AccountData, Account))\n\nfunction Account.new(owner: Player): Account\n\treturn setmetatable({ owner = owner, balance = 0 }, Account)\nend\n\nfunction Account.deposit(self: Account, amount: number)\n\tassert(amount > 0, \"amount must be positive\")\n\tself.balance += amount\nend\n\nreturn Account\n```\n\nPattern B — explicit interface (best for public library APIs; decouples the shape from the metatable):\n\n```luau\n--!strict\nexport type Timer = {\n\tstart: (self: Timer) -> (),\n\telapsed: (self: Timer) -> number,\n}\n\nlocal function newTimer(): Timer\n\tlocal startedAt = os.clock()\n\tlocal timer = {}\n\tfunction timer.start(_self: Timer)\n\t\tstartedAt = os.clock()\n\tend\n\tfunction timer.elapsed(_self: Timer): number\n\t\treturn os.clock() - startedAt\n\tend\n\treturn timer\nend\n\nreturn newTimer\n```\n\nInheritance: prefer composition (a field holding another object). If you must inherit, set the child\nmetatable's `__index` to the parent class and type the child as an intersection of both shapes.\n\n## Common errors → fixes\n\n| Error | Fix |\n| --- | --- |\n| `Value of type 'Instance?' could be nil` | Refine: `local x = parent:FindFirstChild(\"X\"); if not x then return end`. |\n| `Key 'Foo' not found in external type 'Instance'` | Refine with `IsA`, cast (`:: Folder`), or use a Rojo sourcemap so luau-lsp knows children. |\n| `Type 'string' could not be converted into 'number'` | Convert explicitly (`tonumber(s)`) and handle `nil`. |\n| `Unknown type used in + operation` | Annotate the parameter (`function f(a: number)`). |\n| `Cannot add property 'x' to table` | Declare the field in the table type or constructor literal. |\n| Types from a required module are `any` | The require path isn't resolvable statically: use instance paths or string requires, and a sourcemap with Rojo. |\n\n## Migrating a codebase to strict\n\n1. Enable the new solver; add `--!strict` file by file, starting with leaf modules (utilities, data).\n2. Add `export type` for every module's public data shape; annotate function parameters and returns.\n3. Replace `FindFirstChild` chains with typed accessors or validated lookups.\n4. Treat remote payloads and DataStore data as `unknown` and write validators that return typed values.\n5. Keep `any` only at explicit boundaries, with a comment explaining why.\n",
27
+ "skills/roblox-monetization/SKILL.md": "---\nname: roblox-monetization\ndescription: Robux monetization - developer products with idempotent ProcessReceipt, passes, subscriptions, rewarded video ads, personalized shops, regional pricing and trade arbitrage, paid random item policy (PolicyService), commerce products, private servers, Roblox Plus. Use when adding purchases, shops, game passes, subscriptions, ads, or loot boxes, or debugging lost/duplicated purchases.\n---\n\n# Roblox monetization\n\n## Product types\n\n| Type | Buy | Use for | Grant via |\n| --- | --- | --- | --- |\n| Developer product | Repeatedly | Currency, consumables, boosts, revives | `MarketplaceService.ProcessReceipt` (server) |\n| Pass | Once, permanent | VIP, permanent perks, extra slots | `UserOwnsGamePassAsync` check on join + purchase-finished event |\n| Subscription | Monthly, revocable | Recurring perks (daily currency, VIP) | `GetUserSubscriptionStatusAsync` + `UserSubscriptionStatusChanged` |\n| Rewarded video ad | Free (watch ad) | Small rewards (≈3–10 Robux value) | Developer product granted through `ProcessReceipt` |\n| Commerce product | Real money (physical goods) | Merch with bundled digital benefits | `CommerceService`; digital benefits via `ProcessReceipt` |\n| Private server | Monthly | Paid private servers | Configured in Creator Dashboard |\n\nAll granting happens on the **server**. Never grant from client events like\n`PromptProductPurchaseFinished` — clients can fake them.\n\n## Developer products: ProcessReceipt\n\nRules:\n- Exactly **one** server script sets `MarketplaceService.ProcessReceipt`.\n- Return `PurchaseGranted` only after the grant is **persisted**. Otherwise return `NotProcessedYet`;\n Roblox retries later (including on the player's next join) with the same `PurchaseId`.\n- Handlers must be **idempotent**: record handled `PurchaseId`s in the player's saved data so a retry\n never grants twice.\n- Receipts arrive for players who may not be loaded yet or may have left: wait for their data, or\n return `NotProcessedYet`.\n\n```luau\n--!strict\nlocal MarketplaceService = game:GetService(\"MarketplaceService\")\nlocal Players = game:GetService(\"Players\")\n\ntype PlayerData = { coins: number, purchaseIds: { string } }\ntype ReceiptInfo = {\n\tPlayerId: number,\n\tProductId: number,\n\tPurchaseId: string,\n\tCurrencySpent: number,\n\tCurrencyType: Enum.CurrencyType,\n\tPlaceIdWherePurchased: number,\n}\n\nlocal MAX_REMEMBERED_PURCHASES = 100\n\n-- Provided by your data layer (ProfileStore profile.Data, etc.).\nlocal function getLoadedData(_player: Player): PlayerData?\n\treturn nil\nend\nlocal function saveNow(_player: Player): boolean\n\treturn true\nend\n\nlocal PRODUCTS: { [number]: (Player, PlayerData) -> () } = {\n\t[1234567] = function(_player, data) -- 100 coins\n\t\tdata.coins += 100\n\tend,\n}\n\nlocal function processReceipt(receipt: ReceiptInfo): Enum.ProductPurchaseDecision\n\tlocal player = Players:GetPlayerByUserId(receipt.PlayerId)\n\tif not player then\n\t\treturn Enum.ProductPurchaseDecision.NotProcessedYet -- granted on their next visit\n\tend\n\n\tlocal data = getLoadedData(player)\n\tlocal deadline = os.clock() + 20\n\twhile not data and player.Parent == Players and os.clock() < deadline do\n\t\ttask.wait(0.5)\n\t\tdata = getLoadedData(player)\n\tend\n\tif not data then\n\t\treturn Enum.ProductPurchaseDecision.NotProcessedYet\n\tend\n\n\tif table.find(data.purchaseIds, receipt.PurchaseId) then\n\t\treturn Enum.ProductPurchaseDecision.PurchaseGranted -- already granted earlier\n\tend\n\n\tlocal handler = PRODUCTS[receipt.ProductId]\n\tif not handler then\n\t\twarn(`No handler for product {receipt.ProductId}`)\n\t\treturn Enum.ProductPurchaseDecision.NotProcessedYet\n\tend\n\n\tlocal ok, err = pcall(handler, player, data)\n\tif not ok then\n\t\twarn(`Grant failed for {receipt.PurchaseId}: {err}`)\n\t\treturn Enum.ProductPurchaseDecision.NotProcessedYet\n\tend\n\n\ttable.insert(data.purchaseIds, receipt.PurchaseId)\n\twhile #data.purchaseIds > MAX_REMEMBERED_PURCHASES do\n\t\ttable.remove(data.purchaseIds, 1)\n\tend\n\n\tif not saveNow(player) then\n\t\treturn Enum.ProductPurchaseDecision.NotProcessedYet -- the idempotency list prevents a double grant\n\tend\n\treturn Enum.ProductPurchaseDecision.PurchaseGranted\nend\n\nMarketplaceService.ProcessReceipt = processReceipt :: any\n```\n\nWith ProfileStore, \"persisted\" means waiting until the `PurchaseId` appears in `profile.LastSavedData`\n(or calling `profile:Save()` and waiting); see ProfileStore's developer products guide.\n\nPrompt from the client (UI) — the server still grants:\n`MarketplaceService:PromptProductPurchase(Players.LocalPlayer, productId)`.\n\n## Passes\n\n```luau\n--!strict\nlocal MarketplaceService = game:GetService(\"MarketplaceService\")\nlocal Players = game:GetService(\"Players\")\n\nlocal VIP_PASS_ID = 987654\n\nlocal function grantVip(player: Player)\n\tplayer:SetAttribute(\"VIP\", true) -- apply perks from one place\nend\n\nlocal function checkPass(player: Player)\n\tlocal ok, owns = pcall(MarketplaceService.UserOwnsGamePassAsync, MarketplaceService, player.UserId, VIP_PASS_ID)\n\tif ok and owns then\n\t\tgrantVip(player)\n\tend\nend\n\nPlayers.PlayerAdded:Connect(checkPass)\nfor _, player in Players:GetPlayers() do\n\ttask.spawn(checkPass, player)\nend\n\n-- Fires on the server too; re-verify ownership rather than trusting `purchased` alone.\nMarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player, passId, purchased)\n\tif purchased and passId == VIP_PASS_ID then\n\t\tcheckPass(player)\n\tend\nend)\n```\n\n`UserOwnsGamePassAsync` results are cached per server; the purchase-finished handler covers in-session buys.\n\n## Subscriptions\n\n- Check with `MarketplaceService:GetUserSubscriptionStatusAsync(player, \"EXP-...\")` → `{ IsSubscribed, IsRenewing }`\n on join and on `Players.UserSubscriptionStatusChanged(player, subscriptionId)`.\n- Benefits must be **revocable**: when `IsSubscribed` is false, remove perks you persisted.\n- Prompt with `MarketplaceService:PromptSubscriptionPurchase(player, subscriptionId)`.\n- Replacing a pass with a subscription: keep honoring existing pass owners, take the pass off sale.\n\n## Shops that sell\n\n- Personalize order with `MarketplaceService:RankProductsAsync(identifiers)` (call once on join —\n strict rate limit) and \"Top picks\" with `RecommendTopProductsAsync({ Enum.InfoType.Product, Enum.InfoType.GamePass })`\n (needs a sale in the last 28 days; call in `task.spawn`).\n- Show live prices from `GetProductInfoAsync(id, Enum.InfoType.Product)` / `GetDeveloperProductsAsync()` — never\n hardcode Robux prices: **managed pricing** (regional pricing + price optimization) shows different\n users different prices. `receipt.CurrencySpent` has what the user actually paid.\n- Contextual offers (revive on death, boost when stuck) outperform static menus. Also list items in\n the game's **Shop** tab in Creator Hub.\n\n## Compliance\n\n- **Paid random items** (loot boxes, gacha, spins bought with Robux or Robux-bought currency): disclose\n odds before purchase, and check `PolicyService:GetPolicyInfoForPlayerAsync(player)`:\n - `ArePaidRandomItemsRestricted` → offer a free earnable path, a disclosed fixed sequence, or direct purchase.\n - `IsPaidItemTradingAllowed` → disable trading of paid items when false.\n- **Regional pricing arbitrage**: gate gifting/trading of purchased items using\n `MarketplaceService:GetUsersPriceLevelsAsync(userIds)` (1–1000, % of global price); fetch at join,\n don't cache across sessions.\n- Rewarded video rewards must be developer products, not random, and must not harm the player's\n character while the ad plays.\n- Respect age/region restrictions returned by `PolicyService` for ads, social links, and trading.\n\nRewarded video ads, commerce products, Premium/Roblox Plus hooks: [references/ads-and-extras.md](references/ads-and-extras.md).\n\n## Related skills\n\n`roblox-data-stores` (persisting grants), `roblox-security` (economy exploits), `roblox-ui` (shop UI),\n`roblox-analytics-liveops` (economy events, funnels, experiments).\n",
28
+ "skills/roblox-monetization/references/ads-and-extras.md": "# Rewarded video ads, commerce products, Premium and Roblox Plus\n\n## Rewarded video ads (AdService)\n\nRequirements: creator 13+ and ID-verified, public game with ≥ 2k monthly unique visitors, serving\nenabled in **Creator Hub → Monetization → Ads → Settings**, and a developer product as the reward.\n\nClient — show the button only when an ad is available (check right before showing):\n\n```luau\n--!strict\nlocal AdService = game:GetService(\"AdService\")\n\nlocal function isAdAvailable(): boolean\n\tlocal ok, result = pcall(function()\n\t\treturn AdService:GetAdAvailabilityNowAsync(Enum.AdFormat.RewardedVideo)\n\tend)\n\treturn ok and result.AdAvailabilityResult == Enum.AdAvailabilityResult.IsAvailable\nend\n\nprint(isAdAvailable())\n```\n\nServer — play the ad; the reward arrives through `ProcessReceipt` like any developer product:\n\n```luau\n--!strict\nlocal AdService = game:GetService(\"AdService\")\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal REWARD_PRODUCT_ID = 1919753834 -- developer product in this universe\nlocal requestAd = Instance.new(\"RemoteEvent\")\nrequestAd.Name = \"RequestRewardedAd\"\nrequestAd.Parent = ReplicatedStorage\n\nlocal watching: { [Player]: boolean } = {}\n\nrequestAd.OnServerEvent:Connect(function(player)\n\tif watching[player] then\n\t\treturn\n\tend\n\twatching[player] = true\n\tlocal ok, result = pcall(function()\n\t\tlocal reward = AdService:CreateAdRewardFromDevProductId(REWARD_PRODUCT_ID)\n\t\treturn AdService:ShowRewardedVideoAdAsync(player, reward)\n\tend)\n\twatching[player] = nil\n\trequestAd:FireClient(player, ok, result) -- Enum.ShowAdResult.ShowCompleted on success\nend)\n```\n\n- Protect the character during the ad (lobby/safe zone or pause damage).\n- Clearly label \"Watch an ad to get X\"; rewards can't be random.\n\n## Commerce products (real-world goods)\n\nEligible creators can sell physical products (e.g. imported from Shopify) with bundled digital\nbenefits. Prompt with `MarketplaceService:PromptCommerceProductPurchase(player, commerceProductId)`\n(a string ID); bundled developer-product benefits are granted through `ProcessReceipt`, avatar items\nare granted by Roblox. Check eligibility requirements in the Commerce products docs before building.\n\n## Premium and Roblox Plus\n\n- `player.HasRobloxSubscription` is `true` for players with an active Roblox subscription (use it for\n subscriber perks; `Player.MembershipType` is deprecated). Engagement-based payouts reward subscriber\n playtime automatically — make perks cosmetic/convenience, never pay-to-win.\n- **Roblox Plus** subscribers get 10–20% off eligible purchases (Roblox covers the discount; your\n earnings don't drop), free paid private servers (you're still compensated), and free Robux transfers.\n- `MarketplaceService:PromptRobloxSubscriptionPurchase(player)` prompts a Roblox Plus sign-up and\n earns a referral payout. `PromptPremiumPurchase` is deprecated.\n\n## Private servers\n\nEnable and price in Creator Dashboard. Detect with `game.PrivateServerId ~= \"\"` and\n`game.PrivateServerOwnerId` (`VIPServerId`/`VIPServerOwnerId` are deprecated). Reserved servers\n(`TeleportService:ReserveServerAsync`) also have a non-empty `PrivateServerId` but owner id `0`.\n\n## Analytics for monetization\n\nLog currency sources/sinks with `AnalyticsService:LogEconomyEvent` and purchase funnels with\n`LogFunnelStepEvent` (see `roblox-analytics-liveops`) so you can see which offers convert.\n",
29
+ "skills/roblox-networking/SKILL.md": "---\nname: roblox-networking\ndescription: Client-server communication - RemoteEvent, UnreliableRemoteEvent, RemoteFunction, what survives serialization, typed remote modules, server validation and rate limiting, bandwidth (deltas, buffers, batching), synced time, Zap/Blink. Use when adding remotes, syncing state to clients, debugging nil/lost remote arguments, or reducing lag and bandwidth.\n---\n\n# Roblox networking\n\n## Pick the right channel\n\n| Channel | Delivery | Use for |\n| --- | --- | --- |\n| `RemoteEvent` | Reliable, ordered | Requests (client→server), game events, UI updates. The default. |\n| `UnreliableRemoteEvent` | May drop or reorder; payload ≤ **1000 bytes** (larger payloads are dropped) | Continuous, superseded-by-next data: aim direction, cosmetic VFX, positions of non-physics objects. |\n| `RemoteFunction` | Request/response, yields caller | Client→server queries that need an answer (e.g. \"can I buy X?\"). |\n| Attributes / properties | Automatic replication server→client | Persistent state clients read (health, team, round state). |\n| Physics replication | Automatic for owned assemblies | Moving parts; see `roblox-physics`. |\n\nNever use `RemoteFunction:InvokeClient`: if the client errors, disconnects, or never returns, the server\nthrows or **yields forever**. Use a `RemoteEvent` to the client and another back if you need a reply.\n\n## Defining remotes once (typed)\n\nCreate remotes on the server; clients wait for them. Centralize names so typos can't happen.\n\n```luau\n--!strict\n-- ReplicatedStorage/Shared/Net.luau\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal RunService = game:GetService(\"RunService\")\n\nlocal FOLDER_NAME = \"Remotes\"\nlocal IS_SERVER = RunService:IsServer()\n\nlocal folder: Instance\nif IS_SERVER then\n\tfolder = ReplicatedStorage:FindFirstChild(FOLDER_NAME) or Instance.new(\"Folder\")\n\tfolder.Name = FOLDER_NAME\n\tfolder.Parent = ReplicatedStorage\nelse\n\tfolder = ReplicatedStorage:WaitForChild(FOLDER_NAME)\nend\n\nlocal function get<T>(className: string, name: string): T\n\tif IS_SERVER then\n\t\tlocal existing = folder:FindFirstChild(name)\n\t\tif existing then\n\t\t\treturn existing :: any\n\t\tend\n\t\tlocal remote = Instance.new(className)\n\t\tremote.Name = name\n\t\tremote.Parent = folder\n\t\treturn remote :: any\n\tend\n\treturn folder:WaitForChild(name) :: any\nend\n\nreturn {\n\tRequestPurchase = get(\"RemoteEvent\", \"RequestPurchase\") :: RemoteEvent,\n\tInventoryChanged = get(\"RemoteEvent\", \"InventoryChanged\") :: RemoteEvent,\n\tAimDirection = get(\"UnreliableRemoteEvent\", \"AimDirection\") :: UnreliableRemoteEvent,\n\tGetShopStock = get(\"RemoteFunction\", \"GetShopStock\") :: RemoteFunction,\n}\n```\n\n## Server handler template\n\nEvery `OnServerEvent`/`OnServerInvoke` handler: **rate limit → type-check every argument → check\ngame rules → act**. The first parameter is always the real `Player` (Roblox fills it in; it can't be forged).\n\n```luau\n--!strict\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal Net = require(ReplicatedStorage.Shared.Net)\n\nlocal ITEM_PRICES: { [string]: number } = { Sword = 100, Shield = 150 }\nlocal MIN_INTERVAL = 0.25\nlocal lastRequest: { [Player]: number } = {}\n\nNet.RequestPurchase.OnServerEvent:Connect(function(player: Player, itemId: unknown)\n\tlocal now = os.clock()\n\tif now - (lastRequest[player] or 0) < MIN_INTERVAL then\n\t\treturn -- rate limited\n\tend\n\tlastRequest[player] = now\n\n\tif typeof(itemId) ~= \"string\" or #itemId > 64 then\n\t\treturn -- malformed: never trust argument types\n\tend\n\tlocal price = ITEM_PRICES[itemId]\n\tif not price then\n\t\treturn -- unknown item\n\tend\n\t-- check the player's server-side balance, deduct, grant, then notify:\n\tNet.InventoryChanged:FireClient(player, { added = itemId })\nend)\n\ngame:GetService(\"Players\").PlayerRemoving:Connect(function(player)\n\tlastRequest[player] = nil -- avoid leaking Player keys\nend)\n```\n\nFull validation toolkit (NaN/inf checks, instance ownership, distance and cooldown checks): `roblox-security`.\n\n## What survives the wire\n\n- Tables are **copied**: identity and metatables are lost; functions become `nil`.\n- Don't mix array and dictionary keys in one table; don't put `nil` holes in arrays.\n- Non-string dictionary keys (Instances, numbers in sparse tables) are converted to strings — send arrays of pairs instead.\n- Instances arrive as `nil` if the receiver can't see them (e.g. `ServerStorage` items, or parts not\n yet streamed in on the client).\n- Supported: `nil`, booleans, numbers, strings, tables, `buffer`, Roblox datatypes (`Vector3`, `CFrame`,\n `Color3`, `EnumItem`, ...), and replicated `Instance` references.\n- Server→client ordering vs. property replication is not guaranteed: a remote may arrive before or\n after a property change it relates to.\n\n## Bandwidth and CPU\n\n- Send **changes**, not full state (item added, not the whole inventory). Send on change, not per frame.\n- Throttle input-driven sends on the client (e.g. aim at most 20 Hz) and still rate-limit on the server.\n- Send IDs/enums as small numbers; quantize floats; pack hot, high-frequency data into a `buffer`.\n- Batch many small messages into one per frame (queue in a table, flush on `Heartbeat`).\n- Server-side `TweenService` replicates every frame and looks jittery: tween on clients instead.\n- VFX: the server decides the outcome and fires a minimal event; each client spawns effects locally.\n- Profile with the MicroProfiler (`ProcessPackets`, `Allocate Bandwidth and Run Senders`) and the\n Developer Console **Network** tab.\n\n```luau\n--!strict\n-- Pack a hit notification into 11 bytes instead of a table with string keys.\nlocal function encodeHit(targetId: number, damage: number, position: Vector3): buffer\n\tlocal b = buffer.create(11)\n\tbuffer.writeu32(b, 0, targetId)\n\tbuffer.writeu16(b, 4, math.clamp(math.round(damage), 0, 65535))\n\tbuffer.writei16(b, 6, math.clamp(math.round(position.X), -32768, 32767))\n\tbuffer.writei16(b, 8, math.clamp(math.round(position.Z), -32768, 32767))\n\tbuffer.writeu8(b, 10, math.clamp(math.round(position.Y / 4), 0, 255))\n\treturn b\nend\n\nlocal function decodeHit(b: buffer): (number, number, Vector3)\n\treturn buffer.readu32(b, 0),\n\t\tbuffer.readu16(b, 4),\n\t\tVector3.new(buffer.readi16(b, 6), buffer.readu8(b, 10) * 4, buffer.readi16(b, 8))\nend\n\nprint(decodeHit(encodeHit(42, 25, Vector3.new(10, 20, 30))))\n```\n\nFor many remotes with hot paths, schema-driven generators (**Zap**, **Blink**) produce typed,\nvalidated, buffer-packed remotes automatically. Adopt them when profiling shows network cost.\n\n## Time and latency\n\n- `workspace:GetServerTimeNow()` is a synchronized clock on server and clients: send a start timestamp\n and let clients compute progress locally (round timers, cooldown bars, projectiles).\n- `player:GetNetworkPing()` returns round-trip network latency in seconds (clients may only query\n `LocalPlayer`); use it to size lag-compensation windows.\n- Don't send countdown ticks every second; send the end time once.\n\nMore patterns (request/response with timeouts, state snapshots + deltas, per-frame batching, client\nprediction): [references/patterns.md](references/patterns.md).\n\n## Related skills\n\n`roblox-security` (validation, anti-exploit), `roblox-architecture` (where remotes live),\n`roblox-physics` (network ownership, server authority), `roblox-performance`.\n",
30
+ "skills/roblox-networking/references/patterns.md": "# Networking patterns\n\n## Request/response without RemoteFunction hangs\n\n`InvokeServer` is fine (the server always answers), but the server handler must never yield forever\nand must return quickly. Wrap slow work (DataStore) and always return a result table.\n\n```luau\n--!strict\n-- Server\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal Net = require(ReplicatedStorage.Shared.Net)\n\ntype StockResult = { ok: true, stock: { [string]: number } } | { ok: false, reason: string }\n\nlocal stock: { [string]: number } = { Sword = 5 }\n\nNet.GetShopStock.OnServerInvoke = function(_player: Player): StockResult\n\t-- Never yield indefinitely here: the client is blocked until you return.\n\treturn { ok = true, stock = table.clone(stock) }\nend\n```\n\nClient side, add a timeout so UI never freezes if the server is slow:\n\n```luau\n--!strict\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal Net = require(ReplicatedStorage.Shared.Net)\n\nlocal function invokeWithTimeout(remote: RemoteFunction, timeout: number, ...: any): (boolean, any)\n\tlocal thread = coroutine.running()\n\tlocal done = false\n\tlocal args = table.pack(...)\n\ttask.spawn(function()\n\t\tlocal result = table.pack(pcall(remote.InvokeServer, remote, table.unpack(args, 1, args.n)))\n\t\tif not done then\n\t\t\tdone = true\n\t\t\ttask.spawn(thread, table.unpack(result, 1, result.n))\n\t\tend\n\tend)\n\ttask.delay(timeout, function()\n\t\tif not done then\n\t\t\tdone = true\n\t\t\ttask.spawn(thread, false, \"timeout\")\n\t\tend\n\tend)\n\treturn coroutine.yield()\nend\n\nlocal ok, result = invokeWithTimeout(Net.GetShopStock, 5)\nprint(ok, result)\n```\n\n## State replication: snapshot + deltas\n\nFor per-player private state (inventory, quests) keep the truth on the server and send one snapshot\non join, then small deltas. Clients apply deltas to their copy and fire local signals for UI.\n\n```luau\n--!strict\n-- Server: InventoryService (excerpt)\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal Net = require(ReplicatedStorage.Shared.Net)\n\ntype Inventory = { [string]: number }\nlocal inventories: { [Player]: Inventory } = {}\n\nlocal function sendSnapshot(player: Player)\n\tNet.InventoryChanged:FireClient(player, \"snapshot\", inventories[player])\nend\n\nlocal function addItem(player: Player, itemId: string, amount: number)\n\tlocal inventory = inventories[player]\n\tif not inventory then\n\t\treturn\n\tend\n\tlocal newCount = (inventory[itemId] or 0) + amount\n\tinventory[itemId] = if newCount > 0 then newCount else nil\n\tNet.InventoryChanged:FireClient(player, \"delta\", itemId, inventory[itemId] or 0)\nend\n\nreturn { sendSnapshot = sendSnapshot, addItem = addItem, inventories = inventories }\n```\n\nShared, public state (round phase, scores) is simpler as attributes on a replicated instance (for\nexample `ReplicatedStorage.GameState:SetAttribute(\"Phase\", \"Intermission\")`) — clients listen with\n`GetAttributeChangedSignal`.\n\n## Per-frame batching\n\n```luau\n--!strict\nlocal RunService = game:GetService(\"RunService\")\n\nlocal remote = Instance.new(\"RemoteEvent\")\nlocal queue: { { any } } = {}\n\nlocal function send(...: any)\n\ttable.insert(queue, table.pack(...))\nend\n\nRunService.Heartbeat:Connect(function()\n\tif #queue == 0 then\n\t\treturn\n\tend\n\tlocal batch = queue\n\tqueue = {}\n\tremote:FireAllClients(batch) -- one packet instead of many\nend)\n\nsend(\"hit\", 1, 25)\n```\n\n## Client prediction for responsiveness\n\n1. Client plays the action immediately (animation, sound, local VFX, UI change).\n2. Client fires the request with the minimum data (e.g. aim direction, target id, client timestamp).\n3. Server validates against **its** state (cooldowns, range, ammo, line of sight with ping allowance)\n and applies the authoritative result.\n4. Server broadcasts the result; the originating client reconciles (e.g. rolls back ammo if rejected).\n\nFor competitive games, Roblox's built-in **server authority** model (client prediction + rollback,\n`RunService:BindToSimulation`) handles movement/physics prediction for you — see `roblox-security`\nand `roblox-physics`.\n\n## Unreliable events done right\n\n```luau\n--!strict\n-- Client: stream aim direction at most 20 times per second.\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal RunService = game:GetService(\"RunService\")\n\nlocal Net = require(ReplicatedStorage.Shared.Net)\n\nlocal SEND_INTERVAL = 1 / 20\nlocal accumulator = 0\nlocal lastSent = Vector3.zero\n\nRunService.Heartbeat:Connect(function(deltaTime)\n\taccumulator += deltaTime\n\tif accumulator < SEND_INTERVAL then\n\t\treturn\n\tend\n\taccumulator = 0\n\tlocal camera = workspace.CurrentCamera\n\tif not camera then\n\t\treturn\n\tend\n\tlocal direction = camera.CFrame.LookVector\n\tif direction:FuzzyEq(lastSent, 1e-3) then\n\t\treturn -- unchanged: send nothing\n\tend\n\tlastSent = direction\n\tNet.AimDirection:FireServer(direction)\nend)\n```\n\n- Each message must make sense on its own (full value, not a delta), since earlier ones may be lost.\n- Keep payloads far below 1000 bytes.\n\n## Schema-driven networking (Zap / Blink style)\n\nDefine events once in a schema file; the generator emits `client.luau` and `server.luau` modules with\ntyped `Fire`/`On` functions, validation, and buffer packing. Typical schema shape:\n\n```text\nevent DamageDealt = {\n from: Server,\n type: Reliable,\n call: SingleSync,\n data: struct { target: u32, amount: u16, critical: boolean },\n}\n```\n\nCheck the generator's docs for exact syntax — it differs between tools and versions.\n",
31
+ "skills/roblox-npc-ai/SKILL.md": "---\nname: roblox-npc-ai\ndescription: NPCs and enemy AI - PathfindingService (CreatePath, ComputeAsync, costs, modifiers, links, blocked paths), chase/follow loops, state machines, perception and line of sight, spawning/pooling, scaling to many NPCs. Use when creating enemies, pets, followers, shopkeepers, wave spawners, or fixing NPCs that get stuck, jitter, or lag the server.\n---\n\n# NPCs and AI\n\n## Architecture\n\n- NPC **logic runs on the server** (authoritative targets, damage, drops). Visual extras (idle\n animations, nameplates, effects) can run on clients.\n- One NPC service drives all NPCs from a single loop (e.g. 5–10 Hz \"think\" rate, staggered), not one\n script + `while true` per NPC. Tag NPC models (`CollectionService`) and keep per-NPC state in a table.\n- Server-owned physics: `npc.PrimaryPart:SetNetworkOwner(nil)` after spawning, so NPCs don't stutter\n when ownership moves between nearby players (and exploiters can't push them).\n- Use `Humanoid` for NPCs that need walking/jumping/climbing; for simple movers (turrets, floating\n enemies, crowds) use an `AnimationController` + `Animator` and move with constraints or CFrames.\n\n## Pathfinding\n\n```luau\n--!strict\nlocal PathfindingService = game:GetService(\"PathfindingService\")\n\nlocal path = PathfindingService:CreatePath({\n\tAgentRadius = 2,\n\tAgentHeight = 5,\n\tAgentCanJump = true,\n\tAgentCanClimb = false,\n\tWaypointSpacing = 4,\n\tCosts = {\n\t\tWater = 20, -- avoid water (Enum.Material name)\n\t\tDangerZone = math.huge, -- PathfindingModifier.Label: never cross\n\t},\n})\n\n-- Walk `humanoid` to `goal`. Returns true on arrival, false if unreachable or interrupted.\nlocal function walkTo(humanoid: Humanoid, goal: Vector3): boolean\n\tlocal root = humanoid.RootPart\n\tif not root then\n\t\treturn false\n\tend\n\tlocal ok = pcall(function()\n\t\tpath:ComputeAsync(root.Position, goal)\n\tend)\n\tif not ok or path.Status ~= Enum.PathStatus.Success then\n\t\treturn false -- NoPath: try a fallback (direct MoveTo, new goal, teleport if stuck long)\n\tend\n\n\tlocal waypoints = path:GetWaypoints()\n\tlocal blocked = false\n\tlocal blockedConnection = path.Blocked:Connect(function(blockedIndex)\n\t\tblocked = true -- an obstacle appeared on a waypoint ahead; caller recomputes\n\t\tprint(\"path blocked at waypoint\", blockedIndex)\n\tend)\n\n\tfor index = 2, #waypoints do\n\t\tlocal waypoint = waypoints[index]\n\t\tif waypoint.Action == Enum.PathWaypointAction.Jump then\n\t\t\thumanoid.Jump = true\n\t\tend\n\t\thumanoid:MoveTo(waypoint.Position)\n\t\tlocal reached = humanoid.MoveToFinished:Wait() -- false after 8 s timeout\n\t\tif not reached or blocked or humanoid.Health <= 0 then\n\t\t\tblockedConnection:Disconnect()\n\t\t\treturn false\n\t\tend\n\tend\n\tblockedConnection:Disconnect()\n\treturn true\nend\n\nreturn walkTo\n```\n\n- `CreatePath` + `ComputeAsync` is the current API (`FindPathAsync` is superseded). Reuse `Path` objects\n per agent type; `ComputeAsync` yields — never call it every frame for every NPC.\n- **Chasing moving targets**: recompute when the target moved more than a few studs since the last\n compute or every ~0.5–1 s, and skip pathfinding entirely when there's a clear line of sight\n (`workspace:Raycast` from NPC to target) — just `MoveTo` the target.\n- `PathfindingModifier` (child of a part/region, `Label` matching a `Costs` key, `PassThrough` for doors)\n and `PathfindingLink` (between two attachments, e.g. ladders, teleporters, boats; the waypoint has\n `Label` and `Action = Custom`) shape paths.\n- Limits: start→goal straight-line distance ≤ 3,000 studs, 20,000-node budget; far goals need\n intermediate targets. Visualize the navmesh with Studio's **Navigation Mesh** visualization option.\n- On clients, pathfinding sees only streamed-in geometry: compute paths on the server.\n\n## Behavior: state machine\n\n```luau\n--!strict\ntype State = \"Idle\" | \"Chase\" | \"Attack\" | \"Return\"\n\ntype Npc = {\n\tmodel: Model,\n\thumanoid: Humanoid,\n\thome: Vector3,\n\tstate: State,\n\ttarget: Model?,\n\tlastAttack: number,\n}\n\nlocal AGGRO_RANGE = 40\nlocal ATTACK_RANGE = 5\nlocal LEASH_RANGE = 80\nlocal ATTACK_COOLDOWN = 1.2\n\nlocal losParams = RaycastParams.new()\nlosParams.FilterType = Enum.RaycastFilterType.Exclude\n\nlocal function hasLineOfSight(from: Vector3, target: BasePart, ignore: { Instance }): boolean\n\tlosParams.FilterDescendantsInstances = ignore\n\tlocal result = workspace:Raycast(from, target.Position - from, losParams)\n\treturn result == nil or result.Instance:IsDescendantOf(target.Parent :: Instance)\nend\n\nlocal function think(npc: Npc, findTarget: (Vector3, number) -> Model?)\n\tlocal root = npc.humanoid.RootPart\n\tif not root or npc.humanoid.Health <= 0 then\n\t\treturn\n\tend\n\tlocal targetRoot = npc.target and npc.target:FindFirstChild(\"HumanoidRootPart\") :: BasePart?\n\tlocal distance = if targetRoot then (targetRoot.Position - root.Position).Magnitude else math.huge\n\n\tif npc.state == \"Idle\" then\n\t\tnpc.target = findTarget(root.Position, AGGRO_RANGE)\n\t\tif npc.target then\n\t\t\tnpc.state = \"Chase\"\n\t\tend\n\telseif npc.state == \"Chase\" then\n\t\tif not targetRoot or (root.Position - npc.home).Magnitude > LEASH_RANGE then\n\t\t\tnpc.state = \"Return\"\n\t\telseif distance <= ATTACK_RANGE then\n\t\t\tnpc.state = \"Attack\"\n\t\telseif hasLineOfSight(root.Position, targetRoot, { npc.model }) then\n\t\t\tnpc.humanoid:MoveTo(targetRoot.Position)\n\t\tend -- else: request a path (throttled) via walkTo in a separate thread\n\telseif npc.state == \"Attack\" then\n\t\tif not targetRoot or distance > ATTACK_RANGE * 1.5 then\n\t\t\tnpc.state = \"Chase\"\n\t\telseif os.clock() - npc.lastAttack >= ATTACK_COOLDOWN then\n\t\t\tnpc.lastAttack = os.clock()\n\t\t\tlocal targetHumanoid = npc.target and npc.target:FindFirstChildOfClass(\"Humanoid\")\n\t\t\tif targetHumanoid then\n\t\t\t\ttargetHumanoid:TakeDamage(10)\n\t\t\tend\n\t\tend\n\telseif npc.state == \"Return\" then\n\t\tnpc.target = nil\n\t\tnpc.humanoid:MoveTo(npc.home)\n\t\tif (root.Position - npc.home).Magnitude < 4 then\n\t\t\tnpc.state = \"Idle\"\n\t\tend\n\tend\nend\n\nreturn think\n```\n\nFor complex AI (many behaviors, priorities), use a behavior tree or utility AI; keep the same \"think\nat a fixed rate, act through small primitives\" structure. See [references/scaling-npcs.md](references/scaling-npcs.md)\nfor spawning, pooling, and performance with hundreds of NPCs.\n\n## Perception tips\n\n- Find targets with `workspace:GetPartBoundsInRadius` against a collision group/filter containing only\n characters, or iterate `Players:GetPlayers()` (cheaper for small counts) and compare squared distances.\n- Add reaction delays and field-of-view checks (`root.CFrame.LookVector:Dot(direction.Unit) > 0.5`)\n so NPCs feel fair.\n- Stagger thinking: `npcIndex % 5 == frame % 5` spreads work across frames.\n\n## Related skills\n\n`roblox-characters-animation` (humanoids, animation), `roblox-physics` (ownership, raycasts),\n`roblox-performance` (profiling, parallel Luau), `roblox-game-systems` (waves, combat, drops).\n",
32
+ "skills/roblox-npc-ai/references/scaling-npcs.md": "# Scaling NPCs\n\n## Spawning and pooling\n\n```luau\n--!strict\nlocal CollectionService = game:GetService(\"CollectionService\")\nlocal ServerStorage = game:GetService(\"ServerStorage\")\n\nlocal template = ServerStorage:WaitForChild(\"Enemies\"):WaitForChild(\"Zombie\") :: Model\nlocal pool: { Model } = {}\nlocal POOL_PARENT = ServerStorage\n\nlocal function spawnEnemy(at: CFrame): Model\n\tlocal npc = table.remove(pool) or template:Clone()\n\tlocal humanoid = npc:FindFirstChildOfClass(\"Humanoid\")\n\tif humanoid then\n\t\thumanoid.Health = humanoid.MaxHealth\n\tend\n\tnpc:PivotTo(at)\n\tnpc.Parent = workspace\n\tif npc.PrimaryPart then\n\t\tnpc.PrimaryPart:SetNetworkOwner(nil) -- must be in Workspace first\n\tend\n\tCollectionService:AddTag(npc, \"Enemy\")\n\treturn npc\nend\n\nlocal function despawnEnemy(npc: Model)\n\tCollectionService:RemoveTag(npc, \"Enemy\")\n\tnpc.Parent = POOL_PARENT\n\ttable.insert(pool, npc)\nend\n\nreturn { spawn = spawnEnemy, despawn = despawnEnemy }\n```\n\n- Pooling avoids replication cost of creating/destroying complex models repeatedly. Reset every piece of\n state (health, attributes, velocity, animations) when reusing.\n- Humanoids with `BreakJointsOnDeath = true` can't be reused after death — set it `false` for pooled NPCs.\n- Set `Humanoid.RequiresNeck = false` only if your NPC rig has no neck joint.\n\n## Performance levers (measure with the MicroProfiler first)\n\n| Cost | Lever |\n| --- | --- |\n| Humanoid physics/state machine | Fewer Humanoids: `AnimationController` + anchored/CFrame movement for simple mobs; disable unused states (`SetStateEnabled(Enum.HumanoidStateType.Climbing, false)`, `Swimming`, `Ragdoll`, `FallingDown`). |\n| Pathfinding | Share paths between NPCs heading to the same goal; flow fields/grids for hordes; lower recompute rate. |\n| AI thinking | Fixed think rate (5–10 Hz), staggered; skip NPCs far from all players. |\n| Replication | Fewer parts per NPC (MeshParts), no unnecessary descendants, avoid per-frame property changes on the server. |\n| Animation | Let clients animate NPCs (server sets an attribute like `State = \"Run\"`, clients play tracks). |\n| Heavy math (steering, flocking) | `--!native` module; Parallel Luau `Actor`s with `task.desynchronize()` for read-only computation, then `task.synchronize()` to write. |\n| Rendering far NPCs | Streaming + `Model.LevelOfDetail`; hide/disable NPCs outside interest range. |\n\n## Client-simulated crowds\n\nFor hundreds of purely cosmetic NPCs (city crowds, ambient animals), simulate on clients only:\nthe server sends a seed + spawn areas, each client spawns and animates locally. Nothing gameplay-\nrelevant can depend on these NPCs, because every client sees slightly different positions.\n",
33
+ "skills/roblox-open-cloud/SKILL.md": "---\nname: roblox-open-cloud\ndescription: Open Cloud REST APIs and HttpService - API keys/OAuth, v2 patterns, data/memory stores from outside, publishMessage, place publishing, server restarts, Luau execution, bans, configs/experiments, notifications, webhooks with roblox-signature verification, rate limits, Secrets. Use when building external tools, dashboards, bots, CI/CD, admin panels, or calling web APIs from a game.\n---\n\n# Roblox Open Cloud and HttpService\n\nBase URL: `https://apis.roblox.com`. Prefer **Open Cloud** endpoints (API key or OAuth 2.0, stable).\nLegacy cookie-authenticated `*.roblox.com` web APIs can break without notice — don't build on them.\n\n## Authentication\n\n- **API keys** (Creator Dashboard → Open Cloud → API Keys): for your own/your group's resources and\n automation. Grant the minimum scopes per universe, set an **IP allowlist** (CIDR) and **expiration**,\n store as a secret (CI secret, vault), send as header `x-api-key: <key>`. Group-owned resources need a\n key created under the group. Introspect a key with `POST /api-keys/v1/introspect`.\n- **OAuth 2.0** (authorization code + PKCE): apps acting on behalf of other users (third-party tools).\n- Never ship keys in game scripts, attributes, or client code.\n\n## Common endpoints (v2 unless noted)\n\n| Task | Endpoint |\n| --- | --- |\n| Data store entries | `GET/POST /cloud/v2/universes/{u}/data-stores/{ds}/entries`, `GET/PATCH/DELETE .../entries/{id}`, `:increment`, `:listRevisions`; scopes via `/scopes/{scope}/entries` |\n| Ordered data stores | `/cloud/v2/universes/{u}/ordered-data-stores/{ods}/scopes/{scope}/entries[...]` |\n| Memory stores | `/cloud/v2/universes/{u}/memory-store/sorted-maps/{map}/items`, `/queues/{q}/items[:read|:discard]`, `memory-store:flush` |\n| Cross-server message into live servers | `POST /cloud/v2/universes/{u}:publishMessage` `{ \"topic\": \"...\", \"message\": \"...\" }` |\n| Publish a place file | `POST /universes/v1/{u}/places/{p}/versions?versionType=Published|Saved` (body: `.rbxl`) |\n| Restart servers (after an update) | `POST /cloud/v2/universes/{u}:restartServers` |\n| Run Luau in a cloud server | `POST /cloud/v2/universes/{u}/places/{p}[/versions/{v}]/luau-execution-session-tasks` |\n| Bans | `PATCH /cloud/v2/universes/{u}/user-restrictions/{userId}?updateMask=gameJoinRestriction` (scope `universe.user-restriction:write`), `user-restrictions:listLogs` |\n| Experience notifications | `POST /cloud/v2/users/{userId}/notifications` |\n| Secrets store | `/cloud/v2/universes/{u}/secrets` |\n| Configs / experiments | `/creator-configs-public-api/v1/configs/universes/{u}/repositories/{repo}[/draft|/publish]`, `/experimentation/...` |\n| Universe/place settings | `GET/PATCH /cloud/v2/universes/{u}`, `/cloud/v2/universes/{u}/places/{p}` |\n| Assets, badges, game passes, developer products, groups, users, inventory | See the Open Cloud reference by feature. |\n\nThe authoritative list with scopes and per-endpoint rate limits is the Open Cloud reference\n(`create.roblox.com/docs/cloud/reference`) and its OpenAPI description — check it before writing a client.\n\n## Patterns\n\n- **Pagination**: pass `maxPageSize`; follow `nextPageToken` → `pageToken` until empty.\n- **Long-running operations**: some calls return an operation `path`; poll `GET /cloud/v2/{path}` until `done`.\n- **Filtering**: `filter` query parameter on list endpoints that support it.\n- **Rate limits**: read the `x-ratelimit-*` response headers; on `429`, back off exponentially with jitter.\n Data store budgets are **shared** between Open Cloud and live game servers — throttle bulk scripts so they\n don't starve the game (see `roblox-data-stores`).\n- **Errors**: JSON `{ code, message }`; retry only `429` and `5xx`.\n\n```bash\n# Read one player's data store entry\ncurl -s \"https://apis.roblox.com/cloud/v2/universes/$UNIVERSE_ID/data-stores/PlayerData/entries/User_$USER_ID\" \\\n -H \"x-api-key: $ROBLOX_API_KEY\"\n\n# Announce to all live servers (the game subscribes to the \"Announcements\" topic)\ncurl -s -X POST \"https://apis.roblox.com/cloud/v2/universes/$UNIVERSE_ID:publishMessage\" \\\n -H \"x-api-key: $ROBLOX_API_KEY\" -H \"Content-Type: application/json\" \\\n -d '{\"topic\":\"Announcements\",\"message\":\"{\\\"text\\\":\\\"Update in 5 minutes\\\"}\"}'\n```\n\n## HttpService inside games\n\n- Enable **Allow HTTP Requests** (Experience Settings → Security). Server-side only.\n- Limits: 500 requests/min per server for external URLs; Open Cloud calls have a separate\n 2,500/min per server budget. Always `pcall`; handle non-2xx (`response.Success`).\n- Store keys in the experience **Secrets store** (Creator Hub) and read with `HttpService:GetSecret(name)`,\n which returns an opaque `Secret` usable in headers (and `AddPrefix`/`AddSuffix`), not a readable string.\n- Only a **subset** of Open Cloud endpoints can be called from games (assets, bans, configs, data/memory\n stores, messaging, notifications, and more — check the in-game HTTP docs list). Calls to other\n `roblox.com` domains are blocked.\n\n```luau\n--!strict\nlocal HttpService = game:GetService(\"HttpService\")\n\nlocal function postToWebhook(url: string, payload: { [string]: any }): boolean\n\tlocal ok, response = pcall(function()\n\t\treturn HttpService:RequestAsync({\n\t\t\tUrl = url,\n\t\t\tMethod = \"POST\",\n\t\t\tHeaders = { [\"Content-Type\"] = \"application/json\" },\n\t\t\tBody = HttpService:JSONEncode(payload),\n\t\t})\n\tend)\n\tif not ok then\n\t\twarn(\"HTTP request failed to send:\", response)\n\t\treturn false\n\tend\n\tif not response.Success then\n\t\twarn(`HTTP {response.StatusCode}: {response.StatusMessage}`)\n\tend\n\treturn response.Success\nend\n\nlocal function banViaOpenCloud(universeId: number, userId: number, reason: string): boolean\n\tlocal ok, response = pcall(function()\n\t\treturn HttpService:RequestAsync({\n\t\t\tUrl = `https://apis.roblox.com/cloud/v2/universes/{universeId}/user-restrictions/{userId}?updateMask=gameJoinRestriction`,\n\t\t\tMethod = \"PATCH\",\n\t\t\tHeaders = {\n\t\t\t\t[\"Content-Type\"] = \"application/json\",\n\t\t\t\t[\"x-api-key\"] = HttpService:GetSecret(\"OpenCloudKey\"),\n\t\t\t},\n\t\t\tBody = HttpService:JSONEncode({\n\t\t\t\tgameJoinRestriction = { active = true, displayReason = reason, privateReason = reason },\n\t\t\t}),\n\t\t})\n\tend)\n\treturn ok and response.Success\nend\n\nreturn { postToWebhook = postToWebhook, banViaOpenCloud = banViaOpenCloud }\n```\n\nIn-game, prefer native APIs (`Players:BanAsync`, `DataStoreService`) over calling Open Cloud for the same thing.\nNever log or send player chat/PII to third parties without a legitimate purpose and compliance review.\n\n## Webhooks (Roblox → your server)\n\nConfigure in Creator Dashboard → Webhooks. Triggers: subscription purchased/renewed/cancelled/refunded/\nresubscribed, right-to-erasure requests, commerce order paid/refunded, transaction refunded.\nVerification details and a Node/Python verifier: [references/webhooks.md](references/webhooks.md).\n\n## Related skills\n\n`roblox-data-stores` (budgets, schema), `roblox-tooling` (CI publishing), `roblox-testing` (Luau\nexecution tests), `roblox-analytics-liveops` (configs, experiments), `roblox-security` (bans, secrets).\n",
34
+ "skills/roblox-open-cloud/references/webhooks.md": "# Verifying Roblox webhooks\n\nWhen a webhook has a secret, Roblox sends header `roblox-signature: t=<timestamp>,v1=<signature>`.\nSignature = Base64( HMAC-SHA256( key = secret, message = `<timestamp>.<raw JSON body>` ) ).\nReject requests whose signature doesn't match, and requests older than a few minutes (replay protection).\nUse the **raw** request body bytes, not a re-serialized object.\n\n## Node.js (Express)\n\n```javascript\nimport crypto from \"node:crypto\";\nimport express from \"express\";\n\nconst app = express();\nconst SECRET = process.env.ROBLOX_WEBHOOK_SECRET;\n\napp.post(\"/roblox-webhook\", express.raw({ type: \"application/json\" }), (req, res) => {\n const header = req.get(\"roblox-signature\") ?? \"\";\n const parts = Object.fromEntries(header.split(\",\").map((kv) => kv.split(\"=\", 2)));\n const timestamp = parts.t;\n const signature = parts.v1;\n if (!timestamp || !signature) return res.sendStatus(401);\n\n const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));\n if (ageSeconds > 300) return res.sendStatus(401);\n\n const expected = crypto\n .createHmac(\"sha256\", SECRET)\n .update(`${timestamp}.${req.body.toString(\"utf8\")}`)\n .digest(\"base64\");\n const ok =\n expected.length === signature.length &&\n crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));\n if (!ok) return res.sendStatus(401);\n\n const event = JSON.parse(req.body.toString(\"utf8\"));\n // Idempotency: store processed event IDs; Roblox retries failed deliveries and may send duplicates.\n console.log(\"webhook\", event.EventType ?? event.eventType, event);\n res.sendStatus(200);\n});\n\napp.listen(3000);\n```\n\n## Python (Flask)\n\n```python\nimport base64, hashlib, hmac, os, time\nfrom flask import Flask, request, abort\n\napp = Flask(__name__)\nSECRET = os.environ[\"ROBLOX_WEBHOOK_SECRET\"].encode()\n\n@app.post(\"/roblox-webhook\")\ndef roblox_webhook():\n header = request.headers.get(\"roblox-signature\", \"\")\n parts = dict(p.split(\"=\", 1) for p in header.split(\",\") if \"=\" in p)\n ts, sig = parts.get(\"t\"), parts.get(\"v1\")\n if not ts or not sig or abs(time.time() - int(ts)) > 300:\n abort(401)\n body = request.get_data(as_text=True)\n expected = base64.b64encode(hmac.new(SECRET, f\"{ts}.{body}\".encode(), hashlib.sha256).digest()).decode()\n if not hmac.compare_digest(expected, sig):\n abort(401)\n event = request.get_json()\n # handle event idempotently\n return \"\", 200\n```\n\n## Handling events\n\n- **Right to erasure**: delete the user's data from every data store/key and any external storage; see\n automated RTBF in `roblox-data-stores` (preferred when keys follow static patterns).\n- **Refunds**: optionally revoke the refunded item/currency; record abuse patterns.\n- **Commerce order paid**: may be delivered more than once — deduplicate by order ID.\n- Respond `2xx` quickly and process asynchronously; failed deliveries are retried.\n",
35
+ "skills/roblox-performance/SKILL.md": "---\nname: roblox-performance\ndescription: Performance diagnosis and optimization - MicroProfiler, Script Profiler, Developer Console, Scene Analysis, Luau heap and memory categories, memory leaks, network/replication cost, rendering and physics cost, streaming, native codegen, Parallel Luau with Actors. Use when a game lags, stutters, drops FPS, crashes on mobile, has high ping, or before optimizing hot code.\n---\n\n# Roblox performance\n\n**Measure before optimizing.** Identify whether the bottleneck is server compute, client compute\n(CPU/GPU), memory, network, or load time, then use the matching tool.\n\n## Tools\n\n| Tool | Open | Use for |\n| --- | --- | --- |\n| Developer Console | F9 (in game) | Errors, server/client memory by category, network, script activity. Works in live servers. |\n| MicroProfiler | Ctrl+Alt+F6 (⌘⌥F6); Dev Console for server | Per-frame timing; spikes; dump a frame for detail. |\n| Script Profiler | Studio / Dev Console | Which functions consume Luau time (sampling). |\n| Luau heap snapshots | Dev Console → Memory → Luau Heap | What tables/closures/instances are held and by what (leaks). |\n| Scene Analysis | Studio | Script memory, unparented instances (leaks), instance composition, audio/animation memory, triangles. Also `SceneAnalysisService` via Studio MCP. |\n| Performance Stats / Debug Stats | Ctrl+Alt+F7 / Shift+Ctrl+F1–F5 | Live CPU/GPU/memory/network overlays. |\n| Network Simulation | Studio (Alt+S) | Test with latency, jitter, packet loss. |\n| Performance dashboard | Creator Hub | Live aggregate FPS, server heartbeat, memory, crash rates by device. |\n\nLabel your own code in the MicroProfiler and memory views:\n\n```luau\n--!strict\nlocal RunService = game:GetService(\"RunService\")\n\ndebug.setmemorycategory(\"EnemyAI\") -- memory allocated by this thread is attributed to \"EnemyAI\"\n\nRunService.Heartbeat:Connect(function(_deltaTime)\n\tdebug.profilebegin(\"EnemyAI.think\") -- shows as a labeled bar in the MicroProfiler\n\t-- ... work ...\n\tdebug.profileend()\nend)\n```\n\n## Targets\n\n- Client: 60 FPS on mid-range phones (≈16.6 ms/frame); many players are on low-end Android.\n- Server: heartbeat near 60 Hz; `Stats` (`workspace:GetRealPhysicsFPS()`, Dev Console Server Stats).\n- Memory: mobile clients crash from OOM first. Keep client memory well under ~1–1.5 GB on low-end\n devices; watch `PlaceMemory`, `LuaHeap`, `GraphicsTexture`, `Sounds`, `Animation` categories.\n\n## Most common problems → fixes\n\n| Symptom | Usual cause | Fix |\n| --- | --- | --- |\n| Memory grows over time | Leaked connections, tables keyed by players/instances, destroyed-but-referenced instances, unparented instances | Disconnect/clean up per player & per character; clear tables on `PlayerRemoving`; use Scene Analysis \"unparented instances\" and heap snapshots. |\n| Server lag with many players | Per-player loops each frame, `while true do task.wait() end` loops, heavy remote handlers, physics of many unanchored parts | Event-driven code, one scheduler loop, throttled think rates, anchor static parts, server-own fewer assemblies. |\n| Client FPS drops | Too many parts/draw calls, shadows from many lights, transparency/particles overdraw, per-frame UI updates | Merge/instance meshes, `CastShadow = false` on small parts, fewer shadowed lights, limit particles, update UI on change only. |\n| Spikes every N seconds | Autosave/GC bursts, synchronized loops across players, big `Clone()`s | Stagger with random offsets; pool objects; spread work across frames. |\n| High ping / rubber banding | Remote spam, large payloads, replicating many property changes | Batch, delta-compress, throttle; tween on clients; see `roblox-networking`. |\n| Long join times | Huge place, many unique assets, no streaming | Streaming, SLIM LOD, fewer unique meshes/textures, `ContentProvider:PreloadAsync` only for critical assets. |\n\n## Luau hot paths\n\n- Cache services, instances, and `RaycastParams` outside loops; avoid `FindFirstChild`/`GetDescendants`\n per frame. Replace `GetDescendants` scans with tags (`CollectionService`) or maintained sets.\n- Use `deltaTime` from `Heartbeat`/`PreSimulation` rather than `task.wait()` loops for per-frame work.\n- Avoid creating closures/tables per frame in hot loops; reuse buffers with `table.clear`.\n- `--!native` (whole script) or `@native` (per function) for math-heavy code; typed parameters help\n the native compiler. Don't blanket-enable: compile time and a global native-code memory limit.\n- `--!optimize 2` is how Roblox compiles live games already; don't rely on Studio timings for micro-benchmarks.\n\n## Rendering and assets\n\n- Parts: fewer, larger parts; `MeshPart`s with shared `MeshId` are instanced (cheap); unions (`PartOperation`)\n can be expensive — check triangle counts in Scene Analysis.\n- `RenderFidelity = Automatic`/`Performance` for detailed meshes; `CollisionFidelity = Box`/`Hull` for\n decorative meshes; `CanCollide/CanQuery/CanTouch = false` for pure decoration.\n- Lights: limit shadow-casting `PointLight`/`SpotLight`/`SurfaceLight`; limit `ParticleEmitter.Rate`\n and particle size (overdraw); `Beam`/`Trail` counts.\n- Textures/images: reuse, keep resolutions reasonable (1024² max for most); `SurfaceAppearance` sparingly.\n- Enable streaming (`roblox-architecture`), `Model.LevelOfDetail = SLIM` for static models, and let\n `StreamOutBehavior = Opportunistic` free memory.\n\n## Physics\n\n- Anchor everything that doesn't need to move. Welded decorative parts on moving assemblies add cost.\n- Fewer, simpler collisions (`CollisionFidelity`, collision groups to skip pairs); `CanTouch = false`\n where `Touched` isn't needed.\n- Avoid setting `CFrame` of unanchored parts every frame; use constraints or anchor + CFrame.\n- `workspace:BulkMoveTo(parts, cframes, Enum.BulkMoveMode.FireCFrameChanged)` to move many anchored parts.\n- Adaptive timestepping (`PhysicsSteppingMethod = Adaptive`) reduces physics CPU.\n\n## Parallel Luau\n\nScripts under different `Actor` instances can run in parallel:\n\n```luau\n--!strict\n-- Script parented under an Actor (one Actor per logical unit, e.g. per NPC group or chunk).\nlocal RunService = game:GetService(\"RunService\")\n\nlocal results: { number } = {}\n\nRunService.Heartbeat:ConnectParallel(function(_deltaTime)\n\t-- Parallel phase: read-only access to most instances, pure computation, thread-safe APIs (raycasts).\n\tfor i = 1, 100 do\n\t\tresults[i] = math.noise(i / 10, os.clock())\n\tend\n\ttask.synchronize()\n\t-- Serial phase: write to instances, fire events, call thread-unsafe APIs.\nend)\n```\n\n- `task.desynchronize()` / `task.synchronize()` switch phases; `ConnectParallel` starts in parallel.\n- Scripts in the **same** Actor run serially relative to each other: split work across many Actors.\n- Can't `require` or modify most instances while desynchronized. Communicate with `Actor:SendMessage` /\n `BindToMessageParallel` or `SharedTable` (`SharedTableRegistry`).\n- Good fits: raycast validation, procedural generation, pathfinding-adjacent math, large simulations.\n Bad fits: tiny tasks (sync overhead), instance-heavy work.\n\nProfiling walkthrough and memory-leak hunting: [references/profiling.md](references/profiling.md).\n\n## Related skills\n\n`roblox-architecture` (streaming), `roblox-networking` (bandwidth), `roblox-physics`, `roblox-npc-ai`,\n`roblox-studio-mcp` (Roblox's `rbx-perf-profiling`/scene analysis via MCP).\n",
36
+ "skills/roblox-performance/references/profiling.md": "# Profiling walkthrough\n\n## 1. Classify the problem\n\n| Observation | Look at |\n| --- | --- |\n| Low FPS for everyone, server fine | Client MicroProfiler (render vs script vs physics). |\n| Everyone lags at once, ping spikes | Server: Dev Console → Server Stats heartbeat, server MicroProfiler, Script Profiler (server). |\n| Only mobile crashes | Client memory categories, textures, sounds, streaming settings. |\n| Degrades over session length | Memory leak: heap snapshots over time, unparented instances. |\n\n## 2. MicroProfiler\n\n1. Ctrl+Alt+F6 (client) or Dev Console → MicroProfiler (server). Pause (Ctrl+P) on a spike frame.\n2. Read the frame: `Render` (GPU/CPU render prep), `Physics`/`worldStep`, `Heartbeat`/`RunService`,\n `Replicator`/`ProcessPackets` (network), `WaitingForGPU` (GPU bound).\n3. Expand script scopes: your `debug.profilebegin` labels appear under `$Script`/Heartbeat.\n4. Dump (Dev Console → MicroProfiler → Dump) to an HTML file for offline analysis. Roblox Assistant's\n `rbx-perf-profiling` skill and the Studio MCP can interpret dumps.\n\n## 3. Script Profiler\n\nStart recording (client or server), reproduce the lag for 10–30 s, stop, sort by total/self time.\nLook for functions called far more often than expected (per-frame loops, event storms).\n\n## 4. Memory leaks\n\n1. Dev Console → Memory: which category grows (`LuaHeap`, `Instances`, `Signals`, `PhysicsParts`...)?\n2. Luau Heap: take a snapshot, play for a while, take another, compare. Inspect \"Unique references\" to\n find which table/closure holds objects.\n3. Scene Analysis → **Unparented instances**: instances with `Parent = nil` still referenced from Luau\n (classic: cached characters, UI clones, removed parts kept in tables).\n4. Typical culprits:\n - `Players.PlayerAdded` handlers creating connections that are never disconnected.\n - Tables keyed by `Player`/`Instance` never cleared on leave/destroy.\n - `character.Humanoid.Died:Connect` inside `CharacterAdded` capturing big upvalues — OK if the character is\n destroyed (connections are cleaned on `Destroy`), a leak if characters are only reparented.\n - Instances removed with `Parent = nil` instead of `:Destroy()`.\n\n```luau\n--!strict\n-- Pattern: per-player cleanup bag.\nlocal Players = game:GetService(\"Players\")\n\nlocal bags: { [Player]: { () -> () } } = {}\n\nlocal function track(player: Player, cleanup: () -> ())\n\tlocal bag = bags[player]\n\tif not bag then\n\t\tbag = {}\n\t\tbags[player] = bag\n\tend\n\ttable.insert(bag, cleanup)\nend\n\nPlayers.PlayerRemoving:Connect(function(player)\n\tfor _, cleanup in bags[player] or {} do\n\t\ttask.spawn(cleanup)\n\tend\n\tbags[player] = nil\nend)\n\nreturn track\n```\n\n## 5. Verify\n\nRe-measure after each change on the **lowest-end target device** (or the Device Emulator plus a real\nphone), in a live server with realistic player counts — Studio's single-player timings hide server load.\n",
37
+ "skills/roblox-physics/SKILL.md": "---\nname: roblox-physics\ndescription: Physics and spatial queries - Raycast/Blockcast/Spherecast, overlap queries, collision groups, Touched pitfalls, assemblies and welds, mover constraints replacing BodyMovers, mechanical constraints, network ownership, CFrame math, vehicles/projectiles, and server authority (BindToSimulation, prediction, rollback). Use when moving parts, detecting hits, building vehicles or projectiles, or fixing jitter, flinging, or ownership bugs.\n---\n\n# Roblox physics and spatial queries\n\n## Spatial queries (use these, not Touched, for gameplay decisions)\n\n```luau\n--!strict\nlocal params = RaycastParams.new()\nparams.FilterType = Enum.RaycastFilterType.Exclude\nparams.FilterDescendantsInstances = {} -- e.g. { character } of the shooter\nparams.IgnoreWater = true\n\nlocal function castRay(origin: Vector3, direction: Vector3): RaycastResult?\n\t-- direction's length is the max distance\n\treturn workspace:Raycast(origin, direction, params)\nend\n\nlocal result = castRay(Vector3.new(0, 50, 0), Vector3.new(0, -100, 0))\nif result then\n\tprint(result.Instance, result.Position, result.Normal, result.Material, result.Distance)\nend\n\n-- Swept volumes: thick bullets, melee arcs, ground checks with width.\nlocal sphereHit = workspace:Spherecast(Vector3.new(0, 10, 0), 2, Vector3.new(0, 0, 50), params)\nlocal boxHit = workspace:Blockcast(CFrame.new(0, 10, 0), Vector3.new(4, 4, 4), Vector3.new(0, 0, 50), params)\n\n-- Volume overlap: explosions, hitboxes, zone checks.\nlocal overlap = OverlapParams.new()\noverlap.FilterType = Enum.RaycastFilterType.Exclude\noverlap.MaxParts = 50\nlocal partsInBox = workspace:GetPartBoundsInBox(CFrame.new(0, 5, 0), Vector3.new(10, 10, 10), overlap)\nlocal partsInSphere = workspace:GetPartBoundsInRadius(Vector3.zero, 15, overlap)\nprint(sphereHit, boxHit, #partsInBox, #partsInSphere)\n```\n\n- **Reuse** params objects; update `FilterDescendantsInstances` (or `params:AddToFilter(instance)`) instead\n of allocating per call.\n- `RespectCanCollide = true` to ignore non-collidable parts; `CollisionGroup = \"Name\"` to query as a group.\n- `GetPartsInPart(part, params)` does exact geometry overlap (costlier); `GetPartBoundsIn*` use bounding boxes.\n- Deprecated: `FindPartOnRay*`, `Ray.new`, `FindPartsInRegion3*` → use the APIs above.\n- Client queries only see streamed-in parts; authoritative checks run on the server.\n\n## Touched: know its limits\n\n`Touched` fires from the physics engine, only for parts that physically contact, is fired by whoever\nsimulates the part (clients can spoof or suppress it for parts they own), and can fire many times per\ncontact. Use it for triggers (pads, pickups) with a debounce and server-side validation; use spatial\nqueries for hit detection and zones.\n\n```luau\n--!strict\nlocal Players = game:GetService(\"Players\")\n\nlocal pad = Instance.new(\"Part\")\npad.Anchored = true\npad.Parent = workspace\n\nlocal recently: { [Player]: boolean } = {}\n\npad.Touched:Connect(function(hit)\n\tlocal character = hit:FindFirstAncestorOfClass(\"Model\")\n\tlocal player = character and Players:GetPlayerFromCharacter(character)\n\tif not player or recently[player] then\n\t\treturn\n\tend\n\trecently[player] = true\n\tprint(`{player.Name} stepped on the pad`)\n\ttask.delay(1, function()\n\t\trecently[player] = nil\n\tend)\nend)\n```\n\n## Collision groups\n\n```luau\n--!strict\n-- Collision group APIs now live on WorldRoot (workspace); the PhysicsService versions are superseded.\nworkspace:RegisterCollisionGroup(\"Players\")\nworkspace:RegisterCollisionGroup(\"Ghosts\")\nworkspace:CollisionGroupSetCollidable(\"Players\", \"Ghosts\", false)\n\nlocal ghost = Instance.new(\"Part\")\nghost.CollisionGroup = \"Ghosts\" -- assign by name (CollisionGroupId/SetPartCollisionGroup are deprecated)\nghost.Parent = workspace\n```\n\nRegister groups once at server start (max 32, see `workspace:GetMaxCollisionGroups()`). `CanCollide = false` removes all collisions;\n`CanQuery = false` hides a part from raycasts; `CanTouch = false` disables `Touched` (saves CPU);\n`NoCollisionConstraint` disables collision between two specific parts.\n\n## Assemblies and joints\n\n- An **assembly** = parts rigidly connected (welds, `WeldConstraint`, `Motor6D`, rigid constraints). It\n simulates as one body; the root part (`part.AssemblyRootPart`) determines network ownership.\n- Anchor static geometry. Unanchored + welded to an anchored part = anchored assembly.\n- Weld at edit time with `WeldConstraint` (keeps current offset); in code set `Part0`/`Part1` and parent.\n- Move whole models with `model:PivotTo(cframe)`. Moving many parts at once: `workspace:BulkMoveTo`.\n- Read/set physics via `AssemblyLinearVelocity`/`AssemblyAngularVelocity`, `part:ApplyImpulse()`,\n `ApplyAngularImpulse()`, `AssemblyMass`. `Velocity`/`RotVelocity` are deprecated.\n- `CustomPhysicalProperties = PhysicalProperties.new(density, friction, elasticity, frictionWeight, elasticityWeight)`.\n- Units: 1 stud ≈ 0.28 m, `workspace.Gravity` default 196.2 studs/s².\n\n## Mover constraints (replace BodyMovers)\n\n| Goal | Constraint | Replaces |\n| --- | --- | --- |\n| Hold/drive a velocity | `LinearVelocity` (`VelocityConstraintMode` Vector/Plane/Line) | `BodyVelocity` |\n| Move toward a position | `AlignPosition` (`Mode = OneAttachment` + `Position`, `Responsiveness`, `MaxForce`) | `BodyPosition` |\n| Rotate toward an orientation | `AlignOrientation` (`CFrame`/`LookAtPosition`) | `BodyGyro` |\n| Constant force | `VectorForce` (`RelativeTo = World`) | `BodyForce`, `BodyThrust` |\n| Spin | `AngularVelocity` | `BodyAngularVelocity` |\n| Pull toward point | `LineForce` | `RocketPropulsion` |\n| Torque | `Torque` | — |\n\nAll need an `Attachment` (`Attachment0`). Set `MaxForce`/`MaxTorque` realistically (not `math.huge`\nunless intended) or objects fling. Mechanical constraints: `HingeConstraint` (Motor/Servo actuators for\nwheels, doors), `PrismaticConstraint` (sliders, elevators), `SpringConstraint` (suspension),\n`RopeConstraint`, `RodConstraint`, `BallSocketConstraint`, `CylindricalConstraint` (wheel + steering),\n`UniversalConstraint`, `RigidConstraint`.\n\nVehicle and projectile recipes, CFrame cookbook: [references/recipes.md](references/recipes.md).\n\n## Network ownership\n\n- The server auto-assigns ownership of unanchored assemblies to a nearby client (smooth for that client,\n exploitable by it). Characters are owned by their player.\n- `part:SetNetworkOwner(nil)` = server simulates (secure, laggier for players); `SetNetworkOwner(player)`\n for a vehicle driver; `SetNetworkOwnershipAuto()` to restore. Call on the root part, server-side, after\n the assembly is in `Workspace`. Anchored parts have no owner.\n- Ownership changes cause hitches; don't flip it every frame. Visualize with the viewport's\n **Visualization options → Network owners** toggle.\n\n## Server authority (competitive games)\n\nWorkspace `AuthorityMode = Server` makes the server simulate everything while clients **predict** a few\nframes ahead and **roll back/resimulate** on mispredictions. Requirements: `NextGenerationReplication`,\n`PlayerScriptsUseInputActionSystem`, `SignalBehavior = Deferred`, `UseFixedSimulation`, `StreamingEnabled`\n(setting `AuthorityMode` sets these). Core rules:\n- Game logic lives in functions bound with `RunService:BindToSimulation(fn)` inside a ModuleScript required\n by both server and client.\n- Clients affect the game only through **Input Actions** (InputContexts parented under the `Player`),\n not `UserInputService` events.\n- Sync custom state through **attributes** on predicted instances, written only inside simulation callbacks.\n- Don't cache `AnimationTrack`s; query `Animator:GetTrackByAnimationId()` each step.\n\nRollout status can change — check the docs. Details and patterns: [references/server-authority.md](references/server-authority.md).\n\n## Stability tips\n\n- Jitter: conflicting ownership, constraints fighting, huge mass ratios (> 1:100), or setting CFrame on\n unanchored parts every frame (use constraints instead).\n- Flinging: `MaxForce = math.huge` on light parts, overlapping collisions at spawn, NaN/huge velocities.\n- Sleeping assemblies stop simulating until disturbed — wake with a small impulse if needed.\n- Adaptive timestepping (`Workspace.PhysicsSteppingMethod = Adaptive`) saves CPU; use fixed for precise mechanisms.\n\n## Related skills\n\n`roblox-security` (ownership exploits), `roblox-characters-animation`, `roblox-networking`,\n`roblox-performance`, `roblox-input`.\n",
38
+ "skills/roblox-physics/references/recipes.md": "# Physics recipes\n\n## CFrame cookbook\n\n```luau\n--!strict\nlocal origin = CFrame.new(0, 5, 0)\n\nlocal lookAt = CFrame.lookAt(Vector3.new(0, 5, 0), Vector3.new(10, 5, 10)) -- position facing a target\nlocal rotated = origin * CFrame.Angles(0, math.rad(90), 0) -- rotate in local space\nlocal worldRotated = CFrame.Angles(0, math.rad(90), 0) * origin -- rotate around world origin\nlocal offset = origin * CFrame.new(0, 0, -5) -- 5 studs \"forward\" (LookVector is -Z)\nlocal relative = origin:ToObjectSpace(lookAt) -- lookAt expressed relative to origin\nlocal back = origin:ToWorldSpace(relative) -- == lookAt\nlocal halfway = origin:Lerp(lookAt, 0.5)\nlocal rx, ry, rz = lookAt:ToOrientation() -- radians (Y-X-Z order like Orientation)\nlocal flat = CFrame.fromMatrix(Vector3.zero, Vector3.xAxis, Vector3.yAxis) -- from basis vectors\n\nprint(rotated, worldRotated, offset, back, halfway, rx, ry, rz, flat)\nprint(lookAt.LookVector, lookAt.RightVector, lookAt.UpVector, lookAt.Position)\n```\n\n- Compose right-to-left in world space, left-to-right in local space: `a * b` applies `b` in `a`'s space.\n- Use `PivotTo`/`GetPivot` for models; `part.CFrame` for single parts.\n\n## Projectiles\n\nPick by speed and precision:\n1. **Hitscan** (instant): one server-validated `workspace:Raycast` — rifles, lasers.\n2. **Simulated** (visible travel, drop): step a ray each frame; no physics parts needed.\n3. **Physical** parts: only for slow, physics-interactive objects (grenades); set ownership deliberately.\n\n```luau\n--!strict\nlocal RunService = game:GetService(\"RunService\")\n\nlocal GRAVITY = Vector3.new(0, -workspace.Gravity, 0)\n\nlocal function fireProjectile(origin: Vector3, velocity: Vector3, params: RaycastParams, onHit: (RaycastResult) -> ())\n\tlocal position = origin\n\tlocal currentVelocity = velocity\n\tlocal travelled = 0\n\tlocal connection: RBXScriptConnection\n\tconnection = RunService.Heartbeat:Connect(function(deltaTime)\n\t\tlocal step = currentVelocity * deltaTime + 0.5 * GRAVITY * deltaTime * deltaTime\n\t\tlocal result = workspace:Raycast(position, step, params)\n\t\tif result then\n\t\t\tconnection:Disconnect()\n\t\t\tonHit(result)\n\t\t\treturn\n\t\tend\n\t\tposition += step\n\t\tcurrentVelocity += GRAVITY * deltaTime\n\t\ttravelled += step.Magnitude\n\t\tif travelled > 1000 then\n\t\t\tconnection:Disconnect() -- max range\n\t\tend\n\tend)\nend\n\nreturn fireProjectile\n```\n\nClients render the bullet visuals locally (object pool of tracer parts or Beams); the server runs the\nsame stepping (or validates the client's reported hit — see `roblox-security`).\n\n## Hover / follow with AlignPosition + AlignOrientation\n\n```luau\n--!strict\nlocal function makeFollower(part: BasePart, target: BasePart)\n\tlocal attachment = Instance.new(\"Attachment\")\n\tattachment.Parent = part\n\n\tlocal align = Instance.new(\"AlignPosition\")\n\talign.Mode = Enum.PositionAlignmentMode.OneAttachment\n\talign.Attachment0 = attachment\n\talign.MaxForce = part.AssemblyMass * workspace.Gravity * 4\n\talign.Responsiveness = 20\n\talign.Parent = part\n\n\tlocal orient = Instance.new(\"AlignOrientation\")\n\torient.Mode = Enum.OrientationAlignmentMode.OneAttachment\n\torient.Attachment0 = attachment\n\torient.Responsiveness = 20\n\torient.Parent = part\n\n\treturn function()\n\t\talign.Position = target.Position + Vector3.new(0, 4, 0)\n\t\torient.CFrame = target.CFrame\n\tend\nend\n\nreturn makeFollower\n```\n\n## Simple car (constraint-based)\n\n- Chassis part (root) + 4 wheel parts (cylinders), each joined with a `CylindricalConstraint` (or\n `HingeConstraint` for non-steering wheels) whose `ActuatorType = Motor`; set `AngularVelocity` and\n `MotorMaxTorque` from the throttle. Steering: a `HingeConstraint` servo on the front wheel mounts\n (`ActuatorType = Servo`, `TargetAngle`).\n- Suspension: `SpringConstraint` (`Stiffness`, `Damping`, `FreeLength`) or `CylindricalConstraint` limits.\n- Read input from `VehicleSeat.ThrottleFloat`/`SteerFloat` (the integer `Throttle`/`Steer` are deprecated)\n or Input Actions; give the driver network ownership (`chassis:SetNetworkOwner(driver)`) for responsive\n handling, and validate speed on the server — or use server authority.\n\n## Explosion without the Explosion instance's side effects\n\n```luau\n--!strict\nlocal function blast(center: Vector3, radius: number, maxDamage: number)\n\tlocal params = OverlapParams.new()\n\tparams.FilterType = Enum.RaycastFilterType.Exclude\n\tlocal hitHumanoids: { [Humanoid]: boolean } = {}\n\tfor _, part in workspace:GetPartBoundsInRadius(center, radius, params) do\n\t\tlocal model = part:FindFirstAncestorOfClass(\"Model\")\n\t\tlocal humanoid = model and model:FindFirstChildOfClass(\"Humanoid\")\n\t\tif humanoid and not hitHumanoids[humanoid] then\n\t\t\thitHumanoids[humanoid] = true\n\t\t\tlocal distance = (part.Position - center).Magnitude\n\t\t\thumanoid:TakeDamage(maxDamage * (1 - math.clamp(distance / radius, 0, 1)))\n\t\telseif not part.Anchored then\n\t\t\tlocal direction = (part.Position - center).Unit\n\t\t\tpart:ApplyImpulse(direction * part.AssemblyMass * 60)\n\t\tend\n\tend\nend\n\nreturn blast\n```\n",
39
+ "skills/roblox-physics/references/server-authority.md": "# Server authority model\n\nSource: Roblox \"Server authority model\" and \"Server authority techniques\" docs (2026). The feature has\nbeen rolling out through beta; confirm current status in the docs before committing a project to it.\n\n## What it is\n\nThe server is the single source of truth; clients send only **inputs**. To hide latency, each client\n**predicts** a few frames ahead of the last known server state, compares its prediction with the\nauthoritative state when it arrives, and on a **misprediction** rolls back to the server state and\nresimulates forward, re-applying its inputs. Movement exploits (speed, fly, teleport) disappear because\nclients never report positions.\n\n## Setup (Workspace)\n\n`AuthorityMode = Server` (automatically sets the rest): `NextGenerationReplication` on,\n`PlayerScriptsUseInputActionSystem` on, `SignalBehavior = Deferred`, `UseFixedSimulation` on,\n`StreamingEnabled` on.\n\n## Rules for code\n\n1. **Simulation sync**: put core logic in functions bound with `RunService:BindToSimulation(fn)` inside a\n ModuleScript that both a server Script and a client LocalScript require and initialize. These run on\n fixed steps and are re-run during resimulation.\n2. **Only simulation-access APIs** (labelled \"Simulation Access\" in the API reference, e.g.\n `BasePart.CFrame`) can be used inside bound functions.\n3. **Inputs**: clients influence the game only through Input Actions. `InputContext`s must be descendants\n of the `Player` (e.g. the server clones an `Inputs` folder into each player on join). Read\n `action:GetState()` for every player inside the simulation on both sides. Don't use\n `UserInputService` events in core simulation.\n4. **State**: sync custom data via **attributes** on predicted instances, written only inside simulation\n callbacks. Replicated attributes: first 64 per instance, names ≤ 50 chars, string values ≤ 50 chars.\n5. **Remote events** still work for discrete, non-simulation messages (score popups, purchases) but are\n not ordered relative to property/attribute replication.\n6. **Prediction control**: Roblox predicts simulation-access properties near the local character by default;\n force it on/off with `RunService:SetPredictionMode(instance, mode)`.\n\n```luau\n--!strict\n-- ReplicatedStorage/Simulation.luau — required by a server Script and a client LocalScript.\nlocal Players = game:GetService(\"Players\")\nlocal RunService = game:GetService(\"RunService\")\n\nlocal Simulation = {}\n\nfunction Simulation.initialize()\n\tRunService:BindToSimulation(function(deltaTime: number)\n\t\tfor _, player in Players:GetPlayers() do\n\t\t\tlocal inputs = player:FindFirstChild(\"Inputs\")\n\t\t\tlocal dash = inputs and inputs:FindFirstChild(\"Dash\", true)\n\t\t\tif dash and dash:IsA(\"InputAction\") and dash:GetState() == true then\n\t\t\t\tlocal character = player.Character\n\t\t\t\tlocal root = character and character:FindFirstChild(\"HumanoidRootPart\") :: BasePart?\n\t\t\t\tif root then\n\t\t\t\t\troot:ApplyImpulse(root.CFrame.LookVector * root.AssemblyMass * 50 * deltaTime)\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend)\nend\n\nreturn Simulation\n```\n\n## Techniques\n\n- **Instance stitching**: create instances (`Instance.new`, `Clone`, `Instance.fromExisting`) inside a\n simulation callback from a module required on both sides; client and server derive the same GUID and\n the client's predicted instance is merged with the server's copy. Parent them within the same frame.\n- **Position smoothing**: simulate an invisible object and render a massless, non-collidable visual\n copy that follows it with `TweenService:SmoothDamp` to hide correction snaps.\n- **Animations**: don't cache `AnimationTrack`s (rollback invalidates them); each step, get the live\n track with `Animator:GetTrackByAnimationId(id)` or load it, and mirror animation logic on both sides.\n- **Sounds/VFX**: play effects from predicted state carefully — they may need to be cancelled if the\n prediction is rolled back; trigger irreversible effects from confirmed (server) state.\n- **Other players' inputs** can't be known in advance; design mechanics tolerant of small corrections\n (wind-ups, telegraphed attacks) and use smoothing.\n- Debug with the **server authority visualizer** and tune the simulation radius.\n\nOfficial templates: Racing, Soccer, and Laser Tag \"Server Authority Template\" games on Roblox.\n",
40
+ "skills/roblox-security/SKILL.md": "---\nname: roblox-security\ndescription: Anti-exploit design - never trust the client, validate every remote argument (NaN, spoofed instances, strings), rate limiting, ProximityPrompt/ClickDetector abuse, network ownership, movement and hit validation, economy/trade duplication, code confidentiality, secure teleports, heuristics, Ban API. Use when writing server handlers for client input, combat/economy/trading, or reviewing for exploits.\n---\n\n# Roblox security\n\n**Exploiters fully control their client**: they can run any Luau, read every replicated script\n(decompiled), fire any remote with any arguments at any rate, edit any local property (WalkSpeed,\nprompt `Enabled`, `MaxActivationDistance`), and move anything they have network ownership of.\nClient-side anti-cheat is at best a speed bump. Security comes from server design.\n\n## Core rules\n\n1. **The server decides.** Clients send *intent* (\"I pressed attack aiming here\"), never *results*\n (\"I dealt 50 damage\", \"give me 100 coins\").\n2. **Validate every argument** of every `OnServerEvent`/`OnServerInvoke`: type, structure, range, finiteness.\n3. **Validate context**: is the player alive, in range, not on cooldown, owns the item, allowed to act?\n4. **Rate limit** everything a client can trigger (remotes, prompts, touches, clicks).\n5. **Keep secrets server-side**: server code in `ServerScriptService`/`ServerStorage`; API keys via\n `HttpService:GetSecret()`; never ship unreleased content to production places.\n6. **Prefer silent mitigation** (drop, clamp, correct state) over instant kicks; escalate on repeated signals.\n\n## Argument validation toolkit\n\n```luau\n--!strict\n-- ServerScriptService/Server/Validate.luau\nlocal Validate = {}\n\nfunction Validate.number(value: unknown, min: number, max: number): number?\n\tif typeof(value) ~= \"number\" or not math.isfinite(value) then\n\t\treturn nil -- rejects NaN and ±inf (NaN fails every comparison, so range checks alone don't catch it)\n\tend\n\tif value < min or value > max then\n\t\treturn nil\n\tend\n\treturn value\nend\n\nfunction Validate.integer(value: unknown, min: number, max: number): number?\n\tlocal n = Validate.number(value, min, max)\n\treturn if n and n == math.floor(n) then n else nil\nend\n\nfunction Validate.string(value: unknown, maxLength: number): string?\n\tif typeof(value) ~= \"string\" or #value > maxLength or utf8.len(value) == nil then\n\t\treturn nil -- also rejects invalid UTF-8 (which DataStores refuse)\n\tend\n\treturn value\nend\n\nfunction Validate.vector3(value: unknown, maxMagnitude: number): Vector3?\n\tif typeof(value) ~= \"Vector3\" then\n\t\treturn nil\n\tend\n\tif not (math.isfinite(value.X) and math.isfinite(value.Y) and math.isfinite(value.Z)) then\n\t\treturn nil\n\tend\n\treturn if value.Magnitude <= maxMagnitude then value else nil\nend\n\n-- Accept only a real instance of the expected class that lives under a trusted container.\n-- Exploiters can send tables shaped like instances ({ ClassName = \"Part\", ... }).\nfunction Validate.instanceIn(value: unknown, className: string, container: Instance): Instance?\n\tif typeof(value) ~= \"Instance\" or not value:IsA(className) or not value:IsDescendantOf(container) then\n\t\treturn nil\n\tend\n\treturn value\nend\n\nfunction Validate.enum<T>(value: unknown, allowed: { [T]: boolean }): T?\n\tif allowed[value :: any] then\n\t\treturn value :: any\n\tend\n\treturn nil\nend\n\nreturn Validate\n```\n\n- Treat every handler parameter after `player` as `unknown` and narrow it with these helpers.\n- Look items up by ID in **server** tables (`ITEMS[itemId]`); never read prices/stats from client data\n or from replicated `NumberValue`s the client could reference.\n- Cap table sizes and iterate client tables defensively (keys may be NaN-like, nested, huge).\n\n## Rate limiting (token bucket)\n\n```luau\n--!strict\nlocal Players = game:GetService(\"Players\")\n\ntype Bucket = { tokens: number, last: number }\n\nlocal function newLimiter(capacity: number, windowSeconds: number)\n\tlocal refillRate = capacity / windowSeconds\n\tlocal buckets: { [Player]: Bucket } = {}\n\n\tPlayers.PlayerRemoving:Connect(function(player)\n\t\tbuckets[player] = nil\n\tend)\n\n\treturn function(player: Player): boolean\n\t\tlocal now = os.clock()\n\t\tlocal bucket = buckets[player]\n\t\tif not bucket then\n\t\t\tbucket = { tokens = capacity, last = now }\n\t\t\tbuckets[player] = bucket\n\t\telse\n\t\t\tbucket.tokens = math.min(capacity, bucket.tokens + (now - bucket.last) * refillRate)\n\t\t\tbucket.last = now\n\t\tend\n\t\tif bucket.tokens >= 1 then\n\t\t\tbucket.tokens -= 1\n\t\t\treturn true\n\t\tend\n\t\treturn false\n\tend\nend\n\nlocal allowAttack = newLimiter(5, 1) -- bursts of 5, refills 5/second\nprint(allowAttack)\n```\n\nRate-limit expensive or exploitable operations especially: DataStore/Badge/HTTP calls (they have\ntheir own budgets), cloning large models, teleports, currency grants, anything broadcast to all clients.\n\n## Client-triggered instances are not safe\n\n| Object | What exploiters can do | Server must |\n| --- | --- | --- |\n| `ProximityPrompt` | Fire events while disabled; change distance/line-of-sight/`HoldDuration` locally. Only `Triggered` gets a distance check. | Re-check enabled state, distance from character, alive, cooldown. |\n| `ClickDetector` | Fire from any distance even when disabled. No server checks at all. | Same as above. |\n| `DragDetector` | Inherited click events unchecked; drag respects only `Enabled` and `PermissionPolicy`. | Validate drag results on the server. |\n| `Touched` on client-owned parts | Fake or suppress touches. | Validate with server-side distance/overlap queries. |\n\nIf these objects sit in **unanchored** parts the client can own, an exploiter can pull the part to\nthemselves: anchor them, or `part:SetNetworkOwner(nil)` for server ownership.\n\n## Network ownership and movement\n\nClients own their character's physics, so they can teleport, fly, noclip, speed hack, fling other\nplayers (NaN/huge velocities), and suppress `Touched`. Options, strongest first:\n\n1. **Server authority** (Workspace `AuthorityMode = Server`; rolling out in 2026 — check current status): the\n server simulates movement with client prediction and rollback, eliminating movement exploits.\n See `roblox-physics`.\n2. Server-side movement checks: compare horizontal (XZ) displacement per interval against max speed\n plus latency tolerance; exempt legitimate teleports (set a flag server-side when *you* move them);\n rubber-band to the last valid position instead of kicking.\n3. Keep gameplay-critical unanchored parts server-owned: `part:SetNetworkOwner(nil)`.\n\n## Combat validation (hit reports)\n\nClient sends origin, direction/target part, and timestamp. Server checks:\n- Origin is near the character's **server** position (tolerance scaled by `player:GetNetworkPing()`).\n- Target is alive, an enemy, and within weapon range; claimed hit point is near the target's server position.\n- A server raycast against **static** geometry finds no wall between origin and target.\n- Fire rate, ammo, reload/sprint state tracked on the server.\n\n## Economy, trading, purchases\n\n- Currency, inventory, and prices exist only on the server. Clients request; the server checks balance\n and applies changes atomically.\n- Trades: validate both sides fully, then commit both, then save both; on any failure revert everything.\n Beware \"trade then leave before save\" duplication: lock items during trades and save immediately after.\n- Robux purchases: grant only in `MarketplaceService.ProcessReceipt` (developer products) or after a server\n ownership check (passes). Never trust `PromptProductPurchaseFinished` from the client. See `roblox-monetization`.\n\n## Confidentiality\n\n- Any `LocalScript`, client `Script`, or `ModuleScript` that replicates can be decompiled — even if\n disabled or never required. Don't mix server-only branches into shared modules.\n- Anything replicated (models, UI, attributes) can be data-mined. Keep unreleased content in separate\n private universes, not hidden in production.\n- Clients can teleport to **any place in the universe**: set **Access Control for Places → Secure within\n universe only** in Creator Dashboard, and verify eligibility on join in restricted places (default deny).\n- Never put API keys in scripts or attributes; use the in-experience Secrets store (`HttpService:GetSecret`).\n\n## Detection and consequences\n\nLayer server-side heuristics on top of validation: fastest-possible completion time, rate of resource\ngain, inhumanly regular action cadence, honeypot remotes no legit client fires. Accumulate a\nsuspicion score; act on multiple signals. Ladder: silent logging → quiet mitigation → restrict\nfeatures → kick/ban. Delay visible consequences so exploiters can't bisect your checks.\n\n```luau\n--!strict\nlocal Players = game:GetService(\"Players\")\n\nlocal function banForExploiting(player: Player, evidence: string)\n\tlocal ok, err = pcall(function()\n\t\tPlayers:BanAsync({\n\t\t\tUserIds = { player.UserId },\n\t\t\tDuration = 7 * 24 * 60 * 60, -- seconds; -1 = permanent\n\t\t\tDisplayReason = \"Exploiting detected.\", -- shown to the user (max 400 chars, filtered)\n\t\t\tPrivateReason = evidence, -- visible to you only\n\t\t\tExcludeAltAccounts = false, -- also ban detected alt accounts\n\t\t\tApplyToUniverse = true,\n\t\t})\n\tend)\n\tif not ok then\n\t\twarn(\"BanAsync failed:\", err)\n\t\tplayer:Kick(\"Exploiting detected.\")\n\tend\nend\n\nreturn banForExploiting\n```\n\nEnable `Players.BanningEnabled` for the Ban API. Prefer reversible, time-limited bans (check\n`Players:GetBanHistoryAsync` to escalate); version detection rules and monitor false-positive rates.\n\nSecurity review checklist and common vulnerable-code patterns: [references/checklist.md](references/checklist.md).\n\n## Related skills\n\n`roblox-networking`, `roblox-physics` (ownership, server authority), `roblox-data-stores`,\n`roblox-monetization`, `roblox-code-review`.\n",
41
+ "skills/roblox-security/references/checklist.md": "# Security review checklist\n\nRun through this for any code that handles client input, currency, items, or player data.\n\n## Remotes\n\n- [ ] Every `OnServerEvent`/`OnServerInvoke` validates **every** argument's type with `typeof`.\n- [ ] Numbers checked with `math.isfinite` before range checks (NaN/inf bypass comparisons).\n- [ ] Strings length-capped and UTF-8 validated (`utf8.len(s) ~= nil`) before use or saving.\n- [ ] Tables: expected shape only, size-capped, no trusting of nested fields; no reading \"Instance-like\" tables.\n- [ ] Instances checked with `IsA` **and** `IsDescendantOf(trustedContainer)`.\n- [ ] Per-player rate limit; per-player state cleared on `PlayerRemoving`.\n- [ ] Handler checks context: alive, distance, cooldown, ownership, permissions, game phase.\n- [ ] No handler grants rewards based on client-reported outcomes (damage dealt, score, kills, coins).\n- [ ] No `RemoteFunction:InvokeClient` anywhere.\n- [ ] Remotes that relay to other clients (`FireAllClients` on client request) validate and rate-limit,\n and clamp what other clients receive (effect positions near the sender, bounded sizes).\n- [ ] No remote loads assets or requires modules by client-supplied IDs/paths.\n\n## Physics and movement\n\n- [ ] Gameplay-critical unanchored parts are server-owned (`SetNetworkOwner(nil)`) or anchored.\n- [ ] Interactables (`ProximityPrompt`, `ClickDetector`) are not in client-ownable assemblies.\n- [ ] Movement-sensitive rewards (races, obbies) validate checkpoints in order with plausible timing.\n- [ ] Server authority considered for competitive games.\n\n## Economy and data\n\n- [ ] Prices, drop rates, stats defined only in server modules.\n- [ ] Currency changes happen in one server module with invariant checks (never negative, capped).\n- [ ] Trades are atomic (validate both → commit both → save both; revert on failure; lock during trade).\n- [ ] Developer products granted only in `ProcessReceipt`, idempotently by `PurchaseId`.\n- [ ] Session locking for player data (ProfileStore or equivalent) to prevent cross-server duplication.\n\n## Confidentiality and access\n\n- [ ] No server logic or secrets in `ReplicatedStorage`, `ReplicatedFirst`, `StarterPlayer`, `StarterGui`, `Workspace`.\n- [ ] API keys stored as experience Secrets and read with `HttpService:GetSecret`.\n- [ ] Restricted places: \"Secure within universe only\" access control + server-side eligibility check, default deny.\n- [ ] Admin commands check `player.UserId` against a server-side list (or group role via\n `GroupService:GetRolesInGroupAsync`), never `player.Name`, never a client flag.\n\n## Common vulnerable patterns\n\n```luau nocheck\n-- ❌ Client decides the outcome\nRemote.OnServerEvent:Connect(function(player, amount)\n\tplayer.leaderstats.Coins.Value += amount\nend)\n\n-- ❌ NaN passes both checks\nif typeof(amount) == \"number\" and amount > 0 and amount <= balance then end\n\n-- ❌ Trusting a client-provided \"item\" table or ReplicatedStorage value object for the price\nBuyItem.OnServerInvoke = function(player, item) return charge(player, item.Price.Value) end\n\n-- ❌ Relay without validation: any client can make everyone's screen flash anywhere\nCast.OnServerEvent:Connect(function(player, position) Cast:FireAllClients(position) end)\n\n-- ❌ Admin check by name or by client-sent flag\nif player.Name == \"Owner\" or isAdminFlagFromClient then end\n```\n\n```luau\n--!strict\n-- ✅ Server owns the numbers; client only asks\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal ITEMS: { [string]: { price: number } } = { Sword = { price = 100 } }\nlocal balances: { [Player]: number } = {}\n\nlocal buyItem = Instance.new(\"RemoteFunction\")\nbuyItem.Name = \"BuyItem\"\nbuyItem.Parent = ReplicatedStorage\n\nbuyItem.OnServerInvoke = function(player: Player, itemId: unknown): boolean\n\tif typeof(itemId) ~= \"string\" then\n\t\treturn false\n\tend\n\tlocal item = ITEMS[itemId]\n\tlocal balance = balances[player] or 0\n\tif not item or balance < item.price then\n\t\treturn false\n\tend\n\tbalances[player] = balance - item.price\n\t-- grant item in the server-side inventory here\n\treturn true\nend\n```\n",
42
+ "skills/roblox-studio-mcp/SKILL.md": "---\nname: roblox-studio-mcp\ndescription: Drive Roblox Studio from an AI agent via the built-in Studio MCP server - connecting Claude Code, Codex, Cursor, OpenCode, Gemini CLI; tools to explore the DataModel, read/edit scripts, run Luau in edit/server/client, playtest, read output, capture screenshots, simulate input, insert assets. Use when an agent should inspect, modify, or playtest an open Studio place, or to set up the connection.\n---\n\n# Working in Roblox Studio through MCP\n\nRoblox Studio ships a **built-in MCP server** (stdio). With it, an agent can explore the open place,\nedit scripts, run Luau in edit/server/client contexts, playtest, read the Output, capture the viewport,\nand simulate player input. It is the fastest way to **verify** Roblox changes instead of guessing.\n\n## Setup\n\n1. Latest Studio → open **Assistant** → **…** → **Manage MCP Servers** → enable **Enable Studio as MCP server**.\n2. Use **Quick connect** (Claude Code, Claude Desktop, Codex CLI, Cursor, Gemini CLI, VS Code, Antigravity)\n or configure the client manually:\n\n| OS | Command |\n| --- | --- |\n| macOS | `/Applications/RobloxStudio.app/Contents/MacOS/StudioMCP` |\n| Windows | `cmd.exe /c %LOCALAPPDATA%\\Roblox\\mcp.bat` |\n\n```bash\n# Claude Code\nclaude mcp add Roblox_Studio -- /Applications/RobloxStudio.app/Contents/MacOS/StudioMCP\nclaude mcp add Roblox_Studio -- cmd.exe /c \"%LOCALAPPDATA%\\Roblox\\mcp.bat\" # Windows\n```\n\n```json\n{ \"mcpServers\": { \"Roblox_Studio\": { \"command\": \"/Applications/RobloxStudio.app/Contents/MacOS/StudioMCP\" } } }\n```\n\nClient-specific config (Codex `config.toml`, OpenCode `opencode.json`, Cursor/VS Code `mcp.json`):\n[references/client-setup.md](references/client-setup.md). A green indicator in **Manage MCP Servers**\nconfirms the connection. Only connect clients you trust: they can read and modify your places.\n\n## Tools\n\n| Group | Tools |\n| --- | --- |\n| Session | `list_roblox_studios` (every call takes a `studio_id`), `get_studio_state` (play state, available DataModels) |\n| Explore | `search_game_tree` (filter by path/class/keyword, depth), `inspect_instance` (properties, attributes, children), `script_search` (fuzzy by name), `script_grep` (text across scripts, ≤ 50 hits), `script_read` (dot path like `game.ServerScriptService.Main`, line ranges) |\n| Edit | `multi_edit` (several edits to one script, creates it if missing; `datamodel_type = Edit`) |\n| Run | `execute_luau` (`datamodel_type` = `Edit`, `Server`, or `Client`; returns the result or error) |\n| Playtest | `start_stop_play`, `get_console_output`, `screen_capture` (optional camera position/look-at), `character_navigation`, `user_keyboard_input`, `user_mouse_input` |\n| Assets | `search_asset` (Creator Store / inventory), `insert_asset`, `upload_image`, `store_image`, `generate_mesh`, `generate_material`, `generate_procedural_model` + `wait_job_finished` |\n| Delegation | `subagent` (`explore` or `playtest` types for multi-step investigations) |\n| Knowledge | `http_get` (Roblox docs, API reference, Cloud docs), `skill` (Roblox-authored skills: debugging, device simulation, docs search, profiling, scene analysis, unit tests) |\n\n## The working loop\n\n1. **Orient**: `list_roblox_studios` → pick `studio_id`; `get_studio_state`. Map the project with\n `search_game_tree` (depth-limited: services first), then `script_grep`/`script_search` for the feature.\n2. **Read before writing**: `script_read` every script you'll touch and its requirers. Check for a\n Rojo/Script Sync setup (see below) before editing in Studio.\n3. **Edit** with `multi_edit` (exact, minimal edits; keep existing style). Create instances/attributes/\n tags with `execute_luau` in `Edit` context — changes made this way are **real edits to the place**.\n4. **Verify statically**: re-read the script; for pure modules, `execute_luau` a quick check\n (`require` the module in `Edit` and call functions).\n5. **Playtest**: `start_stop_play` (start) → wait → `get_console_output` for errors → drive the scenario\n with `character_navigation` / input tools → `execute_luau` in `Server`/`Client` to assert state →\n `screen_capture` for visual checks → `start_stop_play` (stop). Always stop play when done.\n6. **Report** what changed and what was verified (and what wasn't).\n\n```luau\n-- Example execute_luau (Server context during play): assert game state instead of eyeballing.\nlocal Players = game:GetService(\"Players\")\nlocal results = {}\nfor _, player in Players:GetPlayers() do\n\tlocal leaderstats = player:FindFirstChild(\"leaderstats\")\n\tlocal coins = leaderstats and leaderstats:FindFirstChild(\"Coins\")\n\ttable.insert(results, `{player.Name}: {if coins and coins:IsA(\"IntValue\") then coins.Value else \"missing\"}`)\nend\nreturn table.concat(results, \"\\n\")\n```\n\n## Rules of thumb\n\n- `execute_luau` in `Edit` mutates the place: prefer read-only queries while exploring, and batch\n intentional changes into one clear script. Use `ChangeHistoryService` recording (`TryBeginRecording`/\n `FinishRecording`) so the user can undo your edit as one step.\n- Changes made during play (Server/Client contexts) are discarded when play stops — edit in `Edit`.\n- **Rojo projects**: the file system is the source of truth — edit files on disk, let Rojo sync, and use\n MCP to playtest/inspect. Editing synced scripts via `multi_edit` gets overwritten. **Script Sync**\n projects sync both ways, so either side works, but don't edit the same script in both at once.\n- Keep `search_game_tree` queries narrow (path + depth); large places produce huge outputs.\n- Multiple Studio windows: always pass the right `studio_id`; tell the user which place you touched.\n- Inserted assets from the Creator Store can contain scripts: inspect them (`search_game_tree` +\n `script_read`) before keeping them; remove unexpected scripts (backdoors are common in free models).\n- Don't claim success without evidence: cite console output, assertion results, or screenshots.\n\n## Roblox-authored skills and conversions\n\nStudio exposes Roblox-authored skills through the `skill` tool (e.g. `rbx-debug`, `rbx-perf-profiling`,\n`rbx-scene-analysis`, `rbx-unit-test`, `rbx-docs-search`, `rbx-device-simulator-lua`). Roblox also\npublishes an official **streaming conversion** skill (`/rbx-convert-to-streaming`, downloadable from the\n\"Techniques and conversion\" streaming docs) that runs through this MCP server; back up the place first.\n\n## Related skills\n\n`roblox-testing` (test runners), `roblox-performance` (profiling), `roblox-tooling` (Rojo/Script Sync),\n`roblox-architecture` (where things belong), `roblox-code-review`.\n",
43
+ "skills/roblox-studio-mcp/references/client-setup.md": "# Connecting MCP clients to Roblox Studio\n\nServer command:\n- macOS: `/Applications/RobloxStudio.app/Contents/MacOS/StudioMCP`\n- Windows: `cmd.exe` with args `/c %LOCALAPPDATA%\\Roblox\\mcp.bat`\n\nEnable it first in Studio: **Assistant → … → Manage MCP Servers → Enable Studio as MCP server**.\nClient config formats change over time; if one below doesn't load, check that client's MCP docs.\n\n## Claude Code\n\n```bash\nclaude mcp add Roblox_Studio -- /Applications/RobloxStudio.app/Contents/MacOS/StudioMCP\n# Windows (PowerShell/cmd):\nclaude mcp add Roblox_Studio -- cmd.exe /c \"%LOCALAPPDATA%\\Roblox\\mcp.bat\"\n```\n\nOr project-scoped `.mcp.json` at the repo root:\n\n```json\n{\n \"mcpServers\": {\n \"Roblox_Studio\": {\n \"command\": \"/Applications/RobloxStudio.app/Contents/MacOS/StudioMCP\"\n }\n }\n}\n```\n\n## Claude Desktop / Cursor / VS Code (mcp.json-style)\n\n```json\n{\n \"mcpServers\": {\n \"Roblox_Studio\": {\n \"command\": \"cmd.exe\",\n \"args\": [\"/c\", \"%LOCALAPPDATA%\\\\Roblox\\\\mcp.bat\"]\n }\n }\n}\n```\n\n(VS Code's `.vscode/mcp.json` uses a top-level `\"servers\"` key instead of `\"mcpServers\"`.)\n\n## OpenAI Codex CLI (`~/.codex/config.toml`)\n\n```toml\n[mcp_servers.roblox_studio]\ncommand = \"/Applications/RobloxStudio.app/Contents/MacOS/StudioMCP\"\n```\n\n## OpenCode (`opencode.json`)\n\n```json\n{\n \"$schema\": \"https://opencode.ai/config.json\",\n \"mcp\": {\n \"roblox_studio\": {\n \"type\": \"local\",\n \"command\": [\"/Applications/RobloxStudio.app/Contents/MacOS/StudioMCP\"],\n \"enabled\": true\n }\n }\n}\n```\n\n## Gemini CLI (`~/.gemini/settings.json`)\n\n```json\n{\n \"mcpServers\": {\n \"Roblox_Studio\": { \"command\": \"/Applications/RobloxStudio.app/Contents/MacOS/StudioMCP\" }\n }\n}\n```\n\n## Troubleshooting\n\n- Restart both Studio and the client after changing config.\n- Verify the binary path exists (Studio updates can move it; re-copy from **Manage MCP Servers**).\n- JSON syntax errors (missing commas) silently prevent loading.\n- Remote/cloud agents (running on another machine or in a container) can't reach a local Studio over\n stdio; run the agent on the machine where Studio runs.\n- Older setups used the standalone open-source `Roblox/studio-rust-mcp-server` + plugin; the built-in\n server supersedes it.\n",
44
+ "skills/roblox-teleport-matchmaking/SKILL.md": "---\nname: roblox-teleport-matchmaking\ndescription: Multi-place and multi-server games - universes and places, TeleportAsync with TeleportOptions (reserved/specific servers, teleport data), retrying failed teleports, secure access control, lobby-to-match flows, parties and queues, Roblox matchmaking with custom signals, server type detection, data handoff. Use when adding lobbies, match servers, dungeons, parties, server browsers, or customizing server selection.\n---\n\n# Teleports, places, and matchmaking\n\n## Structure\n\n- A **universe** (experience) contains a start place plus sub-places. All places share DataStores,\n MemoryStores, MessagingService, developer products, and passes.\n- Typical layouts: Lobby (start place) → Match place (reserved servers) → back to Lobby; or a hub with\n separate world places. Keep shared code in packages or a shared Rojo tree.\n- **Access Control for Places** (Creator Dashboard): `Fully open`, `Limited to same universe`, or\n **`Secure within universe only`** (only server-initiated teleports into non-start places — recommended\n for progression gates and test places). Clients can otherwise teleport themselves anywhere in the universe.\n\n## Teleporting (server)\n\n```luau\n--!strict\nlocal TeleportService = game:GetService(\"TeleportService\")\n\nlocal ATTEMPTS = 5\nlocal RETRY_DELAY = 1\nlocal FLOOD_DELAY = 15\n\nlocal function safeTeleport(placeId: number, players: { Player }, options: TeleportOptions?): (boolean, any)\n\tlocal ok: boolean, result: any = false, nil\n\tfor _ = 1, ATTEMPTS do\n\t\tok, result = pcall(function()\n\t\t\treturn TeleportService:TeleportAsync(placeId, players, options)\n\t\tend)\n\t\tif ok then\n\t\t\tbreak\n\t\tend\n\t\ttask.wait(RETRY_DELAY)\n\tend\n\tif not ok then\n\t\twarn(\"Teleport failed:\", result)\n\tend\n\treturn ok, result\nend\n\n-- A teleport can still fail after TeleportAsync succeeds: retry transient failures.\nTeleportService.TeleportInitFailed:Connect(function(player, teleportResult, errorMessage, placeId, options)\n\tif teleportResult == Enum.TeleportResult.Flooded then\n\t\ttask.wait(FLOOD_DELAY)\n\telseif teleportResult == Enum.TeleportResult.Failure then\n\t\ttask.wait(RETRY_DELAY)\n\telse\n\t\twarn(`Invalid teleport [{teleportResult.Name}]: {errorMessage}`)\n\t\treturn\n\tend\n\tsafeTeleport(placeId, { player }, options)\nend)\n\nreturn safeTeleport\n```\n\n`TeleportOptions`:\n- `ShouldReserveServer = true` → a new **reserved server** (private, only reachable by access code) —\n ideal for matches, dungeons, and parties. Returns `TeleportAsyncResult` with `ReservedServerAccessCode`\n and `PrivateServerId`.\n- `ReservedServerAccessCode = code` → join an existing reserved server (from `ReserveServerAsync(placeId)`\n or a previous result).\n- `ServerInstanceId = jobId` → a specific public server (e.g. \"join friend's server\").\n- `SetTeleportData(table)` → non-secure data readable on arrival with `player:GetJoinData().TeleportData`\n (server) or `TeleportService:GetLocalPlayerTeleportData()` (client). **Clients can tamper with it**:\n never trust it for currency, items, or permissions — use DataStores/MemoryStores keyed by user ID\n or by the reserved server's `PrivateServerId`.\n\nDeprecated: `Teleport`, `TeleportToPlaceInstance`, `TeleportToPrivateServer`, `TeleportPartyAsync`,\n`TeleportToSpawnByName`, `ReserveServer` → `TeleportAsync` + `TeleportOptions` / `ReserveServerAsync`.\n\n## Data handoff between places\n\n1. Save the player's data **before** teleporting (ProfileStore: end the session / wait for save).\n2. The destination loads with session locking (ProfileStore retries while the old server releases).\n3. For match parameters (mode, map, team assignments), write a MemoryStore/DataStore entry keyed by\n the reserved server's `PrivateServerId` (or the access code) before teleporting; the match server reads\n `game.PrivateServerId` on start and loads its config.\n4. On arrival, `TeleportService:SetTeleportGui(gui)` (client, before teleport) keeps a loading screen\n up across the transition; use `ReplicatedFirst` loading screens on arrival.\n\n## Detecting server type\n\n| Server | `game.PrivateServerId` | `game.PrivateServerOwnerId` |\n| --- | --- | --- |\n| Public | `\"\"` | `0` |\n| Reserved (`ReserveServerAsync`/`ShouldReserveServer`) | non-empty | `0` |\n| Private (VIP) server | non-empty | owner's user id |\n\n## Lobby → match flow\n\n1. Players queue in the lobby (party leader invites; party members stored server-side).\n2. Queue in a **MemoryStoreQueue** / sorted map by mode and rating (see `roblox-data-stores`), or use\n Roblox matchmaking to fill public servers.\n3. When a match forms: write match config to MemoryStore keyed by a new id, `TeleportAsync` all players\n with `ShouldReserveServer = true`, store the returned `PrivateServerId` ↔ match id mapping.\n4. Match server: waits for expected players (timeout), runs the round, saves results, teleports everyone\n back to the lobby place (a public server, or the party's lobby via `ServerInstanceId`).\n\n## Roblox matchmaking (which server a joining player gets)\n\nDefault matchmaking scores eligible public servers by signals (friends, latency, language, age group,\noccupancy...) and picks the highest weighted sum. You can customize it per place in Creator Hub:\n- Adjust weights of Roblox signals, or add **custom signals** based on **custom attributes**:\n - Player attributes: values stored in a **DataStore** (e.g. `PlayerElo`), configured in the matchmaking settings.\n - Server attributes: set at runtime with `MatchmakingService:SetServerAttribute(name, value)` (e.g.\n `GameMode`, `ServerLevel`); `InitializeServerAttributesForStudio` for testing.\n- Preview scores against mock servers before applying, then apply the configuration to places.\nUse it for skill-based public servers; use reserved servers when you need exact team composition.\n\n## Server lifecycle\n\n- `game:BindToClose` for shutdown saves; `game.JobId` identifies the server; soft shutdowns after updates\n (Open Cloud `:restartServers` or \"Restart servers for updates\") teleport players to fresh servers.\n- Server browsers: each server heartbeats `{ jobId, players, mode }` into a MemoryStore sorted map\n (TTL) — see `roblox-data-stores`.\n\n## Related skills\n\n`roblox-data-stores` (session locking, MemoryStore queues), `roblox-security` (access control),\n`roblox-game-systems` (round loops), `roblox-open-cloud` (server restarts).\n",
45
+ "skills/roblox-testing/SKILL.md": "---\nname: roblox-testing\ndescription: Testing Roblox code - testable modules with dependency injection, Jest Lua (runCLI, .spec files), legacy TestEZ, running tests in Studio, via Studio MCP, run-in-roblox, or CI with Open Cloud Luau Execution, Lune for pure Luau, multi-client playtests, QA checklist. Use when adding tests, setting up a test runner or CI, or verifying a gameplay change works.\n---\n\n# Testing Roblox games\n\n## Make code testable first\n\n- Keep game rules in **pure ModuleScripts** (inputs → outputs, no services, no yields): damage formulas,\n inventory operations, shop pricing, state machines, serialization, validation. These are trivially testable.\n- Services take their dependencies as parameters (**dependency injection**) so tests can pass fakes\n instead of real DataStores, remotes, or `Players`.\n\n```luau\n--!strict\n-- ReplicatedStorage/Shared/Inventory.luau — pure logic, no Roblox services.\nexport type Inventory = { [string]: number }\n\nlocal Inventory = {}\n\nfunction Inventory.add(inventory: Inventory, itemId: string, amount: number, maxStack: number): (boolean, string?)\n\tif amount <= 0 or amount ~= math.floor(amount) then\n\t\treturn false, \"invalid amount\"\n\tend\n\tlocal current = inventory[itemId] or 0\n\tif current + amount > maxStack then\n\t\treturn false, \"stack full\"\n\tend\n\tinventory[itemId] = current + amount\n\treturn true, nil\nend\n\nreturn Inventory\n```\n\n```luau\n--!strict\n-- A service that receives its data store, so tests can inject an in-memory fake.\ntype Store = {\n\tGetAsync: (self: Store, key: string) -> any,\n\tSetAsync: (self: Store, key: string, value: any) -> (),\n}\n\nlocal function newCurrencyService(store: Store)\n\tlocal service = {}\n\tfunction service.award(userId: number, amount: number): number\n\t\tlocal key = `User_{userId}`\n\t\tlocal balance = (store:GetAsync(key) or 0) + amount\n\t\tstore:SetAsync(key, balance)\n\t\treturn balance\n\tend\n\treturn service\nend\n\n-- In tests: a fake store backed by a table.\nlocal fakeData: { [string]: any } = {}\nlocal fakeStore: Store = {\n\tGetAsync = function(_self, key)\n\t\treturn fakeData[key]\n\tend,\n\tSetAsync = function(_self, key, value)\n\t\tfakeData[key] = value\n\tend,\n}\nprint(newCurrencyService(fakeStore).award(1, 50))\n\nreturn newCurrencyService\n```\n\n## Jest Lua (recommended framework)\n\nPort of Jest (`describe`/`it`/`expect`, mocks, snapshots). TestEZ is legacy; Jest Lua 3 includes a\nmigration guide.\n\n```toml\n# wally.toml\n[dev-dependencies]\nJest = \"jsdotlua/jest@3.10.0\"\nJestGlobals = \"jsdotlua/jest-globals@3.10.0\"\n```\n\n```luau\n-- src/shared/Inventory.spec.luau\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal JestGlobals = require(ReplicatedStorage.DevPackages.JestGlobals)\nlocal Inventory = require(ReplicatedStorage.Shared.Inventory)\n\nlocal describe, it, expect = JestGlobals.describe, JestGlobals.it, JestGlobals.expect\n\ndescribe(\"Inventory.add\", function()\n\tit(\"stacks items up to the limit\", function()\n\t\tlocal inventory = {}\n\t\texpect(Inventory.add(inventory, \"Apple\", 3, 5)).toBe(true)\n\t\texpect(inventory.Apple).toBe(3)\n\t\tlocal ok, reason = Inventory.add(inventory, \"Apple\", 3, 5)\n\t\texpect(ok).toBe(false)\n\t\texpect(reason).toBe(\"stack full\")\n\tend)\n\n\tit(\"rejects fractional amounts\", function()\n\t\texpect(Inventory.add({}, \"Apple\", 0.5, 5)).toBe(false)\n\tend)\nend)\n```\n\n- Put a `jest.config.luau` (`return { testMatch = { \"**/*.spec\" } }`) in each test root.\n- Runner script (Studio command bar, a test place Script, or cloud task):\n `require(ReplicatedStorage.DevPackages.Jest).runCLI(root, { ci = true }, { root }):awaitStatus()` →\n inspect `result.results.numFailedTests`.\n- Jest Lua 3 needs the fast flag `FFlagEnableLoadModule = true` in Studio's `ClientAppSettings.json`\n when run locally.\n- Map `DevPackages` into the DataModel only for test builds (separate Rojo project, e.g. `test.project.json`).\n\n## Where tests run\n\n| Runner | Use |\n| --- | --- |\n| Studio (command bar / test Script) | Local development. |\n| Studio MCP (`execute_luau`, `start_stop_play`, `get_console_output`) | Agents running tests and playtests against the open place — see `roblox-studio-mcp`. |\n| `run-in-roblox` | CLI that opens Studio, runs a script, pipes output (needs Studio installed; local/Windows/mac CI runners). |\n| **Open Cloud Luau Execution** | Headless CI: runs a script in a real Roblox server for a published place version. |\n| Lune | Pure-Luau modules with no Roblox API dependencies (fast, runs anywhere). |\n\nOpen Cloud Luau Execution flow (details and a script: [references/cloud-test-runner.md](references/cloud-test-runner.md)):\n1. Build the test place (`rojo build test.project.json -o test.rbxl`) and upload it as a **Saved** version\n of a dedicated test place.\n2. `POST /cloud/v2/universes/{u}/places/{p}/versions/{v}/luau-execution-session-tasks` with `{ \"script\": ... }`\n (API key scope `universe.place.luau-execution-session:write`; rate limit ≈ 5 tasks/min per key owner).\n3. Poll the task `path` until `state` is `COMPLETE`/`FAILED`; read `output.results` and `.../logs`.\n\n## Playtesting in Studio\n\n- **Test** tab: Play (you + server), Run (server only, no character), **Clients and Servers** with 2+\n players to test replication, trading, and PvP. Most multiplayer bugs only appear with ≥ 2 clients.\n- Network simulation (incoming latency, jitter, packet loss) to test lag compensation.\n- Device Emulator (phones, tablets, consoles) and Controller Emulator for input/UI.\n- Streaming: test with `StreamingTargetRadius = 64`.\n- DataStores in Studio: enable API access and use a separate test universe or data store names.\n\n## QA checklist before shipping a feature\n\n- [ ] Works for a player who joins **after** the feature started (late joiners), and after respawn.\n- [ ] Works with 2+ clients; server state is authoritative; UI updates on all clients.\n- [ ] Player leaving mid-action (trade, purchase, round) leaves no broken state or leaked tables.\n- [ ] Remotes rejected when spammed or sent malformed arguments.\n- [ ] Data saves and reloads (rejoin), including teleports between places.\n- [ ] Mobile, gamepad, and keyboard all work; UI readable on a phone.\n- [ ] No errors/warnings in Output (server and client).\n\n## Related skills\n\n`roblox-tooling` (CI, Rojo), `roblox-studio-mcp` (agent-driven playtests), `roblox-open-cloud`,\n`roblox-code-review`.\n",
46
+ "skills/roblox-testing/references/cloud-test-runner.md": "# Running tests in the cloud with Open Cloud Luau Execution\n\nRuns a Luau script inside a real Roblox server for a specific place version. Good for CI of code that\nneeds the engine (instances, services, physics). Not available: players/characters, and DataStores\nbehave like a live server of that universe (use a dedicated test universe).\n\n## One-time setup\n\n1. Create a **test universe** with a test place (never run tests against production data).\n2. Creator Dashboard → Open Cloud → API Keys: create a key with\n `universe.place.luau-execution-session:write` for the test universe (plus `universe-places` write\n to upload versions). Store it as a CI secret.\n\n## Test script executed by the task\n\n```luau\n--!strict\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal Jest = require(ReplicatedStorage.DevPackages.Jest) :: any\nlocal root = ReplicatedStorage.Shared\n\nlocal status, result = Jest.runCLI(root, { ci = true, verbose = true }, { root }):awaitStatus()\nif status ~= \"Resolved\" then\n\terror(`Jest failed to run: {result}`)\nend\nlocal failed = result.results.numFailedTests + result.results.numFailedTestSuites\nif failed > 0 then\n\terror(`{failed} failing tests/suites`) -- makes the task FAILED\nend\nreturn result.results.numPassedTests\n```\n\n## Driver (bash + curl + jq)\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n: \"${ROBLOX_API_KEY:?}\" \"${UNIVERSE_ID:?}\" \"${PLACE_ID:?}\"\nAPI=https://apis.roblox.com\n\n# 1. Upload the built test place as a new saved version.\nVERSION=$(curl -sf -X POST \\\n \"$API/universes/v1/$UNIVERSE_ID/places/$PLACE_ID/versions?versionType=Saved\" \\\n -H \"x-api-key: $ROBLOX_API_KEY\" -H \"Content-Type: application/octet-stream\" \\\n --data-binary @test.rbxl | jq -r .versionNumber)\n\n# 2. Start the task.\nTASK=$(jq -n --rawfile script scripts/run-tests.luau '{script: $script, timeout: \"300s\"}' |\n curl -sf -X POST \\\n \"$API/cloud/v2/universes/$UNIVERSE_ID/places/$PLACE_ID/versions/$VERSION/luau-execution-session-tasks\" \\\n -H \"x-api-key: $ROBLOX_API_KEY\" -H \"Content-Type: application/json\" --data @- | jq -r .path)\n\n# 3. Poll until done.\nwhile true; do\n RESPONSE=$(curl -sf \"$API/cloud/v2/$TASK\" -H \"x-api-key: $ROBLOX_API_KEY\")\n STATE=$(echo \"$RESPONSE\" | jq -r .state)\n case \"$STATE\" in\n COMPLETE) echo \"$RESPONSE\" | jq .output; break ;;\n FAILED|CANCELLED) echo \"$RESPONSE\" | jq .error; FAILED=1; break ;;\n *) sleep 5 ;;\n esac\ndone\n\n# 4. Print the script's log output.\ncurl -sf \"$API/cloud/v2/$TASK/logs\" -H \"x-api-key: $ROBLOX_API_KEY\" | jq -r '.luauExecutionSessionTaskLogs[]?.messages[]?'\nexit \"${FAILED:-0}\"\n```\n\n- Tasks default to a 5-minute timeout; the rate limit is about 5 task creations per minute per API key owner.\n- Check the Open Cloud reference for the current response field names if `jq` paths return null.\n",
47
+ "skills/roblox-text-chat/SKILL.md": "---\nname: roblox-text-chat\ndescription: Chat and text safety - TextChatService (channels, SendAsync, ShouldDeliverCallback, OnIncomingMessage, commands, bubbles, custom UI), mandatory TextService filtering of any player-authored text, rate limits, privacy checks, PolicyService restrictions, legacy Chat migration. Use when adding chat features or commands, or any feature that shows one player's text to others (names, signs, notes).\n---\n\n# Chat and text safety\n\n## Non-negotiable rules (Roblox Community Standards)\n\n1. Player-to-player conversation must go through **TextChatService** `TextChannel`s (handles filtering,\n privacy/parental settings, and abuse reporting). The legacy `Chat` service/`ChatService` Lua modules\n are gone — `TextChatService` is the only chat system.\n2. **Any text authored by one player and shown to another** (pet names, signs, guild names, trade notes,\n custom UI) must be filtered with `TextService:FilterStringAsync` on the server before display — and\n re-filtered when loaded from a DataStore later.\n3. Editable text others can see (signs, bulletin boards) needs a rate limit of at least 1 minute.\n4. Respect privacy settings with `CanUserChatAsync`/`CanUsersChatAsync`/`CanUsersDirectChatAsync` and mark\n direct-message channels with `TextChannel:SetDirectChatRequester()`.\n\nNot chat (no filtering needed): your own UI text, game status messages, admin announcements you write.\n\n## Filtering player-authored text\n\n```luau\n--!strict\nlocal TextService = game:GetService(\"TextService\")\n\n-- Broadcast (e.g. a sign everyone sees). Returns nil if filtering failed: then show nothing.\nlocal function filterForEveryone(text: string, fromUserId: number): string?\n\tlocal ok, result = pcall(function()\n\t\tlocal filterResult = TextService:FilterStringAsync(text, fromUserId, Enum.TextFilterContext.PublicChat)\n\t\treturn filterResult:GetNonChatStringForBroadcastAsync()\n\tend)\n\treturn if ok then result else nil\nend\n\n-- Per-recipient (respects each viewer's age/settings; e.g. a private note).\nlocal function filterForUser(text: string, fromUserId: number, toUserId: number): string?\n\tlocal ok, result = pcall(function()\n\t\tlocal filterResult = TextService:FilterStringAsync(text, fromUserId, Enum.TextFilterContext.PrivateChat)\n\t\treturn filterResult:GetNonChatStringForUserAsync(toUserId)\n\tend)\n\treturn if ok then result else nil\nend\n\nreturn { forEveryone = filterForEveryone, forUser = filterForUser }\n```\n\n- Filter on the **server**, after the player submits (not per keystroke); rate-limit and length-cap first.\n- On failure, **fail closed** (show nothing / a placeholder), never the raw text.\n- Store the raw text if you must, but always filter at display time (filters and viewer settings change).\n- Numbers-only or short strings can still be filtered (hashtags `###`); design UI for that.\n\n## TextChatService essentials\n\n- Defaults: `CreateDefaultTextChannels` creates `RBXGeneral` and `RBXSystem`; `CreateDefaultCommands`\n adds built-ins (`/whisper`, `/team`, `/mute`, ...). Configure look via `ChatWindowConfiguration`,\n `ChatInputBarConfiguration`, `BubbleChatConfiguration` (or disable and build your own UI with\n `TextChannel:SendAsync` and `TextChatService.MessageReceived`).\n- Flow: client `TextChannel:SendAsync(text)` → server `TextChannel.ShouldDeliverCallback(message, textSource)`\n per recipient → filtering → clients' `OnIncomingMessage` callbacks → display.\n- Callbacks must **not yield**. `ShouldDeliverCallback` is server-only; `TextChatService.OnIncomingMessage`\n and `TextChannel.OnIncomingMessage` are client-only (formatting).\n- System messages: `channel:DisplaySystemMessage(text)` on the client (local only).\n- Custom channels (team, party, proximity): create `TextChannel`s under `TextChatService` on the server,\n `channel:AddUserAsync(userId)` to add members; check the returned `TextSource`'s `CanSend`.\n\n```luau\n--!strict\n-- Client: chat tags and colors via OnIncomingMessage (formatting only; never yields).\nlocal Players = game:GetService(\"Players\")\nlocal TextChatService = game:GetService(\"TextChatService\")\n\nTextChatService.OnIncomingMessage = function(message: TextChatMessage)\n\tlocal properties = Instance.new(\"TextChatMessageProperties\")\n\tlocal source = message.TextSource\n\tif source then\n\t\tlocal player = Players:GetPlayerByUserId(source.UserId)\n\t\tif player and player:GetAttribute(\"VIP\") then\n\t\t\tproperties.PrefixText = `<font color=\"#F5CD30\">[VIP]</font> {message.PrefixText}`\n\t\tend\n\tend\n\treturn properties\nend\n```\n\n```luau\n--!strict\n-- Server: a /spawn command, restricted to admins.\nlocal Players = game:GetService(\"Players\")\nlocal TextChatService = game:GetService(\"TextChatService\")\n\nlocal ADMINS: { [number]: boolean } = { [123456] = true }\n\nlocal command = Instance.new(\"TextChatCommand\")\ncommand.Name = \"SpawnCommand\"\ncommand.PrimaryAlias = \"/spawn\"\ncommand.Parent = TextChatService\n\ncommand.Triggered:Connect(function(source: TextSource, unfilteredText: string)\n\tif not ADMINS[source.UserId] then\n\t\treturn\n\tend\n\tlocal player = Players:GetPlayerByUserId(source.UserId)\n\tprint(`{player and player.Name} ran: {unfilteredText}`)\nend)\n```\n\nMore: proximity chat, team channels, custom UI, and migrating from legacy chat:\n[references/chat-recipes.md](references/chat-recipes.md).\n\n## PolicyService (per-user restrictions)\n\n`PolicyService:GetPolicyInfoForPlayerAsync(player)` (server, `pcall`) returns flags such as\n`ArePaidRandomItemsRestricted`, `IsPaidItemTradingAllowed`, `AreAdsAllowed`, `AllowedExternalLinkReferences`,\n`IsSubjectToChinaPolicies`, `IsContentSharingAllowed`. Gate loot boxes, trading, ads, and links to\nsocial media (only show platforms listed in `AllowedExternalLinkReferences`) per player.\n\n## Related skills\n\n`roblox-security` (rate limits, validation), `roblox-ui` (text input UI), `roblox-monetization`\n(paid random items), `roblox-data-stores` (storing user text).\n",
48
+ "skills/roblox-text-chat/references/chat-recipes.md": "# Chat recipes\n\n## Proximity chat (server)\n\n```luau\n--!strict\nlocal Players = game:GetService(\"Players\")\nlocal TextChatService = game:GetService(\"TextChatService\")\n\nlocal RANGE = 50\nlocal channels = TextChatService:WaitForChild(\"TextChannels\")\nlocal general = channels:WaitForChild(\"RBXGeneral\") :: TextChannel\n\nlocal function positionOf(userId: number): Vector3?\n\tlocal player = Players:GetPlayerByUserId(userId)\n\tlocal character = player and player.Character\n\treturn character and character:GetPivot().Position\nend\n\ngeneral.ShouldDeliverCallback = function(message: TextChatMessage, target: TextSource): boolean\n\tlocal source = message.TextSource\n\tif not source or source.UserId == target.UserId then\n\t\treturn true -- always show senders their own message\n\tend\n\tlocal from, to = positionOf(source.UserId), positionOf(target.UserId)\n\treturn from ~= nil and to ~= nil and (from - to).Magnitude <= RANGE\nend\n```\n\n## Team or party channel (server)\n\n```luau\n--!strict\nlocal TextChatService = game:GetService(\"TextChatService\")\n\nlocal function createPartyChannel(partyId: string, members: { Player }): TextChannel\n\tlocal channel = Instance.new(\"TextChannel\")\n\tchannel.Name = `Party_{partyId}`\n\tchannel.Parent = TextChatService:WaitForChild(\"TextChannels\")\n\tfor _, member in members do\n\t\tlocal ok, source = pcall(function()\n\t\t\treturn channel:AddUserAsync(member.UserId)\n\t\tend)\n\t\tif not ok or not source then\n\t\t\twarn(`could not add {member.Name}`)\n\t\tend\n\tend\n\treturn channel -- clients send with channel:SendAsync(text); Destroy() when the party ends\nend\n\nreturn createPartyChannel\n```\n\nTeams: the built-in `/team` command and team channels exist when `CreateDefaultCommands`/\n`CreateDefaultTextChannels` are on and `Teams` are set up.\n\n## Rate-limited bulletin board (server)\n\n```luau\n--!strict\nlocal Players = game:GetService(\"Players\")\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal TextService = game:GetService(\"TextService\")\n\nlocal COOLDOWN = 60 -- at least 1 minute for player-editable public text\nlocal MAX_LENGTH = 140\n\nlocal postNote = Instance.new(\"RemoteEvent\")\npostNote.Name = \"PostNote\"\npostNote.Parent = ReplicatedStorage\n\nlocal lastPost: { [Player]: number } = {}\n\npostNote.OnServerEvent:Connect(function(player, text: unknown)\n\tif typeof(text) ~= \"string\" or #text == 0 or #text > MAX_LENGTH or utf8.len(text) == nil then\n\t\treturn\n\tend\n\tlocal now = os.clock()\n\tif lastPost[player] and now - lastPost[player] < COOLDOWN then\n\t\treturn\n\tend\n\tlastPost[player] = now\n\tlocal ok, filtered = pcall(function()\n\t\treturn TextService:FilterStringAsync(text, player.UserId):GetNonChatStringForBroadcastAsync()\n\tend)\n\tif ok then\n\t\tpostNote:FireAllClients(player.UserId, filtered)\n\tend\nend)\n\nPlayers.PlayerRemoving:Connect(function(player)\n\tlastPost[player] = nil\nend)\n```\n\n## Custom chat UI\n\nSet `ChatWindowConfiguration.Enabled = false` and `ChatInputBarConfiguration.Enabled = false`, then:\n- Send: `(TextChatService.TextChannels.RBXGeneral :: TextChannel):SendAsync(textBox.Text)`.\n- Receive: `TextChatService.MessageReceived:Connect(function(message) ... end)`; render\n `message.PrefixText` and `message.Text` (already filtered). Use `message.MessageId` to replace the\n sender's local pending message when the server-processed version arrives.\n- Keep bubble chat via `BubbleChatConfiguration` or `TextChatService.OnBubbleAdded`.\n\n## Migrating from legacy Chat\n\n| Legacy | TextChatService |\n| --- | --- |\n| `Chat:Chat(part, text)` bubbles | `TextChatService:DisplayBubble(part, text)` (client) |\n| ChatService speakers/channels | `TextChannel` + `AddUserAsync`; permissions via `TextSource.CanSend` |\n| Extra data (tags, colors) on speakers | `OnIncomingMessage` returning `TextChatMessageProperties` (`PrefixText`, `Text`) with rich text |\n| `Chat:FilterStringAsync` | `TextService:FilterStringAsync` |\n| Custom commands via message hooks | `TextChatCommand` instances (`PrimaryAlias`, `SecondaryAlias`, `Triggered`) |\n",
49
+ "skills/roblox-tooling/SKILL.md": "---\nname: roblox-tooling\ndescription: Professional Roblox toolchain - Studio Script Sync vs Rojo, Rokit, Wally/pesde packages, luau-lsp with sourcemaps, StyLua, selene, darklua, Lune, roblox-ts, .luaurc, git, GitHub Actions CI and Open Cloud publishing. Use when creating a Roblox repo, configuring VS Code/Cursor, adding packages, fixing sourcemap/require typing, or automating builds and deploys.\n---\n\n# Roblox tooling\n\n## Pick a sync model\n\n| Need | Use |\n| --- | --- |\n| Edit scripts in an external editor / git for code only; keep Studio as source of truth; Team Create | **Studio Script Sync** (built in: right-click a folder → **Sync to…**) |\n| Whole project as files, file system is the source of truth, CI builds, packages | **Rojo** (`rojo serve` + Studio plugin) |\n\nScript Sync syncs only `Script`/`LocalScript`/`ModuleScript`/`Folder` (≤ 10,000 scripts per synced\nroot, ≤ 128 roots); don't sync scripts that carry attributes/tags (they're not written to disk).\nPair either with the **Luau Language Server** VS Code extension + its **Luau Language Server Companion**\nStudio plugin (sends the DataModel tree so requires and instance paths type-check).\n\n## Rokit: pinned toolchain\n\n```toml\n# rokit.toml (commit it; run `rokit install`)\n[tools]\nrojo = \"rojo-rbx/rojo@7.7.0\"\nwally = \"UpliftGames/wally@0.3.2\"\nlune = \"lune-org/lune@0.10.5\"\nstylua = \"JohnnyMorganz/StyLua@2.5.2\"\nselene = \"Kampfkarren/selene@0.31.0\"\nluau-lsp = \"JohnnyMorganz/luau-lsp@1.70.1\"\ndarklua = \"seaofvoices/darklua@0.19.0\"\n```\n\nInstall Rokit: `curl -sSf https://raw.githubusercontent.com/rojo-rbx/rokit/main/scripts/install.sh | bash`\n(Windows: `install.ps1`). Add tools with `rokit add owner/repo`. Versions above are current as of\nSeptember 2026; bump deliberately. (Aftman and Foreman are older equivalents.)\n\n## Rojo project\n\n```json\n{\n \"name\": \"my-game\",\n \"tree\": {\n \"$className\": \"DataModel\",\n \"ReplicatedStorage\": {\n \"Shared\": { \"$path\": \"src/shared\" },\n \"Packages\": { \"$path\": \"Packages\" }\n },\n \"ServerScriptService\": {\n \"Server\": { \"$path\": \"src/server\" },\n \"ServerPackages\": { \"$path\": \"ServerPackages\" }\n },\n \"StarterPlayer\": {\n \"StarterPlayerScripts\": {\n \"Client\": { \"$path\": \"src/client\" }\n }\n },\n \"Workspace\": {\n \"$properties\": { \"StreamingEnabled\": true }\n }\n }\n}\n```\n\nFile mapping: `Foo.luau` → ModuleScript, `Foo.server.luau` → Script, `Foo.client.luau` → LocalScript,\nfolder with `init.luau`/`init.server.luau`/`init.client.luau` → that script with children,\n`*.meta.json` → properties/attributes, `*.model.json`/`.rbxm`/`.rbxmx` → instances, `*.json`/`*.toml`/`*.txt`\n→ ModuleScript returning data / StringValue. Commands: `rojo serve`, `rojo build -o game.rbxl`,\n`rojo sourcemap default.project.json -o sourcemap.json` (for luau-lsp), `rojo upload` / Open Cloud for\npublishing. Keep maps/art in Studio (or `.rbxm` files) and code in Rojo — partially managed trees are fine.\n\n## Packages\n\n- **Wally** (`wally.toml`, `wally install` → `Packages/`, `ServerPackages/` for `realm = \"server\"`):\n ```toml\n [dependencies]\n React = \"jsdotlua/react@17.2.1\"\n ReactRoblox = \"jsdotlua/react-roblox@17.2.1\"\n [server-dependencies]\n ProfileStore = \"lm-loleris/profilestore@1.0.3\"\n ```\n Wally thunks lose types: run `wally-package-types --sourcemap sourcemap.json Packages/` after install.\n Check each package's page on wally.run for its exact name/version.\n- **pesde** (`pesde.toml`): newer manager for Roblox/Lune/Luau that can also depend on Wally packages.\n- Commit `wally.lock`/`pesde.lock`; don't commit `Packages/`.\n\n## Code quality\n\n- **luau-lsp** (editor + CLI) — type checking against Roblox definitions. CLI:\n `luau-lsp analyze --definitions=@roblox=globalTypes.d.luau --sourcemap=sourcemap.json --base-luaurc=.luaurc src/`\n (definitions: `https://raw.githubusercontent.com/JohnnyMorganz/luau-lsp/main/scripts/globalTypes.d.luau`).\n Enable the new solver in the editor (`luau-lsp.fflags.enableNewSolver`) to match Studio.\n- **StyLua** — formatter. `stylua.toml`: `indent_type = \"Tabs\"`, `column_width = 120`. `stylua --check src`.\n- **selene** — linter. `selene.toml`: `std = \"roblox\"` (generate/refresh with `selene generate-roblox-std`).\n- **darklua** — code transforms (e.g. convert string requires, bundle) for publishing pipelines.\n- `.luaurc` — per-directory type-check mode and require aliases:\n ```json\n { \"languageMode\": \"strict\", \"aliases\": { \"Shared\": \"src/shared\", \"Packages\": \"Packages\" } }\n ```\n Roblox resolves string requires relative to the DataModel (`./`, `../`, `@self`, `@game`); custom\n `.luaurc` aliases need tooling (luau-lsp/darklua) that understands your mapping.\n\n## Lune\n\nStandalone Luau runtime for scripts outside Roblox: build steps, place file manipulation\n(`@lune/roblox` reads/writes `.rbxl`/`.rbxm`), asset pipelines, running pure-Luau unit tests, calling\nOpen Cloud with `@lune/net`. `lune run scripts/build`.\n\n## roblox-ts\n\nIf the repo has `tsconfig.json`, `package.json` with `roblox-ts` and `@rbxts/types`, write TypeScript:\n`npx rbxtsc -w` compiles `src/*.ts` to `out/*.luau`, and Rojo syncs `out/`. Use `@rbxts/services`,\n`@rbxts/*` packages from npm. Don't hand-edit generated Luau.\n\n## Git and CI\n\n`.gitignore`: `*.rbxl.lock`, `*.rbxlx.lock`, `Packages/`, `ServerPackages/`, `sourcemap.json`, build outputs.\nA ready-to-use GitHub Actions workflow (install tools, format check, lint, type-check, build, publish via\nOpen Cloud): [references/ci-workflow.md](references/ci-workflow.md).\n\n## Related skills\n\n`roblox-testing` (running tests in CI), `roblox-open-cloud` (publishing, Luau execution),\n`roblox-studio-mcp` (driving Studio from an agent), `roblox-luau`.\n",
50
+ "skills/roblox-tooling/references/ci-workflow.md": "# GitHub Actions CI for a Rojo project\n\nAssumes `rokit.toml` (with rojo, wally, stylua, selene, luau-lsp), `default.project.json`, `wally.toml`,\n`selene.toml`, `stylua.toml`, and optionally `.luaurc`.\n\n```yaml\n# .github/workflows/ci.yml\nname: CI\non:\n push:\n branches: [main]\n pull_request:\n\njobs:\n check:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - name: Install Rokit and tools\n run: |\n curl -sSf https://raw.githubusercontent.com/rojo-rbx/rokit/main/scripts/install.sh | bash\n echo \"$HOME/.rokit/bin\" >> \"$GITHUB_PATH\"\n \"$HOME/.rokit/bin/rokit\" authenticate github --token \"$GITHUB_PAT\" # avoid API rate limits\n \"$HOME/.rokit/bin/rokit\" install --no-trust-check\n env:\n GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}\n\n - name: Install packages\n run: |\n wally install\n rojo sourcemap default.project.json -o sourcemap.json\n\n - name: Format\n run: stylua --check src\n\n - name: Lint\n run: selene src\n\n - name: Type check\n run: |\n curl -fsSL -o globalTypes.d.luau \\\n https://raw.githubusercontent.com/JohnnyMorganz/luau-lsp/main/scripts/globalTypes.d.luau\n luau-lsp analyze \\\n --definitions=@roblox=globalTypes.d.luau \\\n --sourcemap=sourcemap.json \\\n --base-luaurc=.luaurc \\\n --ignore=\"Packages/**\" \\\n src\n\n - name: Build place\n run: rojo build default.project.json -o build.rbxl\n\n - uses: actions/upload-artifact@v4\n with:\n name: place\n path: build.rbxl\n```\n\nNotes:\n- Rokit's installer path and flags may change between versions; check its README if the step fails.\n- Cache `~/.rokit` and `Packages/` with `actions/cache` keyed on `rokit.toml`/`wally.lock` for speed.\n\n## Publishing with Open Cloud\n\nCreate an API key (Creator Dashboard → Open Cloud → API Keys) with the **universe-places** permission (write) for the\ntarget universe, store it as a repository secret `ROBLOX_API_KEY`, then:\n\n```yaml\n publish:\n needs: check\n if: github.ref == 'refs/heads/main'\n runs-on: ubuntu-latest\n steps:\n - uses: actions/download-artifact@v4\n with:\n name: place\n - name: Publish place version\n run: |\n curl --fail -X POST \\\n \"https://apis.roblox.com/universes/v1/${{ vars.UNIVERSE_ID }}/places/${{ vars.PLACE_ID }}/versions?versionType=Published\" \\\n -H \"x-api-key: ${{ secrets.ROBLOX_API_KEY }}\" \\\n -H \"Content-Type: application/octet-stream\" \\\n --data-binary @build.rbxl\n```\n\nUse `versionType=Saved` to upload without publishing (e.g. to a staging place). Publish to a separate\n**staging universe** first and run tests there (see `roblox-testing` for running tests in the cloud\nwith Open Cloud Luau execution).\n",
51
+ "skills/roblox-ui/SKILL.md": "---\nname: roblox-ui\ndescription: Responsive cross-platform UI - ScreenGui and safe areas (ScreenInsets), Scale vs Offset, UIListLayout flex/UIFlexItem, constraints, StyleSheets/tokens/themes, RichText, gamepad/touch navigation, UIDragDetector, CanvasGroup, tweens, BillboardGui/SurfaceGui, React-lua/Fusion/Vide. Use when building HUDs, menus, shops, inventories, or fixing UI that breaks on mobile, console, or other screen sizes.\n---\n\n# Roblox UI\n\n## Foundations\n\n- Put UI in `StarterGui` as `ScreenGui`s (copied to each player's `PlayerGui`), or create it from a\n client controller. UI code is client-only; the server never touches `PlayerGui` in normal designs.\n- `ScreenGui` defaults to set: `ResetOnSpawn = false` (keep UI across deaths), `ZIndexBehavior = Sibling`,\n `IgnoreGuiInset` per design, `ScreenInsets = CoreUISafeInsets` for interactive UI (clears notches and\n the Roblox top bar); `DeviceSafeInsets`/`None` only for full-bleed backgrounds.\n- **Size and position with Scale** (`UDim2.fromScale`) plus constraints; use Offset only for fixed\n details (padding, strokes, icons). Pure-Offset UI breaks on phones and 4K screens.\n- `AnchorPoint = Vector2.new(0.5, 0.5)` + `Position = UDim2.fromScale(0.5, 0.5)` centers an element.\n- `UIAspectRatioConstraint` keeps shapes square/consistent; `UISizeConstraint` caps sizes;\n `UITextSizeConstraint` with `TextScaled` bounds text size (prefer fixed `TextSize` + `AutomaticSize`\n where possible — `TextScaled` produces inconsistent sizes).\n- `AutomaticSize` lets frames grow with content (lists, tooltips, chat bubbles).\n- Test every screen in the **Device Emulator** (phone portrait/landscape, tablet, console 10-foot, 4K).\n\n## Layout\n\n| Need | Use |\n| --- | --- |\n| Rows/columns that adapt | `UIListLayout` (`FillDirection`, `Padding`, `HorizontalFlex`/`VerticalFlex`, `Wraps`) |\n| One item grows to fill space | `UIFlexItem` child with `FlexMode = Fill` (or `Grow`/`Shrink`/`Custom`) |\n| Grids (inventory) | `UIGridLayout` inside a `ScrollingFrame` with `AutomaticCanvasSize` |\n| Carousels/pages | `UIPageLayout` |\n| Rounded corners, borders, padding | `UICorner`, `UIStroke`, `UIPadding` |\n| Gradients, shadows | `UIGradient`, `UIShadow` |\n| Scale a whole group | `UIScale` |\n\n```luau\n--!strict\nlocal Players = game:GetService(\"Players\")\n\nlocal playerGui = Players.LocalPlayer:WaitForChild(\"PlayerGui\")\n\nlocal screenGui = Instance.new(\"ScreenGui\")\nscreenGui.Name = \"Hud\"\nscreenGui.ResetOnSpawn = false\nscreenGui.ScreenInsets = Enum.ScreenInsets.CoreUISafeInsets\n\nlocal bar = Instance.new(\"Frame\")\nbar.Name = \"Toolbar\"\nbar.AnchorPoint = Vector2.new(0.5, 1)\nbar.Position = UDim2.new(0.5, 0, 1, -16)\nbar.Size = UDim2.fromScale(0.6, 0.1)\nbar.BackgroundTransparency = 0.3\nbar.Parent = screenGui\n\nlocal list = Instance.new(\"UIListLayout\")\nlist.FillDirection = Enum.FillDirection.Horizontal\nlist.HorizontalFlex = Enum.UIFlexAlignment.Fill -- slots share the width equally\nlist.Padding = UDim.new(0, 8)\nlist.Parent = bar\n\nInstance.new(\"UICorner\").Parent = bar\nlocal padding = Instance.new(\"UIPadding\")\npadding.PaddingLeft, padding.PaddingRight = UDim.new(0, 8), UDim.new(0, 8)\npadding.Parent = bar\n\nfor slot = 1, 5 do\n\tlocal button = Instance.new(\"TextButton\")\n\tbutton.Name = `Slot{slot}`\n\tbutton.Text = tostring(slot)\n\tbutton.Size = UDim2.fromScale(0, 1)\n\tbutton.Parent = bar\n\tInstance.new(\"UIAspectRatioConstraint\").Parent = button\nend\n\nscreenGui.Parent = playerGui -- parent last: one layout pass, no flicker\n```\n\n## Styling: StyleSheets (CSS-like)\n\nRoblox's engine-level styling replaces copy-pasted property values:\n- `StyleSheet` with `StyleRule`s (`Selector` like `\"TextButton\"`, `\".Primary\"` (tag), `\"#Close\"` (name),\n `\"TextButton:Hover\"` (state), `\"::UICorner\"` (modifier pseudo-instance)) and `rule:SetProperties({...})`.\n- **Tokens** are attributes on a token sheet (`$Primary`, `$Radius`); **themes** are sheets deriving tokens\n (`StyleDerive`); swap themes by retargeting the `StyleDerive`.\n- A `StyleLink` under a `ScreenGui` applies one sheet to that tree. **Style queries** (`@Name`) switch\n styles by screen size, input type, or accessibility settings.\n- Edit visually in Studio's **Style Editor**. Details and a code example: [references/styling-and-frameworks.md](references/styling-and-frameworks.md).\n\n## Cross-platform input in UI\n\n- Read `UserInputService.PreferredInput` (`KeyboardAndMouse`, `Touch`, `Gamepad`) and listen to\n `UserInputService:GetPropertyChangedSignal(\"PreferredInput\")` to swap button prompts and layouts.\n Don't infer platform from `TouchEnabled`/`KeyboardEnabled` alone.\n- Use `GuiButton.Activated` (works for mouse, touch, and gamepad) instead of `MouseButton1Click`.\n- Gamepad/console: every interactive element `Selectable`, set `GuiService.SelectedObject` when a menu\n opens, group with `SelectionGroup`/`NextSelection*` for predictable D-pad navigation, support `ButtonB`\n to close. Minimum touch target ≈ 44×44 px; readable text ≥ 14 px on phones.\n- Bind actions through the **Input Action System** (`InputBinding.UIButton` makes an on-screen button\n trigger an action) — see `roblox-input`.\n\n## Text\n\n- `RichText = true` enables `<b>`, `<i>`, `<font color=\"#FF0\">`, `<stroke>`, etc. Escape user text.\n- **Any text from one player shown to another must be filtered** with `TextService:FilterStringAsync`\n on the server (`roblox-text-chat`). That includes pet names, signs, guild names, trade notes.\n- Use `FontFace = Font.new(\"rbxasset://fonts/families/GothamSSm.json\", Enum.FontWeight.Bold)` for weights.\n- Localize with `LocalizationTable`s and auto-translation; avoid baking text into images.\n\n## Animation and feel\n\n- Tween on the client: `TweenService:Create(frame, TweenInfo.new(0.2, Enum.EasingStyle.Quad), { Position = ... }):Play()`.\n- Animate `UIScale.Scale` for pop effects rather than `Size` (keeps layout stable).\n- `CanvasGroup` fades a whole subtree (`GroupTransparency`) but renders to a texture: use it for\n fading panels, not for large always-visible UI (memory/quality cost).\n- Drag-and-drop: `UIDragDetector` (the deprecated `GuiObject.Draggable` no longer works for new work).\n\n## World-space UI\n\n- `BillboardGui` (faces camera: names, health bars) — set `MaxDistance`, `AlwaysOnTop` sparingly,\n use `Size` in Scale for distance scaling; `SurfaceGui` for screens/signs on parts. Prefer\n `Adornee` + keep them in `PlayerGui` for per-player content. They stop rendering when their part streams out.\n- `ProximityPrompt` gives cross-platform interaction UI for free (validate on the server!).\n\n## Frameworks\n\nFor more than a few screens, a declarative framework keeps state and UI in sync:\n- **React** (`jsdotlua/react` + `react-roblox`) — components + hooks, familiar to web devs, scales to big apps.\n- **Fusion** — reactive state objects and declarative instance creation.\n- **Vide** — small, fast reactive library.\nMatch whatever the project already uses. Keep game state in controllers; UI only renders it and\nemits user intents.\n\n## Performance\n\n- Avoid updating text/properties every frame when values haven't changed.\n- Hide closed menus with `Visible = false` (or `Enabled = false` on the ScreenGui) instead of\n destroying/recreating them repeatedly; build large lists lazily or virtualize them.\n- Too many `UIGradient`/`CanvasGroup`/`UIStroke` on dynamic UI costs frame time; check the MicroProfiler.\n\n## Related skills\n\n`roblox-input` (actions, gamepad), `roblox-text-chat` (filtering), `roblox-monetization` (shops),\n`roblox-luau`, `roblox-performance`.\n",
52
+ "skills/roblox-ui/references/styling-and-frameworks.md": "# UI styling and declarative frameworks\n\n## StyleSheet selectors\n\n| Selector | Matches | Example |\n| --- | --- | --- |\n| `Class` | GuiObject/UIComponent class | `\"TextButton\"`, `\"UICorner\"` |\n| `.Tag` | CollectionService tag | `\".Primary\"` |\n| `#Name` | Instance name | `\"#CloseButton\"` |\n| `:State` | `Enum.GuiState` (`Hover`, `Press`, `NonInteractable`, `Idle`) | `\"TextButton:Hover\"` |\n| `@Query` | Active `StyleQuery`, or built-ins: `@PreferredInput{Gamepad,KeyboardAndMouse,Touch}`, `@ViewportDisplaySize{Small,Medium,Large}`, `@PreferredTextSize{Medium,Large,Larger,Largest}`, `@ReducedMotionEnabled{True,False}` | `\"@ViewportDisplaySizeSmall\"` |\n| `::Class` | Pseudo-instance modifier (adds e.g. a UICorner without inserting one) | `\"TextButton::UICorner\"` |\n| `A > B`, `A B` | Child / descendant combinators | `\".Container > ImageLabel\"` |\n\nProperty values can reference tokens with `$TokenName`. One `StyleSheet` applies per tree via a `StyleLink`.\n\n## Tokens, theme, and rules in code\n\n```luau\n--!strict\nlocal Players = game:GetService(\"Players\")\n\nlocal screenGui = Instance.new(\"ScreenGui\")\nscreenGui.ResetOnSpawn = false\n\n-- Design tokens live as attributes.\nlocal tokens = Instance.new(\"StyleSheet\")\ntokens.Name = \"Tokens\"\ntokens:SetAttribute(\"Primary\", Color3.fromHex(\"3B82F6\"))\ntokens:SetAttribute(\"PrimaryHover\", Color3.fromHex(\"60A5FA\"))\ntokens:SetAttribute(\"Radius\", UDim.new(0, 8))\n\n-- The sheet linked to the UI derives the tokens.\nlocal sheet = Instance.new(\"StyleSheet\")\nsheet.Name = \"AppStyle\"\nlocal derive = Instance.new(\"StyleDerive\")\nderive.StyleSheet = tokens\nderive.Parent = sheet\n\nlocal button = Instance.new(\"StyleRule\")\nbutton.Selector = \".Primary\"\nbutton:SetProperties({\n\tBackgroundColor3 = \"$Primary\",\n\tTextColor3 = Color3.new(1, 1, 1),\n\tAutoButtonColor = false,\n})\nbutton.Parent = sheet\n\nlocal hover = Instance.new(\"StyleRule\")\nhover.Selector = \".Primary:Hover\"\nhover:SetProperty(\"BackgroundColor3\", \"$PrimaryHover\")\nhover.Parent = sheet\n\nlocal corner = Instance.new(\"StyleRule\")\ncorner.Selector = \".Primary::UICorner\"\ncorner:SetProperty(\"CornerRadius\", \"$Radius\")\ncorner.Parent = sheet\n\nlocal link = Instance.new(\"StyleLink\")\nlink.StyleSheet = sheet\nlink.Parent = screenGui\n\nlocal buy = Instance.new(\"TextButton\")\nbuy.Text = \"Buy\"\nbuy.Size = UDim2.fromOffset(160, 48)\nbuy:AddTag(\"Primary\")\nbuy.Parent = screenGui\n\ntokens.Parent = screenGui\nsheet.Parent = screenGui\nscreenGui.Parent = Players.LocalPlayer:WaitForChild(\"PlayerGui\")\n```\n\nThemes: create `LightTheme`/`DarkTheme` sheets that define the same token names, and point the\napp sheet's `StyleDerive.StyleSheet` at the active theme to switch at runtime.\n\n## React (jsdotlua/react)\n\n```luau\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal Players = game:GetService(\"Players\")\n\nlocal React = require(ReplicatedStorage.Packages.React)\nlocal ReactRoblox = require(ReplicatedStorage.Packages.ReactRoblox)\n\nlocal function Counter(props: { label: string })\n\tlocal count, setCount = React.useState(0)\n\treturn React.createElement(\"TextButton\", {\n\t\tSize = UDim2.fromOffset(200, 50),\n\t\tText = `{props.label}: {count}`,\n\t\t[React.Event.Activated] = function()\n\t\t\tsetCount(count + 1)\n\t\tend,\n\t})\nend\n\nlocal container = Instance.new(\"ScreenGui\")\ncontainer.Parent = Players.LocalPlayer:WaitForChild(\"PlayerGui\")\nlocal root = ReactRoblox.createRoot(container)\nroot:render(React.createElement(Counter, { label = \"Clicks\" }))\n```\n\n- Keep game state outside components (a store/controller); pass data down, emit intents up.\n- Use `React.useEffect` cleanup to disconnect Roblox signals.\n\n## Fusion (0.3 style)\n\n```luau\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal Fusion = require(ReplicatedStorage.Packages.Fusion)\n\nlocal scope = Fusion.scoped(Fusion)\nlocal count = scope:Value(0)\n\nlocal button = scope:New(\"TextButton\")({\n\tSize = UDim2.fromOffset(200, 50),\n\tText = scope:Computed(function(use)\n\t\treturn `Clicks: {use(count)}`\n\tend),\n\t[Fusion.OnEvent(\"Activated\")] = function()\n\t\tcount:set(Fusion.peek(count) + 1)\n\tend,\n})\n\n-- scope:doCleanup() destroys everything created in this scope.\n```\n\nAPIs differ between major versions of these libraries: check the installed version's docs.\n",
53
+ "skills/roblox-world-building/SKILL.md": "---\nname: roblox-world-building\ndescription: Building worlds with code - parts, models and pivots, materials, Lighting (LightingStyle, Atmosphere, post-processing), day/night, Terrain API, in-game CSG and destruction (GeometryService Subtract/Union/Fragment), ProceduralModel, EditableMesh/EditableImage, particles/beams/trails/highlights. Use when creating maps, environments, lighting, destructible objects, procedural levels, or visual effects.\n---\n\n# World building with code\n\n## Parts and models\n\n```luau\n--!strict\nlocal function makePart(size: Vector3, cframe: CFrame, props: { [string]: any }?): Part\n\tlocal part = Instance.new(\"Part\")\n\tpart.Anchored = true\n\tpart.Size = size\n\tpart.CFrame = cframe\n\tpart.TopSurface = Enum.SurfaceType.Smooth\n\tpart.BottomSurface = Enum.SurfaceType.Smooth\n\tfor key, value in props or {} do\n\t\t(part :: any)[key] = value\n\tend\n\treturn part -- caller sets Parent last\nend\n\nlocal floor = makePart(Vector3.new(64, 1, 64), CFrame.new(0, 0, 0), {\n\tMaterial = Enum.Material.Slate,\n\tColor = Color3.fromRGB(90, 90, 100),\n})\nlocal model = Instance.new(\"Model\")\nmodel.Name = \"Arena\"\nfloor.Parent = model\nmodel.WorldPivot = floor.CFrame\nmodel.Parent = workspace\nmodel:PivotTo(CFrame.new(0, 10, 0) * CFrame.Angles(0, math.rad(45), 0))\n```\n\n- Set properties first, **parent last** (one replication/physics insert). Anchor static geometry.\n- Move/rotate models with `PivotTo`/`GetPivot`; `Model.WorldPivot` / `PrimaryPart` define the pivot.\n- Group logically related parts in Models (streaming, LOD, `ModelStreamingMode`), not giant containers.\n- Decorative parts: `CanCollide/CanTouch/CanQuery = false`, `CastShadow = false` for tiny details.\n- Materials: `Enum.Material.*`; custom looks via `MaterialVariant` in `MaterialService` (set\n `part.MaterialVariant = \"Name\"`) or `SurfaceAppearance` on MeshParts (PBR). `Decal.Texture` is\n superseded by `ColorMapContent`.\n- Many identical meshes are cheap (instanced); many unique meshes/unions are not.\n\n## Lighting\n\n| Property | Guidance |\n| --- | --- |\n| `Lighting.LightingStyle` | `Realistic` (naturalistic) or `Soft` (stylized). Replaces the superseded `Lighting.Technology`. |\n| `Lighting.PrioritizeLightingQuality` | Prefer quality (e.g. shadow maps with Soft) vs performance on lower-end devices. |\n| `ClockTime` / `GeographicLatitude` | Sun position; animate `ClockTime` for day/night. |\n| `Brightness`, `ExposureCompensation`, `Ambient`/`OutdoorAmbient` | Overall exposure and fill light. |\n| `EnvironmentDiffuseScale`/`EnvironmentSpecularScale` | Skybox contribution to lighting and reflections. |\n| Children | `Atmosphere` (haze/density, replaces fog), `Sky`, `Clouds` (in Terrain), `BloomEffect`, `ColorCorrectionEffect`, `ColorGradingEffect`, `DepthOfFieldEffect`, `SunRaysEffect`, `BlurEffect`. |\n\n```luau\n--!strict\n-- Server: smooth day/night cycle (one full day every 20 minutes).\nlocal Lighting = game:GetService(\"Lighting\")\nlocal RunService = game:GetService(\"RunService\")\n\nlocal DAY_LENGTH_SECONDS = 20 * 60\n\nRunService.Heartbeat:Connect(function(deltaTime)\n\tLighting.ClockTime = (Lighting.ClockTime + deltaTime * 24 / DAY_LENGTH_SECONDS) % 24\nend)\n```\n\nFor per-frame visual changes, prefer computing `ClockTime` on each client from\n`workspace:GetServerTimeNow()` (no replication traffic, perfectly smooth). Local lights: `PointLight`,\n`SpotLight`, `SurfaceLight` — few with `Shadows = true`.\n\n## Terrain\n\n```luau\n--!strict\nlocal terrain = workspace.Terrain\n\nterrain:FillBlock(CFrame.new(0, -10, 0), Vector3.new(512, 20, 512), Enum.Material.Grass)\nterrain:FillBall(Vector3.new(0, 0, 0), 24, Enum.Material.Air) -- carve a crater\nterrain:FillBlock(CFrame.new(40, -2, 0), Vector3.new(30, 4, 30), Enum.Material.Water)\nterrain:FillCylinder(CFrame.new(-40, 5, 0), 20, 8, Enum.Material.Rock)\n```\n\n- Bulk generation: build a `Region3` (aligned with `:ExpandToGrid(4)`) and use `ReadVoxels`/`WriteVoxels`\n with materials/occupancy arrays (4-stud voxels), in chunks, yielding between chunks.\n- `terrain:Clear()`; water look via `Terrain.WaterColor`, `WaterWaveSize`, etc.\n- Procedural heightmaps: sample `math.noise(x * scale, z * scale, seed)` with octaves; use a `Random.new(seed)`\n for deterministic worlds; generate on the server or use Parallel Luau for heavy math.\n\n## In-game solid modeling and destruction (GeometryService)\n\n```luau\n--!strict\nlocal GeometryService = game:GetService(\"GeometryService\")\n\n-- Punch a hole through a wall (e.g. explosions, building games).\nlocal function carve(wall: BasePart, cutter: BasePart): { Instance }?\n\tlocal ok, results = pcall(function()\n\t\treturn GeometryService:SubtractAsync(wall, { cutter }, {\n\t\t\tCollisionFidelity = Enum.CollisionFidelity.Default,\n\t\t\tRenderFidelity = Enum.RenderFidelity.Automatic,\n\t\t\tSplitApart = true, -- disconnected pieces become separate parts\n\t\t})\n\tend)\n\tif not ok or not results then\n\t\treturn nil\n\tend\n\tfor _, piece in results do\n\t\tif piece:IsA(\"BasePart\") then\n\t\t\tpiece.Anchored = wall.Anchored\n\t\t\tpiece.Parent = wall.Parent\n\t\tend\n\tend\n\twall:Destroy()\n\treturn results\nend\n\nreturn carve\n```\n\n- `UnionAsync`, `IntersectAsync`, `SubtractAsync` return new unparented parts; `FragmentAsync(part, sites)`\n shatters a part (Voronoi) using points from `GenerateFragmentSites`; `SweepPartAsync` extrudes a part\n along CFrames; `CalculateConstraintsToPreserve` keeps welds/constraints when replacing parts.\n- Operations are expensive: run on the server, rate-limit, and pool/clean up fragments.\n\n## ProceduralModel (parameter-driven models)\n\n`ProceduralModel` instances regenerate their contents from a **generator ModuleScript** exposing\n`Attributes` (parameters) and `OnGenerate(params, targetContainer)`; works at edit time (undo/redo,\nTeam Create) and runtime. Generate from Studio, Assistant, or the Studio MCP (`generate_procedural_model`).\nCreator Store procedural models are sandboxed automatically.\n\n## Runtime meshes and images\n\n`AssetService:CreateEditableMesh()` / `CreateEditableMeshAsync(content)` and `CreateEditableImage` enable\nruntime geometry and pixel editing (deformable terrain chunks, painting, custom avatars). Published games\nrequire the owner to be ID + 13+ verified and **Enable Mesh / Image APIs** in Creator Dashboard; strict\nclient memory budgets apply; only assets owned by/shared with the game owner can be loaded. Display via\n`AssetService:CreateMeshPartAsync(Content.fromObject(editableMesh))`.\n\n## Effects\n\n- `ParticleEmitter` (use `:Emit(n)` for bursts), `Beam` (between attachments; lasers, ropes), `Trail`\n (swords, projectiles), `Highlight` (outline/fill; limited count), `Explosion` (sets off physics —\n set `DestroyJointRadiusPercent = 0` and `BlastPressure = 0` for visual-only).\n- Spawn cosmetic effects on clients; destroy or pool them (`Debris:AddItem(obj, seconds)` is fine).\n\n## Related skills\n\n`roblox-performance` (rendering cost, streaming), `roblox-architecture` (streaming settings),\n`roblox-studio-mcp` (build in Studio through MCP), `roblox-physics` (constraints, destruction physics).\n"
54
+ }
55
+ }