roblox-opencode 1.0.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 (248) hide show
  1. package/README.md +122 -0
  2. package/commands/setup-game.md +108 -0
  3. package/commands/sync-check.md +53 -0
  4. package/core/roblox-core.md +93 -0
  5. package/dist/server.js +167 -0
  6. package/package.json +35 -0
  7. package/skills/roblox-analytics/SKILL.md +277 -0
  8. package/skills/roblox-analytics/references/event-batcher.luau +75 -0
  9. package/skills/roblox-animation-vfx/SKILL.md +1325 -0
  10. package/skills/roblox-architecture/SKILL.md +863 -0
  11. package/skills/roblox-architecture/references/combat-systems.md +1381 -0
  12. package/skills/roblox-code-review/SKILL.md +687 -0
  13. package/skills/roblox-data/SKILL.md +889 -0
  14. package/skills/roblox-data/references/inventory-systems.md +1729 -0
  15. package/skills/roblox-debug/SKILL.md +99 -0
  16. package/skills/roblox-gui/SKILL.md +1103 -0
  17. package/skills/roblox-gui-fusion/SKILL.md +150 -0
  18. package/skills/roblox-gui-fusion/references/inventory.luau +427 -0
  19. package/skills/roblox-gui-fusion/references/settings-menu.luau +579 -0
  20. package/skills/roblox-gui-fusion/references/shop.luau +411 -0
  21. package/skills/roblox-luau-mastery/SKILL.md +1519 -0
  22. package/skills/roblox-monetization/SKILL.md +1084 -0
  23. package/skills/roblox-monetization/references/process-receipt.luau +131 -0
  24. package/skills/roblox-networking/SKILL.md +669 -0
  25. package/skills/roblox-networking/references/remote-validator.luau +193 -0
  26. package/skills/roblox-publish-checklist/SKILL.md +128 -0
  27. package/skills/roblox-runtime/SKILL.md +753 -0
  28. package/skills/roblox-sharp-edges/SKILL.md +295 -0
  29. package/skills/roblox-sync/SKILL.md +126 -0
  30. package/skills/roblox-testing/SKILL.md +943 -0
  31. package/skills/roblox-tooling/SKILL.md +150 -0
  32. package/vendor/LICENSES/ProfileStore-LICENSE +201 -0
  33. package/vendor/LICENSES/RbxUtil-LICENSE +7 -0
  34. package/vendor/LICENSES/promise-LICENSE +21 -0
  35. package/vendor/LICENSES/t-LICENSE +21 -0
  36. package/vendor/LICENSES/testez-LICENSE +201 -0
  37. package/vendor/README.md +84 -0
  38. package/vendor/fusion/Animation/ExternalTime.luau +84 -0
  39. package/vendor/fusion/Animation/Spring.luau +322 -0
  40. package/vendor/fusion/Animation/Stopwatch.luau +128 -0
  41. package/vendor/fusion/Animation/Tween.luau +187 -0
  42. package/vendor/fusion/Animation/getTweenDuration.luau +27 -0
  43. package/vendor/fusion/Animation/getTweenRatio.luau +47 -0
  44. package/vendor/fusion/Animation/lerpType.luau +164 -0
  45. package/vendor/fusion/Animation/packType.luau +100 -0
  46. package/vendor/fusion/Animation/springCoefficients.luau +80 -0
  47. package/vendor/fusion/Animation/unpackType.luau +103 -0
  48. package/vendor/fusion/Colour/Oklab.luau +70 -0
  49. package/vendor/fusion/Colour/sRGB.luau +55 -0
  50. package/vendor/fusion/External.luau +168 -0
  51. package/vendor/fusion/ExternalDebug.luau +70 -0
  52. package/vendor/fusion/Graph/Observer.luau +114 -0
  53. package/vendor/fusion/Graph/castToGraph.luau +29 -0
  54. package/vendor/fusion/Graph/change.luau +81 -0
  55. package/vendor/fusion/Graph/depend.luau +33 -0
  56. package/vendor/fusion/Graph/evaluate.luau +56 -0
  57. package/vendor/fusion/Instances/Attribute.luau +58 -0
  58. package/vendor/fusion/Instances/AttributeChange.luau +47 -0
  59. package/vendor/fusion/Instances/AttributeOut.luau +63 -0
  60. package/vendor/fusion/Instances/Child.luau +21 -0
  61. package/vendor/fusion/Instances/Children.luau +148 -0
  62. package/vendor/fusion/Instances/Hydrate.luau +33 -0
  63. package/vendor/fusion/Instances/New.luau +53 -0
  64. package/vendor/fusion/Instances/OnChange.luau +50 -0
  65. package/vendor/fusion/Instances/OnEvent.luau +54 -0
  66. package/vendor/fusion/Instances/Out.luau +69 -0
  67. package/vendor/fusion/Instances/applyInstanceProps.luau +149 -0
  68. package/vendor/fusion/Instances/defaultProps.luau +194 -0
  69. package/vendor/fusion/LICENSE +21 -0
  70. package/vendor/fusion/Logging/formatError.luau +49 -0
  71. package/vendor/fusion/Logging/messages.luau +52 -0
  72. package/vendor/fusion/Logging/parseError.luau +25 -0
  73. package/vendor/fusion/Memory/checkLifetime.luau +134 -0
  74. package/vendor/fusion/Memory/deriveScope.luau +24 -0
  75. package/vendor/fusion/Memory/deriveScopeImpl.luau +45 -0
  76. package/vendor/fusion/Memory/doCleanup.luau +79 -0
  77. package/vendor/fusion/Memory/innerScope.luau +34 -0
  78. package/vendor/fusion/Memory/legacyCleanup.luau +18 -0
  79. package/vendor/fusion/Memory/needsDestruction.luau +17 -0
  80. package/vendor/fusion/Memory/poisonScope.luau +34 -0
  81. package/vendor/fusion/Memory/scopePool.luau +55 -0
  82. package/vendor/fusion/Memory/scoped.luau +27 -0
  83. package/vendor/fusion/Memory/whichLivesLonger.luau +75 -0
  84. package/vendor/fusion/RobloxExternal.luau +98 -0
  85. package/vendor/fusion/State/Computed.luau +139 -0
  86. package/vendor/fusion/State/For/Disassembly.luau +211 -0
  87. package/vendor/fusion/State/For/ForTypes.luau +30 -0
  88. package/vendor/fusion/State/For/init.luau +110 -0
  89. package/vendor/fusion/State/ForKeys.luau +94 -0
  90. package/vendor/fusion/State/ForPairs.luau +97 -0
  91. package/vendor/fusion/State/ForValues.luau +94 -0
  92. package/vendor/fusion/State/Value.luau +88 -0
  93. package/vendor/fusion/State/castToState.luau +26 -0
  94. package/vendor/fusion/State/peek.luau +31 -0
  95. package/vendor/fusion/State/updateAll.luau +1 -0
  96. package/vendor/fusion/Types.luau +314 -0
  97. package/vendor/fusion/Utility/Contextual.luau +91 -0
  98. package/vendor/fusion/Utility/Safe.luau +23 -0
  99. package/vendor/fusion/Utility/isSimilar.luau +29 -0
  100. package/vendor/fusion/Utility/merge.luau +35 -0
  101. package/vendor/fusion/Utility/nameOf.luau +35 -0
  102. package/vendor/fusion/Utility/never.luau +14 -0
  103. package/vendor/fusion/Utility/nicknames.luau +11 -0
  104. package/vendor/fusion/Utility/xtypeof.luau +27 -0
  105. package/vendor/fusion/init.luau +82 -0
  106. package/vendor/profilestore/init.luau +2243 -0
  107. package/vendor/promise/init.luau +1982 -0
  108. package/vendor/rbxutil/buffer-util/Buffer.test.luau +25 -0
  109. package/vendor/rbxutil/buffer-util/BufferReader.luau +228 -0
  110. package/vendor/rbxutil/buffer-util/BufferWriter.luau +269 -0
  111. package/vendor/rbxutil/buffer-util/DataTypeBuffer.luau +223 -0
  112. package/vendor/rbxutil/buffer-util/Types.luau +60 -0
  113. package/vendor/rbxutil/buffer-util/index.d.ts +153 -0
  114. package/vendor/rbxutil/buffer-util/init.luau +41 -0
  115. package/vendor/rbxutil/buffer-util/package.json +16 -0
  116. package/vendor/rbxutil/buffer-util/wally.toml +9 -0
  117. package/vendor/rbxutil/comm/Client/ClientComm.luau +232 -0
  118. package/vendor/rbxutil/comm/Client/ClientRemoteProperty.luau +156 -0
  119. package/vendor/rbxutil/comm/Client/ClientRemoteSignal.luau +109 -0
  120. package/vendor/rbxutil/comm/Client/init.luau +135 -0
  121. package/vendor/rbxutil/comm/Server/RemoteProperty.luau +295 -0
  122. package/vendor/rbxutil/comm/Server/RemoteSignal.luau +211 -0
  123. package/vendor/rbxutil/comm/Server/ServerComm.luau +211 -0
  124. package/vendor/rbxutil/comm/Server/init.luau +140 -0
  125. package/vendor/rbxutil/comm/Types.luau +18 -0
  126. package/vendor/rbxutil/comm/Util.luau +27 -0
  127. package/vendor/rbxutil/comm/init.luau +35 -0
  128. package/vendor/rbxutil/comm/wally.toml +13 -0
  129. package/vendor/rbxutil/component/init.luau +759 -0
  130. package/vendor/rbxutil/component/init.test.luau +311 -0
  131. package/vendor/rbxutil/component/wally.toml +14 -0
  132. package/vendor/rbxutil/concur/init.luau +542 -0
  133. package/vendor/rbxutil/concur/init.test.luau +364 -0
  134. package/vendor/rbxutil/concur/wally.toml +8 -0
  135. package/vendor/rbxutil/enum-list/init.luau +101 -0
  136. package/vendor/rbxutil/enum-list/init.test.luau +91 -0
  137. package/vendor/rbxutil/enum-list/wally.toml +8 -0
  138. package/vendor/rbxutil/find/index.d.ts +20 -0
  139. package/vendor/rbxutil/find/init.luau +44 -0
  140. package/vendor/rbxutil/find/package.json +17 -0
  141. package/vendor/rbxutil/find/wally.toml +8 -0
  142. package/vendor/rbxutil/input/Gamepad.luau +559 -0
  143. package/vendor/rbxutil/input/Keyboard.luau +124 -0
  144. package/vendor/rbxutil/input/Mouse.luau +278 -0
  145. package/vendor/rbxutil/input/PreferredInput.luau +91 -0
  146. package/vendor/rbxutil/input/Touch.luau +120 -0
  147. package/vendor/rbxutil/input/init.luau +33 -0
  148. package/vendor/rbxutil/input/wally.toml +12 -0
  149. package/vendor/rbxutil/loader/index.d.ts +15 -0
  150. package/vendor/rbxutil/loader/init.luau +137 -0
  151. package/vendor/rbxutil/loader/wally.toml +8 -0
  152. package/vendor/rbxutil/log/index.d.ts +38 -0
  153. package/vendor/rbxutil/log/init.luau +746 -0
  154. package/vendor/rbxutil/log/wally.toml +8 -0
  155. package/vendor/rbxutil/net/init.luau +190 -0
  156. package/vendor/rbxutil/net/wally.toml +8 -0
  157. package/vendor/rbxutil/option/index.d.ts +44 -0
  158. package/vendor/rbxutil/option/init.luau +489 -0
  159. package/vendor/rbxutil/option/init.test.luau +342 -0
  160. package/vendor/rbxutil/option/wally.toml +8 -0
  161. package/vendor/rbxutil/pid/index.d.ts +53 -0
  162. package/vendor/rbxutil/pid/init.luau +195 -0
  163. package/vendor/rbxutil/pid/package.json +16 -0
  164. package/vendor/rbxutil/pid/wally.toml +9 -0
  165. package/vendor/rbxutil/quaternion/index.d.ts +117 -0
  166. package/vendor/rbxutil/quaternion/init.luau +570 -0
  167. package/vendor/rbxutil/quaternion/package.json +16 -0
  168. package/vendor/rbxutil/quaternion/wally.toml +9 -0
  169. package/vendor/rbxutil/query/index.d.ts +43 -0
  170. package/vendor/rbxutil/query/init.luau +117 -0
  171. package/vendor/rbxutil/query/package.json +18 -0
  172. package/vendor/rbxutil/query/wally.toml +9 -0
  173. package/vendor/rbxutil/sequent/index.d.ts +28 -0
  174. package/vendor/rbxutil/sequent/init.luau +340 -0
  175. package/vendor/rbxutil/sequent/package.json +16 -0
  176. package/vendor/rbxutil/sequent/wally.toml +9 -0
  177. package/vendor/rbxutil/ser/init.luau +175 -0
  178. package/vendor/rbxutil/ser/init.test.luau +50 -0
  179. package/vendor/rbxutil/ser/wally.toml +11 -0
  180. package/vendor/rbxutil/shake/index.d.ts +36 -0
  181. package/vendor/rbxutil/shake/init.luau +532 -0
  182. package/vendor/rbxutil/shake/init.test.luau +267 -0
  183. package/vendor/rbxutil/shake/package.json +16 -0
  184. package/vendor/rbxutil/shake/wally.toml +9 -0
  185. package/vendor/rbxutil/signal/index.d.ts +100 -0
  186. package/vendor/rbxutil/signal/init.luau +432 -0
  187. package/vendor/rbxutil/signal/init.test.luau +190 -0
  188. package/vendor/rbxutil/signal/package.json +17 -0
  189. package/vendor/rbxutil/signal/wally.toml +9 -0
  190. package/vendor/rbxutil/silo/TableWatcher.luau +65 -0
  191. package/vendor/rbxutil/silo/Util.luau +55 -0
  192. package/vendor/rbxutil/silo/init.luau +338 -0
  193. package/vendor/rbxutil/silo/init.test.luau +215 -0
  194. package/vendor/rbxutil/silo/wally.toml +8 -0
  195. package/vendor/rbxutil/spring/index.d.ts +40 -0
  196. package/vendor/rbxutil/spring/init.luau +97 -0
  197. package/vendor/rbxutil/spring/package.json +17 -0
  198. package/vendor/rbxutil/spring/wally.toml +8 -0
  199. package/vendor/rbxutil/stream/index.d.ts +88 -0
  200. package/vendor/rbxutil/stream/init.luau +597 -0
  201. package/vendor/rbxutil/stream/package.json +18 -0
  202. package/vendor/rbxutil/stream/wally.toml +9 -0
  203. package/vendor/rbxutil/streamable/Streamable.luau +202 -0
  204. package/vendor/rbxutil/streamable/StreamableUtil.luau +80 -0
  205. package/vendor/rbxutil/streamable/init.luau +8 -0
  206. package/vendor/rbxutil/streamable/wally.toml +12 -0
  207. package/vendor/rbxutil/symbol/init.luau +56 -0
  208. package/vendor/rbxutil/symbol/init.test.luau +37 -0
  209. package/vendor/rbxutil/symbol/wally.toml +8 -0
  210. package/vendor/rbxutil/table-util/init.luau +938 -0
  211. package/vendor/rbxutil/table-util/init.test.luau +439 -0
  212. package/vendor/rbxutil/table-util/wally.toml +8 -0
  213. package/vendor/rbxutil/task-queue/index.d.ts +27 -0
  214. package/vendor/rbxutil/task-queue/init.luau +97 -0
  215. package/vendor/rbxutil/task-queue/wally.toml +8 -0
  216. package/vendor/rbxutil/timer/index.d.ts +81 -0
  217. package/vendor/rbxutil/timer/init.luau +249 -0
  218. package/vendor/rbxutil/timer/init.test.luau +73 -0
  219. package/vendor/rbxutil/timer/wally.toml +11 -0
  220. package/vendor/rbxutil/tree/index.d.ts +15 -0
  221. package/vendor/rbxutil/tree/init.luau +137 -0
  222. package/vendor/rbxutil/tree/wally.toml +8 -0
  223. package/vendor/rbxutil/trove/index.d.ts +46 -0
  224. package/vendor/rbxutil/trove/init.luau +787 -0
  225. package/vendor/rbxutil/trove/init.test.luau +203 -0
  226. package/vendor/rbxutil/trove/wally.toml +8 -0
  227. package/vendor/rbxutil/typed-remote/init.luau +196 -0
  228. package/vendor/rbxutil/typed-remote/wally.toml +8 -0
  229. package/vendor/rbxutil/wait-for/index.d.ts +17 -0
  230. package/vendor/rbxutil/wait-for/init.luau +257 -0
  231. package/vendor/rbxutil/wait-for/init.test.luau +182 -0
  232. package/vendor/rbxutil/wait-for/wally.toml +11 -0
  233. package/vendor/t/t.lua +1350 -0
  234. package/vendor/testez/Context.lua +26 -0
  235. package/vendor/testez/Expectation.lua +311 -0
  236. package/vendor/testez/ExpectationContext.lua +38 -0
  237. package/vendor/testez/LifecycleHooks.lua +89 -0
  238. package/vendor/testez/Reporters/TeamCityReporter.lua +102 -0
  239. package/vendor/testez/Reporters/TextReporter.lua +106 -0
  240. package/vendor/testez/Reporters/TextReporterQuiet.lua +97 -0
  241. package/vendor/testez/TestBootstrap.lua +147 -0
  242. package/vendor/testez/TestEnum.lua +28 -0
  243. package/vendor/testez/TestPlan.lua +304 -0
  244. package/vendor/testez/TestPlanner.lua +40 -0
  245. package/vendor/testez/TestResults.lua +112 -0
  246. package/vendor/testez/TestRunner.lua +188 -0
  247. package/vendor/testez/TestSession.lua +243 -0
  248. package/vendor/testez/init.lua +40 -0
@@ -0,0 +1,759 @@
1
+ -- Component
2
+ -- Stephen Leitnick
3
+ -- November 26, 2021
4
+
5
+ type AncestorList = { Instance }
6
+
7
+ --[=[
8
+ @type ExtensionFn (component) -> ()
9
+ @within Component
10
+ ]=]
11
+ type ExtensionFn = (any) -> ()
12
+
13
+ --[=[
14
+ @type ExtensionShouldFn (component) -> boolean
15
+ @within Component
16
+ ]=]
17
+ type ExtensionShouldFn = (any) -> boolean
18
+
19
+ --[=[
20
+ @interface Extension
21
+ @within Component
22
+ .ShouldExtend ExtensionShouldFn?
23
+ .ShouldConstruct ExtensionShouldFn?
24
+ .Constructing ExtensionFn?
25
+ .Constructed ExtensionFn?
26
+ .Starting ExtensionFn?
27
+ .Started ExtensionFn?
28
+ .Stopping ExtensionFn?
29
+ .Stopped ExtensionFn?
30
+
31
+ An extension allows the ability to extend the behavior of
32
+ components. This is useful for adding injection systems or
33
+ extending the behavior of components by wrapping around
34
+ component lifecycle methods.
35
+
36
+ The `ShouldConstruct` function can be used to indicate
37
+ if the component should actually be created. This must
38
+ return `true` or `false`. A component with multiple
39
+ `ShouldConstruct` extension functions must have them _all_
40
+ return `true` in order for the component to be constructed.
41
+ The `ShouldConstruct` function runs _before_ all other
42
+ extension functions and component lifecycle methods.
43
+
44
+ The `ShouldExtend` function can be used to indicate if
45
+ the extension itself should be used. This can be used in
46
+ order to toggle an extension on/off depending on whatever
47
+ logic is appropriate. If no `ShouldExtend` function is
48
+ provided, the extension will always be used if provided
49
+ as an extension to the component.
50
+
51
+ As an example, an extension could be created to simply log
52
+ when the various lifecycle stages run on the component:
53
+
54
+ ```lua
55
+ local Logger = {}
56
+ function Logger.Constructing(component) print("Constructing", component) end
57
+ function Logger.Constructed(component) print("Constructed", component) end
58
+ function Logger.Starting(component) print("Starting", component) end
59
+ function Logger.Started(component) print("Started", component) end
60
+ function Logger.Stopping(component) print("Stopping", component) end
61
+ function Logger.Stopped(component) print("Stopped", component) end
62
+
63
+ local MyComponent = Component.new({Tag = "MyComponent", Extensions = {Logger}})
64
+ ```
65
+
66
+ Sometimes it is useful for an extension to control whether or
67
+ not a component should be constructed. For instance, if a
68
+ component on the client should only be instantiated for the
69
+ local player, an extension might look like this, assuming the
70
+ instance has an attribute linking it to the player's UserId:
71
+ ```lua
72
+ local player = game:GetService("Players").LocalPlayer
73
+
74
+ local OnlyLocalPlayer = {}
75
+ function OnlyLocalPlayer.ShouldConstruct(component)
76
+ local ownerId = component.Instance:GetAttribute("OwnerId")
77
+ return ownerId == player.UserId
78
+ end
79
+
80
+ local MyComponent = Component.new({Tag = "MyComponent", Extensions = {OnlyLocalPlayer}})
81
+ ```
82
+
83
+ It can also be useful for an extension itself to turn on/off
84
+ depending on various contexts. For example, let's take the
85
+ Logger from the first example, and only use that extension
86
+ if the bound instance has a Log attribute set to `true`:
87
+ ```lua
88
+ function Logger.ShouldExtend(component)
89
+ return component.Instance:GetAttribute("Log") == true
90
+ end
91
+ ```
92
+ ]=]
93
+ type Extension = {
94
+ ShouldExtend: ExtensionShouldFn?,
95
+ ShouldConstruct: ExtensionShouldFn?,
96
+ Constructing: ExtensionFn?,
97
+ Constructed: ExtensionFn?,
98
+ Starting: ExtensionFn?,
99
+ Started: ExtensionFn?,
100
+ Stopping: ExtensionFn?,
101
+ Stopped: ExtensionFn?,
102
+ }
103
+
104
+ --[=[
105
+ @interface ComponentConfig
106
+ @within Component
107
+ .Tag string -- CollectionService tag to use
108
+ .Ancestors {Instance}? -- Optional array of ancestors in which components will be started
109
+ .Extensions {Extension}? -- Optional array of extension objects
110
+
111
+ Component configuration passed to `Component.new`.
112
+
113
+ - If no Ancestors option is included, it defaults to `{workspace, game.Players}`.
114
+ - If no Extensions option is included, it defaults to a blank table `{}`.
115
+ ]=]
116
+ type ComponentConfig = {
117
+ Tag: string,
118
+ Ancestors: AncestorList?,
119
+ Extensions: { Extension }?,
120
+ }
121
+
122
+ --[=[
123
+ @within Component
124
+ @prop Started Signal
125
+ @tag Event
126
+ @tag Component Class
127
+
128
+ Fired when a new instance of a component is started.
129
+
130
+ ```lua
131
+ local MyComponent = Component.new({Tag = "MyComponent"})
132
+
133
+ MyComponent.Started:Connect(function(component) end)
134
+ ```
135
+ ]=]
136
+
137
+ --[=[
138
+ @within Component
139
+ @prop Stopped Signal
140
+ @tag Event
141
+ @tag Component Class
142
+
143
+ Fired when an instance of a component is stopped.
144
+
145
+ ```lua
146
+ local MyComponent = Component.new({Tag = "MyComponent"})
147
+
148
+ MyComponent.Stopped:Connect(function(component) end)
149
+ ```
150
+ ]=]
151
+
152
+ --[=[
153
+ @tag Component Instance
154
+ @within Component
155
+ @prop Instance Instance
156
+
157
+ A reference back to the _Roblox_ instance from within a _component_ instance. When
158
+ a component instance is created, it is bound to a specific Roblox instance, which
159
+ will always be present through the `Instance` property.
160
+
161
+ ```lua
162
+ MyComponent.Started:Connect(function(component)
163
+ local robloxInstance: Instance = component.Instance
164
+ print("Component is bound to " .. robloxInstance:GetFullName())
165
+ end)
166
+ ```
167
+ ]=]
168
+
169
+ local CollectionService = game:GetService("CollectionService")
170
+ local RunService = game:GetService("RunService")
171
+
172
+ local Promise = require(script.Parent.Promise)
173
+ local Signal = require(script.Parent.Signal)
174
+ local Symbol = require(script.Parent.Symbol)
175
+ local Trove = require(script.Parent.Trove)
176
+
177
+ local IS_SERVER = RunService:IsServer()
178
+ local DEFAULT_ANCESTORS = { workspace, game:GetService("Players") }
179
+ local DEFAULT_TIMEOUT = 60
180
+
181
+ -- Symbol keys:
182
+ local KEY_ANCESTORS = Symbol("Ancestors")
183
+ local KEY_INST_TO_COMPONENTS = Symbol("InstancesToComponents")
184
+ local KEY_LOCK_CONSTRUCT = Symbol("LockConstruct")
185
+ local KEY_COMPONENTS = Symbol("Components")
186
+ local KEY_TROVE = Symbol("Trove")
187
+ local KEY_EXTENSIONS = Symbol("Extensions")
188
+ local KEY_ACTIVE_EXTENSIONS = Symbol("ActiveExtensions")
189
+ local KEY_STARTING = Symbol("Starting")
190
+ local KEY_STARTED = Symbol("Started")
191
+
192
+ local renderId = 0
193
+ local function NextRenderName(): string
194
+ renderId += 1
195
+ return "ComponentRender" .. tostring(renderId)
196
+ end
197
+
198
+ local function InvokeExtensionFn(component, fnName: string)
199
+ for _, extension in ipairs(component[KEY_ACTIVE_EXTENSIONS]) do
200
+ local fn = extension[fnName]
201
+ if type(fn) == "function" then
202
+ fn(component)
203
+ end
204
+ end
205
+ end
206
+
207
+ local function ShouldConstruct(component): boolean
208
+ for _, extension in ipairs(component[KEY_ACTIVE_EXTENSIONS]) do
209
+ local fn = extension.ShouldConstruct
210
+ if type(fn) == "function" then
211
+ local shouldConstruct = fn(component)
212
+ if not shouldConstruct then
213
+ return false
214
+ end
215
+ end
216
+ end
217
+ return true
218
+ end
219
+
220
+ local function GetActiveExtensions(component, extensionList)
221
+ local activeExtensions = table.create(#extensionList)
222
+ local allActive = true
223
+ for _, extension in ipairs(extensionList) do
224
+ local fn = extension.ShouldExtend
225
+ local shouldExtend = type(fn) ~= "function" or not not fn(component)
226
+ if shouldExtend then
227
+ table.insert(activeExtensions, extension)
228
+ else
229
+ allActive = false
230
+ end
231
+ end
232
+ return if allActive then extensionList else activeExtensions
233
+ end
234
+
235
+ --[=[
236
+ @class Component
237
+
238
+ Bind components to Roblox instances using the Component class and CollectionService tags.
239
+
240
+ To avoid confusion of terms:
241
+ - `Component` refers to this module.
242
+ - `Component Class` (e.g. `MyComponent` through this documentation) refers to a class created via `Component.new`
243
+ - `Component Instance` refers to an instance of a component class.
244
+ - `Roblox Instance` refers to the Roblox instance to which the component instance is bound.
245
+
246
+ Methods and properties are tagged with the above terms to help clarify the level at which they are used.
247
+ ]=]
248
+ local Component = {}
249
+ Component.__index = Component
250
+
251
+ --[=[
252
+ @tag Component
253
+ @param config ComponentConfig
254
+ @return ComponentClass
255
+
256
+ Create a new custom Component class.
257
+
258
+ ```lua
259
+ local MyComponent = Component.new({Tag = "MyComponent"})
260
+ ```
261
+
262
+ A full example might look like this:
263
+
264
+ ```lua
265
+ local MyComponent = Component.new({
266
+ Tag = "MyComponent",
267
+ Ancestors = {workspace},
268
+ Extensions = {Logger}, -- See Logger example within the example for the Extension type
269
+ })
270
+
271
+ local AnotherComponent = require(somewhere.AnotherComponent)
272
+
273
+ -- Optional if UpdateRenderStepped should use BindToRenderStep:
274
+ MyComponent.RenderPriority = Enum.RenderPriority.Camera.Value
275
+
276
+ function MyComponent:Construct()
277
+ self.MyData = "Hello"
278
+ end
279
+
280
+ function MyComponent:Start()
281
+ local another = self:GetComponent(AnotherComponent)
282
+ another:DoSomething()
283
+ end
284
+
285
+ function MyComponent:Stop()
286
+ self.MyData = "Goodbye"
287
+ end
288
+
289
+ function MyComponent:HeartbeatUpdate(dt)
290
+ end
291
+
292
+ function MyComponent:SteppedUpdate(dt)
293
+ end
294
+
295
+ function MyComponent:RenderSteppedUpdate(dt)
296
+ end
297
+ ```
298
+ ]=]
299
+ function Component.new(config: ComponentConfig)
300
+ local customComponent = {}
301
+ customComponent.__index = customComponent
302
+ customComponent.__tostring = function()
303
+ return "Component<" .. config.Tag .. ">"
304
+ end
305
+ customComponent[KEY_ANCESTORS] = config.Ancestors or DEFAULT_ANCESTORS
306
+ customComponent[KEY_INST_TO_COMPONENTS] = {}
307
+ customComponent[KEY_COMPONENTS] = {}
308
+ customComponent[KEY_LOCK_CONSTRUCT] = {}
309
+ customComponent[KEY_TROVE] = Trove.new()
310
+ customComponent[KEY_EXTENSIONS] = config.Extensions or {}
311
+ customComponent[KEY_STARTED] = false
312
+ customComponent.Tag = config.Tag
313
+ customComponent.Started = customComponent[KEY_TROVE]:Construct(Signal)
314
+ customComponent.Stopped = customComponent[KEY_TROVE]:Construct(Signal)
315
+ setmetatable(customComponent, Component)
316
+ customComponent:_setup()
317
+ return customComponent
318
+ end
319
+
320
+ function Component:_instantiate(instance: Instance)
321
+ local component = setmetatable({}, self)
322
+ component.Instance = instance
323
+ component[KEY_ACTIVE_EXTENSIONS] = GetActiveExtensions(component, self[KEY_EXTENSIONS])
324
+ if not ShouldConstruct(component) then
325
+ return nil
326
+ end
327
+ InvokeExtensionFn(component, "Constructing")
328
+ if type(component.Construct) == "function" then
329
+ component:Construct()
330
+ end
331
+ InvokeExtensionFn(component, "Constructed")
332
+ return component
333
+ end
334
+
335
+ function Component:_setup()
336
+ local watchingInstances = {}
337
+
338
+ local function StartComponent(component)
339
+ component[KEY_STARTING] = coroutine.running()
340
+
341
+ InvokeExtensionFn(component, "Starting")
342
+
343
+ component:Start()
344
+ if component[KEY_STARTING] == nil then
345
+ -- Component's Start method stopped the component
346
+ return
347
+ end
348
+
349
+ InvokeExtensionFn(component, "Started")
350
+
351
+ local hasHeartbeatUpdate = typeof(component.HeartbeatUpdate) == "function"
352
+ local hasSteppedUpdate = typeof(component.SteppedUpdate) == "function"
353
+ local hasRenderSteppedUpdate = typeof(component.RenderSteppedUpdate) == "function"
354
+
355
+ if hasHeartbeatUpdate then
356
+ component._heartbeatUpdate = RunService.Heartbeat:Connect(function(dt)
357
+ component:HeartbeatUpdate(dt)
358
+ end)
359
+ end
360
+
361
+ if hasSteppedUpdate then
362
+ component._steppedUpdate = RunService.Stepped:Connect(function(_, dt)
363
+ component:SteppedUpdate(dt)
364
+ end)
365
+ end
366
+
367
+ if hasRenderSteppedUpdate and not IS_SERVER then
368
+ if component.RenderPriority then
369
+ component._renderName = NextRenderName()
370
+ RunService:BindToRenderStep(component._renderName, component.RenderPriority, function(dt)
371
+ component:RenderSteppedUpdate(dt)
372
+ end)
373
+ else
374
+ component._renderSteppedUpdate = RunService.RenderStepped:Connect(function(dt)
375
+ component:RenderSteppedUpdate(dt)
376
+ end)
377
+ end
378
+ end
379
+
380
+ component[KEY_STARTED] = true
381
+ component[KEY_STARTING] = nil
382
+
383
+ self.Started:Fire(component)
384
+ end
385
+
386
+ local function StopComponent(component)
387
+ if component[KEY_STARTING] then
388
+ -- Stop the component during its start method invocation:
389
+ local startThread = component[KEY_STARTING]
390
+ if coroutine.status(startThread) ~= "normal" then
391
+ pcall(function()
392
+ task.cancel(startThread)
393
+ end)
394
+ else
395
+ task.defer(function()
396
+ pcall(function()
397
+ task.cancel(startThread)
398
+ end)
399
+ end)
400
+ end
401
+ component[KEY_STARTING] = nil
402
+ end
403
+
404
+ if component._heartbeatUpdate then
405
+ component._heartbeatUpdate:Disconnect()
406
+ end
407
+
408
+ if component._steppedUpdate then
409
+ component._steppedUpdate:Disconnect()
410
+ end
411
+
412
+ if component._renderSteppedUpdate then
413
+ component._renderSteppedUpdate:Disconnect()
414
+ elseif component._renderName then
415
+ RunService:UnbindFromRenderStep(component._renderName)
416
+ end
417
+
418
+ InvokeExtensionFn(component, "Stopping")
419
+ component:Stop()
420
+ InvokeExtensionFn(component, "Stopped")
421
+ self.Stopped:Fire(component)
422
+ end
423
+
424
+ local function SafeConstruct(instance, id)
425
+ if self[KEY_LOCK_CONSTRUCT][instance] ~= id then
426
+ return nil
427
+ end
428
+ local component = self:_instantiate(instance)
429
+ if self[KEY_LOCK_CONSTRUCT][instance] ~= id then
430
+ return nil
431
+ end
432
+ return component
433
+ end
434
+
435
+ local function TryConstructComponent(instance)
436
+ if self[KEY_INST_TO_COMPONENTS][instance] then
437
+ return
438
+ end
439
+ local id = self[KEY_LOCK_CONSTRUCT][instance] or 0
440
+ id += 1
441
+ self[KEY_LOCK_CONSTRUCT][instance] = id
442
+ task.defer(function()
443
+ local component = SafeConstruct(instance, id)
444
+ if not component then
445
+ return
446
+ end
447
+ self[KEY_INST_TO_COMPONENTS][instance] = component
448
+ table.insert(self[KEY_COMPONENTS], component)
449
+ task.defer(function()
450
+ if self[KEY_INST_TO_COMPONENTS][instance] == component then
451
+ StartComponent(component)
452
+ end
453
+ end)
454
+ end)
455
+ end
456
+
457
+ local function TryDeconstructComponent(instance)
458
+ local component = self[KEY_INST_TO_COMPONENTS][instance]
459
+ if not component then
460
+ return
461
+ end
462
+ self[KEY_INST_TO_COMPONENTS][instance] = nil
463
+ self[KEY_LOCK_CONSTRUCT][instance] = nil
464
+ local components = self[KEY_COMPONENTS]
465
+ local index = table.find(components, component)
466
+ if index then
467
+ local n = #components
468
+ components[index] = components[n]
469
+ components[n] = nil
470
+ end
471
+ if component[KEY_STARTED] or component[KEY_STARTING] then
472
+ task.spawn(StopComponent, component)
473
+ end
474
+ end
475
+
476
+ local function StartWatchingInstance(instance)
477
+ if watchingInstances[instance] then
478
+ return
479
+ end
480
+ local function IsInAncestorList(): boolean
481
+ for _, parent in ipairs(self[KEY_ANCESTORS]) do
482
+ if instance:IsDescendantOf(parent) then
483
+ return true
484
+ end
485
+ end
486
+ return false
487
+ end
488
+ local ancestryChangedHandle = self[KEY_TROVE]:Connect(instance.AncestryChanged, function(_, parent)
489
+ if parent and IsInAncestorList() then
490
+ TryConstructComponent(instance)
491
+ else
492
+ TryDeconstructComponent(instance)
493
+ end
494
+ end)
495
+ watchingInstances[instance] = ancestryChangedHandle
496
+ if IsInAncestorList() then
497
+ TryConstructComponent(instance)
498
+ end
499
+ end
500
+
501
+ local function InstanceTagged(instance: Instance)
502
+ StartWatchingInstance(instance)
503
+ end
504
+
505
+ local function InstanceUntagged(instance: Instance)
506
+ local watchHandle = watchingInstances[instance]
507
+ if watchHandle then
508
+ watchingInstances[instance] = nil
509
+ self[KEY_TROVE]:Remove(watchHandle)
510
+ end
511
+ TryDeconstructComponent(instance)
512
+ end
513
+
514
+ self[KEY_TROVE]:Connect(CollectionService:GetInstanceAddedSignal(self.Tag), InstanceTagged)
515
+ self[KEY_TROVE]:Connect(CollectionService:GetInstanceRemovedSignal(self.Tag), InstanceUntagged)
516
+
517
+ local tagged = CollectionService:GetTagged(self.Tag)
518
+ for _, instance in ipairs(tagged) do
519
+ task.defer(InstanceTagged, instance)
520
+ end
521
+ end
522
+
523
+ --[=[
524
+ @tag Component Class
525
+ @return {Component}
526
+ Gets a table array of all existing component objects. For example,
527
+ if there was a component class linked to the "MyComponent" tag,
528
+ and three Roblox instances in your game had that same tag, then
529
+ calling `GetAll` would return the three component instances.
530
+
531
+ ```lua
532
+ local MyComponent = Component.new({Tag = "MyComponent"})
533
+
534
+ -- ...
535
+
536
+ local components = MyComponent:GetAll()
537
+ for _,component in ipairs(components) do
538
+ component:DoSomethingHere()
539
+ end
540
+ ```
541
+ ]=]
542
+ function Component:GetAll()
543
+ return self[KEY_COMPONENTS]
544
+ end
545
+
546
+ --[=[
547
+ @tag Component Class
548
+ @return Component?
549
+
550
+ Gets an instance of a component class from the given Roblox
551
+ instance. Returns `nil` if not found.
552
+
553
+ ```lua
554
+ local MyComponent = require(somewhere.MyComponent)
555
+
556
+ local myComponentInstance = MyComponent:FromInstance(workspace.SomeInstance)
557
+ ```
558
+ ]=]
559
+ function Component:FromInstance(instance: Instance)
560
+ return self[KEY_INST_TO_COMPONENTS][instance]
561
+ end
562
+
563
+ --[=[
564
+ @tag Component Class
565
+ @return Promise<ComponentInstance>
566
+
567
+ Resolves a promise once the component instance is present on a given
568
+ Roblox instance.
569
+
570
+ An optional `timeout` can be provided to reject the promise if it
571
+ takes more than `timeout` seconds to resolve. If no timeout is
572
+ supplied, `timeout` defaults to 60 seconds.
573
+
574
+ ```lua
575
+ local MyComponent = require(somewhere.MyComponent)
576
+
577
+ MyComponent:WaitForInstance(workspace.SomeInstance):andThen(function(myComponentInstance)
578
+ -- Do something with the component class
579
+ end)
580
+ ```
581
+ ]=]
582
+ function Component:WaitForInstance(instance: Instance, timeout: number?)
583
+ local componentInstance = self:FromInstance(instance)
584
+ if componentInstance and componentInstance[KEY_STARTED] then
585
+ return Promise.resolve(componentInstance)
586
+ end
587
+ return Promise.fromEvent(self.Started, function(c)
588
+ local match = c.Instance == instance
589
+ if match then
590
+ componentInstance = c
591
+ end
592
+ return match
593
+ end)
594
+ :andThen(function()
595
+ return componentInstance
596
+ end)
597
+ :timeout(if type(timeout) == "number" then timeout else DEFAULT_TIMEOUT)
598
+ end
599
+
600
+ --[=[
601
+ @tag Component Class
602
+ `Construct` is called before the component is started, and should be used
603
+ to construct the component instance.
604
+
605
+ ```lua
606
+ local MyComponent = Component.new({Tag = "MyComponent"})
607
+
608
+ function MyComponent:Construct()
609
+ self.SomeData = 32
610
+ self.OtherStuff = "HelloWorld"
611
+ end
612
+ ```
613
+ ]=]
614
+ function Component:Construct() end
615
+
616
+ --[=[
617
+ @tag Component Class
618
+ `Start` is called when the component is started. At this point in time, it
619
+ is safe to grab other components also bound to the same instance.
620
+
621
+ ```lua
622
+ local MyComponent = Component.new({Tag = "MyComponent"})
623
+ local AnotherComponent = require(somewhere.AnotherComponent)
624
+
625
+ function MyComponent:Start()
626
+ -- e.g., grab another component:
627
+ local another = self:GetComponent(AnotherComponent)
628
+ end
629
+ ```
630
+ ]=]
631
+ function Component:Start() end
632
+
633
+ --[=[
634
+ @tag Component Class
635
+ `Stop` is called when the component is stopped. This occurs either when the
636
+ bound instance is removed from one of the whitelisted ancestors _or_ when
637
+ the matching tag is removed from the instance. This also means that the
638
+ instance _might_ be destroyed, and thus it is not safe to continue using
639
+ the bound instance (e.g. `self.Instance`) any longer.
640
+
641
+ This should be used to clean up the component.
642
+
643
+ ```lua
644
+ local MyComponent = Component.new({Tag = "MyComponent"})
645
+
646
+ function MyComponent:Stop()
647
+ self.SomeStuff:Destroy()
648
+ end
649
+ ```
650
+ ]=]
651
+ function Component:Stop() end
652
+
653
+ --[=[
654
+ @tag Component Instance
655
+ @param componentClass ComponentClass
656
+ @return Component?
657
+
658
+ Retrieves another component instance bound to the same
659
+ Roblox instance.
660
+
661
+ ```lua
662
+ local MyComponent = Component.new({Tag = "MyComponent"})
663
+ local AnotherComponent = require(somewhere.AnotherComponent)
664
+
665
+ function MyComponent:Start()
666
+ local another = self:GetComponent(AnotherComponent)
667
+ end
668
+ ```
669
+ ]=]
670
+ function Component:GetComponent(componentClass)
671
+ return componentClass[KEY_INST_TO_COMPONENTS][self.Instance]
672
+ end
673
+
674
+ --[=[
675
+ @tag Component Class
676
+ @function HeartbeatUpdate
677
+ @param dt number
678
+ @within Component
679
+
680
+ If this method is present on a component, then it will be
681
+ automatically connected to `RunService.Heartbeat`.
682
+
683
+ :::note Method
684
+ This is a method, not a function. This is a limitation
685
+ of the documentation tool which should be fixed soon.
686
+ :::
687
+
688
+ ```lua
689
+ local MyComponent = Component.new({Tag = "MyComponent"})
690
+
691
+ function MyComponent:HeartbeatUpdate(dt)
692
+ end
693
+ ```
694
+ ]=]
695
+ --[=[
696
+ @tag Component Class
697
+ @function SteppedUpdate
698
+ @param dt number
699
+ @within Component
700
+
701
+ If this method is present on a component, then it will be
702
+ automatically connected to `RunService.Stepped`.
703
+
704
+ :::note Method
705
+ This is a method, not a function. This is a limitation
706
+ of the documentation tool which should be fixed soon.
707
+ :::
708
+
709
+ ```lua
710
+ local MyComponent = Component.new({Tag = "MyComponent"})
711
+
712
+ function MyComponent:SteppedUpdate(dt)
713
+ end
714
+ ```
715
+ ]=]
716
+ --[=[
717
+ @tag Component Class
718
+ @function RenderSteppedUpdate
719
+ @param dt number
720
+ @within Component
721
+ @client
722
+
723
+ If this method is present on a component, then it will be
724
+ automatically connected to `RunService.RenderStepped`. If
725
+ the `[Component].RenderPriority` field is found, then the
726
+ component will instead use `RunService:BindToRenderStep()`
727
+ to bind the function.
728
+
729
+ :::note Method
730
+ This is a method, not a function. This is a limitation
731
+ of the documentation tool which should be fixed soon.
732
+ :::
733
+
734
+ ```lua
735
+ -- Example that uses `RunService.RenderStepped` automatically:
736
+
737
+ local MyComponent = Component.new({Tag = "MyComponent"})
738
+
739
+ function MyComponent:RenderSteppedUpdate(dt)
740
+ end
741
+ ```
742
+ ```lua
743
+ -- Example that uses `RunService:BindToRenderStep` automatically:
744
+
745
+ local MyComponent = Component.new({Tag = "MyComponent"})
746
+
747
+ -- Defining a RenderPriority will force the component to use BindToRenderStep instead
748
+ MyComponent.RenderPriority = Enum.RenderPriority.Camera.Value
749
+
750
+ function MyComponent:RenderSteppedUpdate(dt)
751
+ end
752
+ ```
753
+ ]=]
754
+
755
+ function Component:Destroy()
756
+ self[KEY_TROVE]:Destroy()
757
+ end
758
+
759
+ return Component