@rbxts/planck 0.3.0-alpha.1 → 0.3.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1057 +1,1557 @@
1
- --!nonstrict
2
- local DependencyGraph = require(script.Parent.DependencyGraph)
3
- local Pipeline = require(script.Parent.Pipeline)
4
- local Phase = require(script.Parent.Phase)
5
-
6
- local utils = require(script.Parent.utils)
7
- local hooks = require(script.Parent.hooks)
8
- local conditions = require(script.Parent.conditions)
9
-
10
- local getSystem = utils.getSystem
11
- local getSystemName = utils.getSystemName
12
-
13
- local isPhase = utils.isPhase
14
- local isPipeline = utils.isPipeline
15
-
16
- local isValidEvent = utils.isValidEvent
17
- local getEventIdentifier = utils.getEventIdentifier
18
-
19
- -- Recent errors in Planks itself
20
- local recentLogs = {}
21
- local timeLastLogged = os.clock()
22
-
23
- --- @type SystemFn ((U...) -> ())
24
- --- @within Scheduler
25
- --- Standard system function that runs every time it's scheduled
26
-
27
- --- @type InitializerSystemFn ((U...) -> (SystemFn<U...> | (SystemFn<U...>, CleanupFn)))
28
- --- @within Scheduler
29
- --- Initializer system that returns the runtime function, optionally with cleanup
30
-
31
- --- @type CleanupFn (() -> ())
32
- --- @within Scheduler
33
- --- Cleanup function called when system is removed
34
-
35
- --- @interface SystemTable
36
- --- @within Scheduler
37
- --- .system SystemFn<U...> | InitializerSystemFn<U...>
38
- --- .phase Phase?
39
- --- .name string?
40
- --- .runConditions {RunCondition}?
41
- --- .[any] any
42
-
43
- --- @type System SystemFn<U...> | SystemTable<U...>
44
- --- @within Scheduler
45
-
46
- --- @class Scheduler
47
- ---
48
- --- An Object which handles scheduling Systems to run within different
49
- --- Phases. The order of which Systems run will be defined either
50
- --- implicitly by when it was added, or explicitly by tagging the system
51
- --- with a Phase.
52
- local Scheduler = {}
53
- Scheduler.__index = Scheduler
54
-
55
- Scheduler.Hooks = hooks.Hooks
56
-
57
- --- @method addPlugin
58
- --- @within Scheduler
59
- --- @param plugin PlanckPlugin
60
- ---
61
- --- Initializes a plugin with the scheduler, see the [Plugin Docs](/docs/plugins) for more information.
62
- function Scheduler:addPlugin(plugin)
63
- plugin:build(self)
64
- table.insert(self._plugins, plugin)
65
- return self
66
- end
67
-
68
- function Scheduler:_addHook(hook, fn)
69
- assert(self._hooks[hook], `Unknown Hook: {hook}`)
70
- table.insert(self._hooks[hook], fn)
71
- end
72
-
73
- --- @method getDeltaTime
74
- --- @within Scheduler
75
- --- @return number
76
- ---
77
- --- Returns the time since the system was ran last.
78
- --- This must be used within a registered system.
79
- function Scheduler:getDeltaTime()
80
- local systemFn = debug.info(2, "f")
81
- if not systemFn or not self._systemInfo[systemFn] then
82
- error(
83
- "Scheduler:getDeltaTime() must be used within a registered system"
84
- )
85
- end
86
-
87
- return self._systemInfo[systemFn].deltaTime or 0
88
- end
89
-
90
- -- Inspiration from https://github.com/matter-ecs/matter <3
91
- function Scheduler:_handleLogs(systemInfo)
92
- if not systemInfo.timeLastLogged then
93
- systemInfo.timeLastLogged = os.clock()
94
- end
95
-
96
- if not systemInfo.recentLogs then
97
- systemInfo.recentLogs = {}
98
- end
99
-
100
- if os.clock() - systemInfo.timeLastLogged > 10 then
101
- systemInfo.timeLastLogged = os.clock()
102
- systemInfo.recentLogs = {}
103
- end
104
-
105
- local name = systemInfo.name
106
-
107
- for _, logMessage in systemInfo.logs do
108
- if not systemInfo.recentLogs[logMessage] then
109
- task.spawn(error, logMessage, 0)
110
- warn(
111
- `Planck: Error occurred in system{string.len(name) > 0 and ` '{name}'` or ""}, this error will be ignored for 10 seconds`
112
- )
113
- systemInfo.recentLogs[logMessage] = true
114
- end
115
- end
116
-
117
- table.clear(systemInfo.logs)
118
- end
119
-
120
- function Scheduler:runSystem(system, justInitialized)
121
- local systemInfo = self._systemInfo[system]
122
- local now = os.clock()
123
-
124
- if justInitialized ~= true then
125
- if self:_canRun(system) == false then
126
- return
127
- end
128
-
129
- systemInfo.deltaTime = now - (systemInfo.lastTime or now)
130
- end
131
-
132
- systemInfo.lastTime = now
133
-
134
- if not self._thread then
135
- self._thread = coroutine.create(function()
136
- while true do
137
- local fn = coroutine.yield()
138
- self._yielded = true
139
- fn()
140
- self._yielded = false
141
- end
142
- end)
143
-
144
- coroutine.resume(self._thread)
145
- end
146
-
147
- local didYield = false
148
- local hasSystem = false
149
-
150
- local function systemCall()
151
- local function noYield()
152
- local success, errOrSys, cleanup
153
- coroutine.resume(self._thread, function()
154
- success, errOrSys, cleanup = xpcall(function()
155
- return systemInfo.run(table.unpack(self._vargs))
156
- end, function(e)
157
- return debug.traceback(e)
158
- end)
159
- end)
160
-
161
- if success == false then
162
- didYield = true
163
- table.insert(systemInfo.logs, errOrSys)
164
- hooks.systemError(self, systemInfo, errOrSys)
165
- return
166
- end
167
-
168
- if self._yielded then
169
- didYield = true
170
- local source, line = debug.info(self._thread, 1, "sl")
171
- local errMessage = `{source}:{line}: System yielded`
172
- table.insert(
173
- systemInfo.logs,
174
- debug.traceback(self._thread, errMessage, 2)
175
- )
176
- hooks.systemError(
177
- self,
178
- systemInfo,
179
- debug.traceback(self._thread, errMessage, 2)
180
- )
181
- return
182
- end
183
-
184
- if not systemInfo.initialized then
185
- if errOrSys == nil and cleanup == nil then
186
- systemInfo.initialized = true
187
- return
188
- end
189
-
190
- if type(errOrSys) == "function" then
191
- systemInfo.run = errOrSys
192
- systemInfo.initialized = true
193
- if type(cleanup) == "function" then
194
- systemInfo.cleanup = cleanup
195
- end
196
-
197
- hasSystem = true
198
- return
199
- end
200
-
201
- if type(errOrSys) == "table" then
202
- hasSystem = type(errOrSys.system) == "function"
203
- local hasCleanup = type(errOrSys.cleanup) == "function"
204
-
205
- if hasSystem or hasCleanup then
206
- if hasSystem then
207
- systemInfo.run = errOrSys.system
208
- end
209
-
210
- if hasCleanup then
211
- systemInfo.cleanup = errOrSys.cleanup
212
- end
213
-
214
- systemInfo.initialized = true
215
- return
216
- end
217
- end
218
-
219
- local err = string.format(
220
- "System '%s' initializer returned invalid type. "
221
- .. "Expected: function, {system?, cleanup?}, or (function, function). "
222
- .. "Got: %s, %s",
223
- systemInfo.name,
224
- type(errOrSys),
225
- type(cleanup)
226
- )
227
- table.insert(systemInfo.logs, err)
228
- hooks.systemError(self, systemInfo, err)
229
- systemInfo.initialized = true
230
- end
231
- end
232
-
233
- hooks.systemCall(self, "SystemCall", systemInfo, noYield)
234
- end
235
-
236
- local function inner()
237
- hooks.systemCall(self, "InnerSystemCall", systemInfo, systemCall)
238
- end
239
-
240
- local function outer()
241
- hooks.systemCall(self, "OuterSystemCall", systemInfo, inner)
242
- end
243
-
244
- if os.clock() - timeLastLogged > 10 then
245
- timeLastLogged = os.clock()
246
- recentLogs = {}
247
- end
248
-
249
- local success, err: string? = pcall(outer)
250
- if not success and not recentLogs[err] then
251
- task.spawn(error, err, 0)
252
- warn(
253
- `Planck: Error occurred while running hooks, this error will be ignored for 10 seconds`
254
- )
255
- hooks.systemError(
256
- self,
257
- systemInfo,
258
- `Error occurred while running hooks: {err}`
259
- )
260
- recentLogs[err] = true
261
- end
262
-
263
- if didYield then
264
- coroutine.close(self._thread)
265
-
266
- self._thread = coroutine.create(function()
267
- while true do
268
- local fn = coroutine.yield()
269
- self._yielded = true
270
- fn()
271
- self._yielded = false
272
- end
273
- end)
274
-
275
- coroutine.resume(self._thread)
276
- end
277
-
278
- self:_handleLogs(systemInfo)
279
-
280
- if hasSystem and justInitialized ~= true then
281
- self:runSystem(system, true)
282
- end
283
- end
284
-
285
- function Scheduler:runPhase(phase)
286
- if self:_canRun(phase) == false then
287
- return
288
- end
289
-
290
- hooks.phaseBegan(self, phase)
291
-
292
- if not self._phaseToSystems[phase] then
293
- self._phaseToSystems[phase] = {}
294
- end
295
-
296
- for _, system in self._phaseToSystems[phase] do
297
- self:runSystem(system)
298
- end
299
- end
300
-
301
- function Scheduler:runPipeline(pipeline)
302
- if self:_canRun(pipeline) == false then
303
- return
304
- end
305
-
306
- local orderedList = pipeline.dependencyGraph:getOrderedList()
307
- assert(
308
- orderedList,
309
- `Pipeline {pipeline} contains a circular dependency, check it's Phases`
310
- )
311
-
312
- for _, phase in orderedList do
313
- self:runPhase(phase)
314
- end
315
- end
316
-
317
- function Scheduler:_canRun(dependent)
318
- local conditions = self._runIfConditions[dependent]
319
-
320
- if conditions then
321
- for _, runIf in conditions do
322
- if runIf(table.unpack(self._vargs)) == false then
323
- return false
324
- end
325
- end
326
- end
327
-
328
- return true
329
- end
330
-
331
- --- @method run
332
- --- @within Scheduler
333
- --- @param phase Phase
334
- --- @return Scheduler
335
- ---
336
- --- Runs all Systems tagged with the Phase in order.
337
-
338
- --- @method run
339
- --- @within Scheduler
340
- --- @param pipeline Pipeline
341
- --- @return Scheduler
342
- ---
343
- --- Runs all Systems tagged with any Phase within the Pipeline in order.
344
-
345
- --- @method run
346
- --- @within Scheduler
347
- --- @param system System
348
- --- @return Scheduler
349
- ---
350
- --- Runs the System, passing in the arguments of the Scheduler, `U...`.
351
- function Scheduler:run(dependent)
352
- if not dependent then
353
- error("No dependent specified in Scheduler:run(_)")
354
- end
355
-
356
- self:runPipeline(Pipeline.Startup)
357
-
358
- if getSystem(dependent) then
359
- self:runSystem(dependent)
360
- elseif isPhase(dependent) then
361
- self:runPhase(dependent)
362
- elseif isPipeline(dependent) then
363
- self:runPipeline(dependent)
364
- else
365
- error("Unknown dependent passed into Scheduler:run(unknown)")
366
- end
367
-
368
- return self
369
- end
370
-
371
- --- @method runAll
372
- --- @within Scheduler
373
- --- @return Scheduler
374
- ---
375
- --- Runs all Systems within order.
376
- ---
377
- --- :::note
378
- --- When you add a Pipeline or Phase with an event, it will be grouped
379
- --- with other Pipelines/Phases on that event. Otherwise, it will be
380
- --- added to the default group.
381
- ---
382
- --- When not running systems on Events, such as with the `runAll` method,
383
- --- the Default group will be ran first, and then each Event Group in the
384
- --- order created.
385
- ---
386
- --- Pipelines/Phases in these groups are still ordered by their dependencies
387
- --- and by the order of insertion.
388
- --- :::
389
- function Scheduler:runAll()
390
- local orderedDefaults = self._defaultDependencyGraph:getOrderedList()
391
- assert(
392
- orderedDefaults,
393
- "Default Group contains a circular dependency, check your Pipelines/Phases"
394
- )
395
-
396
- for _, dependency in orderedDefaults do
397
- self:run(dependency)
398
- end
399
-
400
- for identifier, dependencyGraph in self._eventDependencyGraphs do
401
- local orderedList = dependencyGraph:getOrderedList()
402
- assert(
403
- orderedDefaults,
404
- `Event Group '{identifier}' contains a circular dependency, check your Pipelines/Phases`
405
- )
406
- for _, dependency in orderedList do
407
- self:run(dependency)
408
- end
409
- end
410
-
411
- return self
412
- end
413
-
414
- --- @method insert
415
- --- @within Scheduler
416
- --- @param phase Phase
417
- --- @return Scheduler
418
- ---
419
- --- Initializes the Phase within the Scheduler, ordering it implicitly by
420
- --- setting it as a dependent of the previous Phase/Pipeline.
421
-
422
- --- @method insert
423
- --- @within Scheduler
424
- --- @param pipeline Pipeline
425
- --- @return Scheduler
426
- ---
427
- --- Initializes the Pipeline and it's Phases within the Scheduler,
428
- --- ordering the Pipeline implicitly by setting it as a dependent
429
- --- of the previous Phase/Pipeline.
430
-
431
- --- @method insert
432
- --- @within Scheduler
433
- --- @param phase Phase
434
- --- @param instance Instance | EventLike
435
- --- @param event string | EventLike
436
- --- @return Scheduler
437
- ---
438
- --- Initializes the Phase within the Scheduler, ordering it implicitly
439
- --- by setting it as a dependent of the previous Phase/Pipeline, and
440
- --- scheduling it to be ran on the specified event.
441
- ---
442
- --- ```lua
443
- --- local myScheduler = Scheduler.new()
444
- --- :insert(myPhase, RunService, "Heartbeat")
445
- --- ```
446
-
447
- --- @method insert
448
- --- @within Scheduler
449
- --- @param pipeline Pipeline
450
- --- @param instance Instance | EventLike
451
- --- @param event string | EventLike
452
- --- @return Scheduler
453
- ---
454
- --- Initializes the Pipeline and it's Phases within the Scheduler,
455
- --- ordering the Pipeline implicitly by setting it as a dependent of
456
- --- the previous Phase/Pipeline, and scheduling it to be ran on the
457
- --- specified event.
458
- ---
459
- --- ```lua
460
- --- local myScheduler = Scheduler.new()
461
- --- :insert(myPipeline, RunService, "Heartbeat")
462
- --- ```
463
- function Scheduler:insert(dependency, instance, event)
464
- assert(
465
- isPhase(dependency) or isPipeline(dependency),
466
- "Unknown dependency passed to Scheduler:insert(unknown, _, _)"
467
- )
468
-
469
- if not instance then
470
- local dependencyGraph = self._defaultDependencyGraph
471
- dependencyGraph:insertBefore(dependency, self._defaultPhase)
472
- else
473
- assert(
474
- isValidEvent(instance, event),
475
- "Unknown instance/event passed to Scheduler:insert(_, instance, event)"
476
- )
477
-
478
- local dependencyGraph = self:_getEventDependencyGraph(instance, event)
479
- dependencyGraph:insert(dependency)
480
- end
481
-
482
- if isPhase(dependency) then
483
- self._phaseToSystems[dependency] = {}
484
- hooks.phaseAdd(self, dependency)
485
- end
486
-
487
- return self
488
- end
489
-
490
- --- @method insertAfter
491
- --- @within Scheduler
492
- --- @param phase Phase
493
- --- @param after Phase | Pipeline
494
- --- @return Scheduler
495
- ---
496
- --- Initializes the Phase within the Scheduler, ordering it
497
- --- explicitly by setting the after Phase/Pipeline as a dependent.
498
-
499
- --- @method insertAfter
500
- --- @within Scheduler
501
- --- @param pipeline Pipeline
502
- --- @param after Phase | Pipeline
503
- --- @return Scheduler
504
- ---
505
- --- Initializes the Pipeline and it's Phases within the Scheduler,
506
- --- ordering the Pipeline explicitly by setting the after Phase/Pipeline
507
- --- as a dependent.
508
- function Scheduler:insertAfter(dependent, after)
509
- assert(
510
- isPhase(after) or isPipeline(after),
511
- "Unknown dependency passed in Scheduler:insertAfter(_, unknown)"
512
- )
513
- assert(
514
- isPhase(dependent) or isPipeline(dependent),
515
- "Unknown dependent passed in Scheduler:insertAfter(unknown, _)"
516
- )
517
-
518
- local dependencyGraph = self:_getGraphOfDependency(after)
519
- dependencyGraph:insertAfter(dependent, after)
520
-
521
- if isPhase(dependent) then
522
- self._phaseToSystems[dependent] = {}
523
- hooks.phaseAdd(self, dependent)
524
- end
525
-
526
- return self
527
- end
528
-
529
- --- @method insertBefore
530
- --- @within Scheduler
531
- --- @param phase Phase
532
- --- @param before Phase | Pipeline
533
- --- @return Scheduler
534
- ---
535
- --- Initializes the Phase within the Scheduler, ordering it
536
- --- explicitly by setting the before Phase/Pipeline as a dependency.
537
-
538
- --- @method insertBefore
539
- --- @within Scheduler
540
- --- @param pipeline Pipeline
541
- --- @param before Phase | Pipeline
542
- --- @return Scheduler
543
- ---
544
- --- Initializes the Pipeline and it's Phases within the Scheduler,
545
- --- ordering the Pipeline explicitly by setting the before Phase/Pipeline
546
- --- as a dependency.
547
- function Scheduler:insertBefore(dependent, before)
548
- assert(
549
- isPhase(before) or isPipeline(before),
550
- "Unknown dependency passed in Scheduler:insertBefore(_, unknown)"
551
- )
552
- assert(
553
- isPhase(dependent) or isPipeline(dependent),
554
- "Unknown dependent passed in Scheduler:insertBefore(unknown, _)"
555
- )
556
-
557
- local dependencyGraph = self:_getGraphOfDependency(before)
558
- dependencyGraph:insertBefore(dependent, before)
559
-
560
- if isPhase(dependent) then
561
- self._phaseToSystems[dependent] = {}
562
- hooks.phaseAdd(self, dependent)
563
- end
564
-
565
- return self
566
- end
567
-
568
- --- @method addSystem
569
- --- @within Scheduler
570
- --- @param system System
571
- --- @param phase Phase?
572
- --- @return Scheduler
573
- ---
574
- --- Adds the System to the Scheduler, scheduling it to be ran
575
- --- implicitly within the provided Phase or on the default Main phase.
576
- ---
577
- --- **Initializer Systems**: Systems can optionally return a function on their
578
- --- first execution, which becomes the runtime system. This allows one-time
579
- --- setup logic without creating separate initialization phases.
580
- ---
581
- --- ```lua
582
- --- local function renderSystem(world, state)
583
- --- -- This runs once on first execution
584
- --- local renderables = world:query(Transform, Model):cached()
585
- ---
586
- --- -- This runs on each subsequent execution
587
- --- return function(world, state)
588
- --- for id, transform, model in renderables do
589
- --- render(transform, model)
590
- --- end
591
- --- end, function()
592
- --- -- Optional cleanup logic runs on removeSystem
593
- --- end
594
- --- end
595
- --- ```
596
- function Scheduler:addSystem(system, phase)
597
- local systemFn = getSystem(system)
598
-
599
- if not systemFn then
600
- error("Unknown system passed to Scheduler:addSystem(unknown, phase?)")
601
- end
602
-
603
- local name = getSystemName(system)
604
-
605
- local scheduledPhase
606
- if phase then
607
- scheduledPhase = phase
608
- elseif type(system) == "table" and system.phase then
609
- scheduledPhase = system.phase
610
- else
611
- scheduledPhase = self._defaultPhase
612
- end
613
-
614
- local systemInfo = {
615
- system = systemFn,
616
- run = systemFn,
617
- cleanup = nil,
618
- phase = scheduledPhase,
619
- name = name,
620
- logs = {},
621
- initialized = false,
622
- }
623
-
624
- self._systemInfo[systemFn] = systemInfo
625
-
626
- if not self._phaseToSystems[systemInfo.phase] then
627
- self._phaseToSystems[systemInfo.phase] = {}
628
- end
629
-
630
- table.insert(self._phaseToSystems[systemInfo.phase], systemFn)
631
-
632
- hooks.systemAdd(self, systemInfo)
633
-
634
- if type(system) == "table" and system.runConditions then
635
- for _, condition in system.runConditions do
636
- condition = if typeof(condition) == "table"
637
- then condition[1]
638
- else condition
639
- self:addRunCondition(systemFn, condition)
640
- end
641
- end
642
-
643
- return self
644
- end
645
-
646
- --- @method addSystems
647
- --- @within Scheduler
648
- --- @param systems { System }
649
- --- @param phase Phase?
650
- ---
651
- --- Adds the Systems to the Scheduler, scheduling them to be ran
652
- --- implicitly within the provided Phase or on the default Main phase.
653
- function Scheduler:addSystems(systems, phase)
654
- if type(systems) ~= "table" then
655
- error("Unknown systems passed to Scheduler:addSystems(unknown, phase?)")
656
- end
657
-
658
- local foundSystem = false
659
- local n = 0
660
-
661
- for _, system in systems do
662
- n += 1
663
- if getSystem(system) then
664
- foundSystem = true
665
- self:addSystem(system, phase)
666
- end
667
- end
668
-
669
- if n == 0 then
670
- error("Empty table passed to Scheduler:addSystems({ }, phase?)")
671
- end
672
-
673
- if not foundSystem then
674
- error(
675
- "Unknown table passed to Scheduler:addSystems({ unknown }, phase?)"
676
- )
677
- end
678
-
679
- return self
680
- end
681
-
682
- --- @method editSystem
683
- --- @within Scheduler
684
- --- @param system System
685
- --- @param newPhase Phase
686
- ---
687
- --- Changes the Phase that this system is scheduled on.
688
- function Scheduler:editSystem(system, newPhase)
689
- local systemFn = getSystem(system)
690
- local systemInfo = self._systemInfo[systemFn]
691
- assert(
692
- systemInfo,
693
- "Attempt to edit a non-existent system in Scheduler:editSystem(_)"
694
- )
695
-
696
- assert(
697
- newPhase and self._phaseToSystems[newPhase] ~= nil or true,
698
- "Phase never initialized before using Scheduler:editSystem(_, Phase)"
699
- )
700
-
701
- local systems = self._phaseToSystems[systemInfo.phase]
702
-
703
- local index = table.find(systems, systemFn)
704
- assert(index, "Unable to find system within phase")
705
-
706
- table.remove(systems, index)
707
-
708
- if not self._phaseToSystems[newPhase] then
709
- self._phaseToSystems[newPhase] = {}
710
- end
711
- table.insert(self._phaseToSystems[newPhase], systemFn)
712
-
713
- systemInfo.phase = newPhase
714
- return self
715
- end
716
-
717
- function Scheduler:_removeCondition(dependent, condition)
718
- self._runIfConditions[dependent] = nil
719
-
720
- for _, _conditions in self._runIfConditions do
721
- if table.find(_conditions, condition) then
722
- return
723
- end
724
- end
725
-
726
- conditions.cleanupCondition(condition)
727
- end
728
-
729
- --- @method removeSystem
730
- --- @within Scheduler
731
- --- @param system System
732
- --- @return Scheduler
733
- ---
734
- --- Removes the System from the Scheduler.
735
- ---
736
- --- If the system provided a cleanup function during initialization,
737
- --- that cleanup function will be executed before removal.
738
- ---
739
- --- ```lua
740
- --- -- System with cleanup
741
- --- local function networkSystem(world, state)
742
- --- local connection = Players.PlayerAdded:Connect(function(player)
743
- --- -- Player joined logic
744
- --- end)
745
- ---
746
- --- return function(world, state)
747
- --- -- Runtime logic
748
- --- end, function()
749
- --- -- Cleanup runs on removeSystem
750
- --- connection:Disconnect()
751
- --- end
752
- --- end
753
- ---
754
- --- scheduler:addSystem(networkSystem, Phase.Update)
755
- --- -- Later...
756
- --- scheduler:removeSystem(networkSystem) -- Cleanup executes here
757
- --- ```
758
- function Scheduler:removeSystem(system)
759
- local systemFn = getSystem(system)
760
- local systemInfo = self._systemInfo[systemFn]
761
- assert(
762
- systemInfo,
763
- "Attempt to remove a non-existent system in Scheduler:removeSystem(_)"
764
- )
765
-
766
- if systemInfo.cleanup then
767
- local success, err =
768
- pcall(systemInfo.cleanup, table.unpack(self._vargs))
769
- if success then
770
- hooks.systemCleanup(self, systemInfo, nil)
771
- else
772
- local errMsg = string.format(
773
- "Cleanup failed for system '%s': %s",
774
- systemInfo.name,
775
- tostring(err)
776
- )
777
- hooks.systemError(self, systemInfo, errMsg)
778
- hooks.systemCleanup(self, systemInfo, errMsg)
779
- end
780
- end
781
-
782
- local systems = self._phaseToSystems[systemInfo.phase]
783
-
784
- local index = table.find(systems, systemFn)
785
- assert(index, "Unable to find system within phase")
786
-
787
- table.remove(systems, index)
788
- self._systemInfo[systemFn] = nil
789
-
790
- if self._runIfConditions[system] then
791
- for _, condition in self._runIfConditions[system] do
792
- self:_removeCondition(system, condition)
793
- end
794
-
795
- self._runIfConditions[system] = nil
796
- end
797
-
798
- hooks.systemRemove(self, systemInfo)
799
-
800
- return self
801
- end
802
-
803
- --- @method replaceSystem
804
- --- @within Scheduler
805
- --- @param old System
806
- --- @param new System
807
- ---
808
- --- Replaces the System with a new System.
809
- function Scheduler:replaceSystem(old, new)
810
- local oldSystemFn = getSystem(old)
811
- local oldSystemInfo = self._systemInfo[oldSystemFn]
812
- assert(
813
- oldSystemInfo,
814
- "Attempt to replace a non-existent system in Scheduler:replaceSystem(unknown, _)"
815
- )
816
-
817
- local newSystemFn = getSystem(new)
818
- assert(
819
- newSystemFn,
820
- "Attempt to pass non-system in Scheduler:replaceSystem(_, unknown)"
821
- )
822
-
823
- if oldSystemInfo.cleanup then
824
- local success, err =
825
- pcall(oldSystemInfo.cleanup, table.unpack(self._vargs))
826
- if success then
827
- hooks.systemCleanup(self, oldSystemInfo, nil)
828
- else
829
- local errMsg = string.format(
830
- "Cleanup failed for system '%s': %s",
831
- oldSystemInfo.name,
832
- tostring(err)
833
- )
834
- hooks.systemError(self, oldSystemInfo, errMsg)
835
- hooks.systemCleanup(self, oldSystemInfo, errMsg)
836
- end
837
- end
838
-
839
- local systems = self._phaseToSystems[oldSystemInfo.phase]
840
-
841
- local index = table.find(systems, oldSystemFn)
842
- assert(index, "Unable to find system within phase")
843
-
844
- table.remove(systems, index)
845
- table.insert(systems, index, newSystemFn)
846
-
847
- local copy = table.clone(oldSystemInfo)
848
-
849
- oldSystemInfo.system = newSystemFn
850
- oldSystemInfo.run = newSystemFn
851
- oldSystemInfo.cleanup = nil
852
- oldSystemInfo.initialized = false
853
- oldSystemInfo.name = getSystemName(new)
854
-
855
- hooks.systemReplace(self, copy, oldSystemInfo)
856
-
857
- self._systemInfo[newSystemFn] = self._systemInfo[oldSystemFn]
858
- self._systemInfo[oldSystemFn] = nil
859
-
860
- return self
861
- end
862
-
863
- --- @method addRunCondition
864
- --- @within Scheduler
865
- --- @param system System
866
- --- @param fn (U...) -> boolean
867
- ---
868
- --- Adds a Run Condition which the Scheduler will check before
869
- --- this System is ran.
870
-
871
- --- @method addRunCondition
872
- --- @within Scheduler
873
- --- @param phase Phase
874
- --- @param fn (U...) -> boolean
875
- ---
876
- --- Adds a Run Condition which the Scheduler will check before
877
- --- any Systems within this Phase are ran.
878
-
879
- --- @method addRunCondition
880
- --- @within Scheduler
881
- --- @param pipeline Pipeline
882
- --- @param fn (U...) -> boolean
883
- ---
884
- --- Adds a Run Condition which the Scheduler will check before
885
- --- any Systems within any Phases apart of this Pipeline are ran.
886
- function Scheduler:addRunCondition(dependent, fn)
887
- fn = if typeof(fn) == "table" then fn[1] else fn
888
-
889
- local system = getSystem(dependent)
890
- if system then
891
- dependent = system
892
- end
893
-
894
- assert(
895
- system or isPhase(dependent) or isPipeline(dependent),
896
- "Attempt to pass unknown dependent into Scheduler:addRunCondition(unknown, _)"
897
- )
898
-
899
- if not self._runIfConditions[dependent] then
900
- self._runIfConditions[dependent] = {}
901
- end
902
-
903
- table.insert(self._runIfConditions[dependent], fn)
904
-
905
- return self
906
- end
907
-
908
- function Scheduler:_addBuiltins()
909
- self._defaultPhase = Phase.new("Default")
910
- self._defaultDependencyGraph = DependencyGraph.new()
911
-
912
- self._defaultDependencyGraph:insert(Pipeline.Startup)
913
- self._defaultDependencyGraph:insert(self._defaultPhase)
914
-
915
- self:addRunCondition(Pipeline.Startup, conditions.runOnce())
916
- for _, phase in Pipeline.Startup.dependencyGraph.nodes do
917
- self:addRunCondition(phase, conditions.runOnce())
918
- end
919
- end
920
-
921
- function Scheduler:_scheduleEvent(instance, event)
922
- local connect = utils.getConnectFunction(instance, event)
923
- assert(
924
- connect,
925
- "Couldn't connect to event as no valid connect methods were found! Ensure the passed event has a 'Connect' or an 'on' method!"
926
- )
927
-
928
- local identifier = getEventIdentifier(instance, event)
929
-
930
- local dependencyGraph = DependencyGraph.new()
931
-
932
- local callback = function()
933
- local orderedList = dependencyGraph:getOrderedList()
934
-
935
- if orderedList == nil then
936
- local err =
937
- `Event Group '{identifier}' contains a circular dependency, check your Pipelines/Phases`
938
- if not recentLogs[err] then
939
- task.spawn(error, err, 0)
940
- warn(
941
- `Planck: Error occurred while running event, this error will be ignored for 10 seconds`
942
- )
943
- recentLogs[err] = true
944
- end
945
- end
946
-
947
- for _, dependency in orderedList do
948
- self:run(dependency)
949
- end
950
- end
951
-
952
- self._connectedEvents[identifier] = connect(callback)
953
- self._eventDependencyGraphs[identifier] = dependencyGraph
954
- end
955
-
956
- function Scheduler:_getEventDependencyGraph(instance, event)
957
- local identifier = getEventIdentifier(instance, event)
958
-
959
- if not self._connectedEvents[identifier] then
960
- self:_scheduleEvent(instance, event)
961
- end
962
-
963
- return self._eventDependencyGraphs[identifier]
964
- end
965
-
966
- function Scheduler:_getGraphOfDependency(dependency)
967
- if table.find(self._defaultDependencyGraph.nodes, dependency) then
968
- return self._defaultDependencyGraph
969
- end
970
-
971
- for _, dependencyGraph in self._eventDependencyGraphs do
972
- if table.find(dependencyGraph.nodes, dependency) then
973
- return dependencyGraph
974
- end
975
- end
976
-
977
- error("Dependency does not belong to a DependencyGraph")
978
- end
979
-
980
- --- @within Scheduler
981
- ---
982
- --- Disconnects all events, closes all threads, and performs
983
- --- other cleanup work.
984
- ---
985
- --- :::danger
986
- --- Only use this if you intend to not use the associated
987
- --- Scheduler anymore. It will not work as intended.
988
- ---
989
- --- You should dereference the scheduler object so that
990
- --- it may be garbage collected.
991
- --- :::
992
- ---
993
- --- :::warning
994
- --- If you're creating a "throwaway" scheduler, you should
995
- --- not add plugins like Jabby or the Matter Debugger to it.
996
- --- These plugins are unable to properly be cleaned up, use
997
- --- them with caution.
998
- --- :::
999
- function Scheduler:cleanup()
1000
- for _, connection in self._connectedEvents do
1001
- utils.disconnectEvent(connection)
1002
- end
1003
-
1004
- for _, plugin in self._plugins do
1005
- if plugin.cleanup then
1006
- plugin:cleanup()
1007
- end
1008
- end
1009
-
1010
- if self._thread then
1011
- coroutine.close(self._thread)
1012
- end
1013
-
1014
- for _, _conditions in self._runIfConditions do
1015
- for _, condition in _conditions do
1016
- conditions.cleanupCondition(condition)
1017
- end
1018
- end
1019
- end
1020
-
1021
- --- @function new
1022
- --- @within Scheduler
1023
- --- @param args U...
1024
- ---
1025
- --- Creates a new Scheduler, the args passed will be passed to
1026
- --- any System anytime it is ran by the Scheduler.
1027
- function Scheduler.new(...)
1028
- local self = {}
1029
-
1030
- self._hooks = {}
1031
-
1032
- self._vargs = { ... }
1033
-
1034
- self._eventDependencyGraphs = {}
1035
- self._connectedEvents = {}
1036
-
1037
- self._phaseToSystems = {}
1038
- self._systemInfo = {}
1039
-
1040
- self._runIfConditions = {}
1041
-
1042
- self._plugins = {}
1043
-
1044
- setmetatable(self, Scheduler)
1045
-
1046
- for _, hookName in hooks.Hooks do
1047
- if not self._hooks[hookName] then
1048
- self._hooks[hookName] = {}
1049
- end
1050
- end
1051
-
1052
- self:_addBuiltins()
1053
-
1054
- return self
1055
- end
1056
-
1057
- return Scheduler
1
+ local DependencyGraph = require(script.Parent.DependencyGraph)
2
+ local Pipeline = require(script.Parent.Pipeline)
3
+ local Phase = require(script.Parent.Phase)
4
+
5
+ type DependencyGraph<T> = DependencyGraph.DependencyGraph<T>
6
+ type Pipeline = Pipeline.Pipeline
7
+ type Phase = Phase.Phase
8
+
9
+ local utils = require(script.Parent.utils)
10
+ local conditions = require(script.Parent.conditions)
11
+
12
+ local getSystem = utils.getSystem
13
+ local getSystemName = utils.getSystemName
14
+ local getEventIdentifier = utils.getEventIdentifier
15
+
16
+ local isSystem = utils.isSystem
17
+ local isPhase = utils.isPhase
18
+ local isPipeline = utils.isPipeline
19
+ local isValidEvent = utils.isValidEvent
20
+
21
+ type GenericTable = utils.GenericTable
22
+ type SignalLike<U...> = utils.SignalLike<U...>
23
+ type ConnectionLike = utils.ConnectionLike
24
+ type ConnectFn<T, U...> = utils.ConnectFn<T, U...>
25
+ type Callback<U...> = utils.Callback<U...>
26
+
27
+ export type SystemFn<U...> = utils.SystemFn<U...>
28
+ export type InitializerSystemFn<U...> = utils.InitializerSystemFn<U...>
29
+ export type InitializerResult<U...> = utils.InitializerResult<U...>
30
+
31
+ type InternalSystem<U...> = utils.InternalSystem<U...>
32
+ export type System<U...> = utils.System<U...>
33
+ export type SystemTable<U...> = utils.SystemTable<U...>
34
+
35
+ type Condition<U...> = conditions.Condition<U...>
36
+
37
+ type Vec<T> = { T }
38
+ type Map<K, V> = { [K]: V }
39
+
40
+ type BasePlugin<T> = {
41
+ build: (self: T, scheduler: Scheduler<...unknown>) -> (),
42
+ new: (...any) -> T,
43
+ }
44
+
45
+ type PluginImpl = {
46
+ __index: PluginImpl,
47
+ build: (self: Plugin, scheduler: Scheduler<...unknown>) -> (),
48
+ cleanup: ((self: Plugin) -> ())?,
49
+ new: (...any) -> Plugin,
50
+ }
51
+
52
+ type Plugin = setmetatable<{}, PluginImpl>
53
+
54
+ type SystemLog<E> = {
55
+ -- An Error object or message
56
+ e: E,
57
+ -- The string that will be printed
58
+ log: string,
59
+ -- The traceback as reported by the Scheduler
60
+ trace: string,
61
+ }
62
+
63
+ export type SystemInfo<U...> = {
64
+ system: InternalSystem<U...>,
65
+ run: InternalSystem<U...>,
66
+ cleanup: SystemFn<U...>?,
67
+ initialized: boolean,
68
+ name: string,
69
+ deltaTime: number?,
70
+ lastTime: number?,
71
+ timeLastLogged: number?,
72
+ recentLogs: { [string]: boolean }?,
73
+ logs: { SystemLog<unknown> },
74
+ phase: Phase,
75
+ }
76
+
77
+ type Dependent<U...> = Pipeline | Phase | System<U...>
78
+ type Dependency = Pipeline | Phase
79
+
80
+ -- Recent errors in Planks itself
81
+ local recentLogs = {}
82
+ local timeLastLogged = os.clock()
83
+
84
+ --- @class Scheduler
85
+ ---
86
+ --- An Object which handles scheduling Systems to run within different
87
+ --- Phases. The order of which Systems run will be defined either
88
+ --- implicitly by when it was added, or explicitly by tagging the system
89
+ --- with a Phase.
90
+ local Scheduler = {}
91
+ Scheduler.__index = Scheduler
92
+
93
+ --- @method addPlugin
94
+ --- @within Scheduler
95
+ --- @param plugin PlanckPlugin
96
+ ---
97
+ --- Initializes a plugin with the scheduler, see the [Plugin Docs](/docs/plugins) for more information.
98
+ function Scheduler.addPlugin<T, U...>(
99
+ self: Scheduler<U...>,
100
+ plugin: setmetatable<{}, BasePlugin<T>>
101
+ )
102
+ local _plugin = plugin :: Plugin
103
+ _plugin:build(self)
104
+
105
+ table.insert(self._plugins, _plugin)
106
+ return self
107
+ end
108
+
109
+ type PartialHookContext = {
110
+ scheduler: Scheduler<...unknown>,
111
+ }
112
+
113
+ type Hook<T> = {
114
+ __T: T,
115
+ }
116
+
117
+ type Hooks = {
118
+ SystemAdd: Hook<(context: SystemHookContext) -> ()>,
119
+ SystemRemove: Hook<(context: SystemHookContext) -> ()>,
120
+ SystemReplace: Hook<(context: SystemReplaceContext) -> ()>,
121
+ SystemEdited: Hook<(context: SystemEditedContext) -> ()>,
122
+ SystemCleanup: Hook<(context: SystemErrorContext) -> ()>,
123
+ SystemError: Hook<(context: SystemErrorContext) -> ()>,
124
+ SystemTriedRun: Hook<(context: SystemHookContext) -> ()>,
125
+
126
+ OuterSystemCall: Hook<(context: SystemCallContext) -> SystemCallHookFn>,
127
+ InnerSystemCall: Hook<(context: SystemCallContext) -> SystemCallHookFn>,
128
+ SystemCall: Hook<(context: SystemCallContext) -> SystemCallHookFn>,
129
+
130
+ PhaseAdd: Hook<(context: PhaseContext) -> ()>,
131
+ PhaseBegan: Hook<(context: PhaseContext) -> ()>,
132
+ }
133
+
134
+ --- @prop Hooks { [string]: string }
135
+ --- @within Scheduler
136
+ --- @since 0.3.0
137
+ ---
138
+ --- See [creating plugins](/docs/plugins/creating).
139
+ Scheduler.Hooks = (
140
+ {
141
+ SystemAdd = "SystemAdd",
142
+ SystemRemove = "SystemRemove",
143
+ SystemReplace = "SystemReplace",
144
+ SystemEdited = "SystemEdited",
145
+ SystemCleanup = "SystemCleanup",
146
+ SystemError = "SystemError",
147
+ SystemTriedRun = "SystemTriedRun",
148
+
149
+ OuterSystemCall = "OuterSystemCall",
150
+ InnerSystemCall = "InnerSystemCall",
151
+ SystemCall = "SystemCall",
152
+
153
+ PhaseAdd = "PhaseAdd",
154
+ PhaseBegan = "PhaseBegan",
155
+ } :: any
156
+ ) :: Hooks
157
+
158
+ type HookId = keyof<typeof(Scheduler.Hooks)>
159
+
160
+ --- @within Scheduler
161
+ --- @since 0.3.0
162
+ ---
163
+ --- Allows for registering hooks, see [creating plugins](/docs/plugins/creating).
164
+ Scheduler.addHook = function<T, U...>(self: Scheduler<U...>, hook: Hook<T>, fn: T)
165
+ local hookId: HookId = hook :: any
166
+
167
+ assert(self._hooks[hookId], `Unknown Hook: {hook}`)
168
+ table.insert(self._hooks[hookId], fn)
169
+ end
170
+
171
+ --- @method _addHook
172
+ --- @within Scheduler
173
+ --- @deprecated 0.3.0 -- Use `Scheduler:addHook()` instead
174
+ --- @private
175
+ ---
176
+ --- Internal method for adding hooks maintained for backwards compatibility.
177
+ --- Use [Scheduler:addHook] instead.
178
+ Scheduler._addHook = Scheduler.addHook
179
+
180
+ local function callHooks<U...>(
181
+ hooks: Vec<(PartialHookContext) -> unknown>,
182
+ info: PartialHookContext
183
+ )
184
+ for _, hook in hooks do
185
+ local success, err = pcall(hook, info)
186
+ if not success then
187
+ warn("Unexpected error in hook:", err)
188
+ end
189
+ end
190
+ end
191
+
192
+ export type SystemHookContext = PartialHookContext & {
193
+ system: SystemInfo<...any>,
194
+ }
195
+
196
+ function Scheduler._systemAdd<U...>(
197
+ self: Scheduler<U...>,
198
+ systemInfo: SystemInfo<U...>
199
+ )
200
+ local context = {
201
+ scheduler = self,
202
+ system = systemInfo,
203
+ }
204
+
205
+ callHooks(self._hooks["SystemAdd"], context)
206
+ end
207
+
208
+ function Scheduler._systemRemove<U...>(
209
+ self: Scheduler<U...>,
210
+ systemInfo: SystemInfo<U...>
211
+ )
212
+ local context: SystemHookContext = {
213
+ scheduler = self,
214
+ system = systemInfo :: any,
215
+ }
216
+
217
+ callHooks(self._hooks["SystemRemove"], context)
218
+ end
219
+
220
+ export type SystemReplaceContext = PartialHookContext & {
221
+ new: SystemInfo<...any>,
222
+ old: SystemInfo<...any>,
223
+ }
224
+
225
+ function Scheduler._systemReplace<U...>(
226
+ self: Scheduler<U...>,
227
+ oldSystemInfo: SystemInfo<U...>,
228
+ newSystemInfo: SystemInfo<U...>
229
+ )
230
+ local context: SystemReplaceContext = {
231
+ scheduler = self,
232
+ new = newSystemInfo :: any,
233
+ old = oldSystemInfo :: any,
234
+ }
235
+
236
+ callHooks(self._hooks["SystemReplace"], context)
237
+ end
238
+
239
+ export type SystemEditedContext = SystemHookContext & {
240
+ old: Phase,
241
+ new: Phase,
242
+ }
243
+
244
+ function Scheduler._systemEdited<U...>(
245
+ self: Scheduler<U...>,
246
+ systemInfo: SystemInfo<U...>,
247
+ old: Phase,
248
+ new: Phase
249
+ )
250
+ local context: SystemEditedContext = {
251
+ scheduler = self,
252
+ system = systemInfo :: any,
253
+ new = new,
254
+ old = old,
255
+ }
256
+
257
+ callHooks(self._hooks["SystemEdited"], context)
258
+ end
259
+
260
+ export type SystemErrorContext = PartialHookContext & {
261
+ system: SystemInfo<...any>,
262
+ error: SystemLog<unknown>?,
263
+ }
264
+
265
+ function Scheduler._systemCleanup<E, U...>(
266
+ self: Scheduler<U...>,
267
+ systemInfo,
268
+ cleanupError: SystemLog<E>?
269
+ )
270
+ local context: SystemErrorContext = {
271
+ scheduler = self,
272
+ system = systemInfo,
273
+ error = cleanupError,
274
+ }
275
+
276
+ callHooks(self._hooks["SystemCleanup"], context)
277
+ end
278
+
279
+ function Scheduler._systemError<E, U...>(
280
+ self: Scheduler<U...>,
281
+ systemInfo,
282
+ err: SystemLog<E>
283
+ )
284
+ local context: SystemErrorContext = {
285
+ scheduler = self,
286
+ system = systemInfo,
287
+ error = err,
288
+ }
289
+
290
+ callHooks(self._hooks["SystemError"], context)
291
+ end
292
+
293
+ function Scheduler._systemTriedRun<T, U...>(self: Scheduler<U...>, systemInfo)
294
+ local context: SystemHookContext = {
295
+ scheduler = self,
296
+ system = systemInfo,
297
+ }
298
+
299
+ callHooks(self._hooks["SystemTriedRun"], context)
300
+ end
301
+
302
+ type SystemHookId = "OuterSystemCall" | "InnerSystemCall" | "SystemCall"
303
+
304
+ type SystemCallHookFn = () -> ()
305
+ type SystemCallCallback = (SystemCallContext) -> SystemCallHookFn
306
+ export type SystemCallContext = PartialHookContext & {
307
+ system: SystemInfo<...any>,
308
+ nextFn: SystemCallHookFn,
309
+ }
310
+
311
+ function Scheduler._systemCall<T, U...>(
312
+ self: Scheduler<U...>,
313
+ hookName: SystemHookId,
314
+ systemInfo: SystemInfo<U...>,
315
+ nextFn: SystemCallHookFn
316
+ )
317
+ for _, hook in self._hooks[hookName] :: Vec<SystemCallCallback> do
318
+ local context: SystemCallContext = {
319
+ scheduler = self,
320
+ system = systemInfo :: any,
321
+ nextFn = nextFn,
322
+ }
323
+
324
+ nextFn = hook(context)
325
+
326
+ if not nextFn then
327
+ local source, line = debug.info(hook, "sl")
328
+ warn(
329
+ `{source}:{line}: Expected 'SystemCall' hook to return a function`
330
+ )
331
+ end
332
+ end
333
+
334
+ nextFn()
335
+ end
336
+
337
+ export type PhaseContext = PartialHookContext & {
338
+ phase: Phase,
339
+ }
340
+
341
+ function Scheduler._phaseAdd<U...>(self: Scheduler<U...>, phase: Phase)
342
+ local context: PhaseContext = {
343
+ scheduler = self,
344
+ phase = phase,
345
+ }
346
+
347
+ callHooks(self._hooks["PhaseAdd"], context)
348
+ end
349
+
350
+ function Scheduler._phaseBegan<U...>(self: Scheduler<U...>, phase: Phase)
351
+ local context: PhaseContext = {
352
+ scheduler = self,
353
+ phase = phase,
354
+ }
355
+
356
+ callHooks(self._hooks["PhaseBegan"], context)
357
+ end
358
+
359
+ --- @method getDeltaTime
360
+ --- @within Scheduler
361
+ --- @return number
362
+ ---
363
+ --- Returns the time since the system was ran last.
364
+ --- This must be used within a registered system.
365
+ function Scheduler.getDeltaTime<U...>(self: Scheduler<U...>)
366
+ if self._currentSystem then
367
+ return self._currentSystem.deltaTime or 0
368
+ end
369
+
370
+ local systemFn = debug.info(2, "f")
371
+ if not systemFn or not self._systemInfo[systemFn] then
372
+ error(
373
+ "Scheduler:getDeltaTime() must be used within a registered system"
374
+ )
375
+ end
376
+
377
+ return self._systemInfo[systemFn].deltaTime or 0
378
+ end
379
+
380
+ function Scheduler._reportLog<E, U...>(
381
+ _: Scheduler<U...>,
382
+ systemInfo: SystemInfo<U...>,
383
+ log: SystemLog<E>,
384
+ -- A warning after the error providing context
385
+ context: string
386
+ )
387
+ local recentLogs = systemInfo.recentLogs
388
+ local logMessage = log.log
389
+
390
+ assert(recentLogs)
391
+
392
+ if not recentLogs[logMessage] then
393
+ -- LUAU FUTURE: Inference bug
394
+ task.spawn<<(unknown, number)>>(error, logMessage, 0)
395
+ warn(
396
+ `Planck: {context},`
397
+ .. " "
398
+ .. `this error will be ignored for 10 seconds`
399
+ )
400
+ recentLogs[logMessage] = true
401
+ end
402
+ end
403
+
404
+ -- Inspiration from https://github.com/matter-ecs/matter <3
405
+ function Scheduler._handleLogs<U...>(
406
+ self: Scheduler<U...>,
407
+ systemInfo: SystemInfo<U...>
408
+ )
409
+ if not systemInfo.timeLastLogged then
410
+ systemInfo.timeLastLogged = os.clock()
411
+ end
412
+
413
+ if not systemInfo.recentLogs then
414
+ systemInfo.recentLogs = {}
415
+ end
416
+
417
+ -- selene: allow(incorrect_standard_library_use)
418
+ do
419
+ assert(systemInfo.timeLastLogged)
420
+ assert(systemInfo.recentLogs)
421
+ end
422
+
423
+ if os.clock() - systemInfo.timeLastLogged > 10 then
424
+ systemInfo.timeLastLogged = os.clock()
425
+ systemInfo.recentLogs = {}
426
+ end
427
+
428
+ local name = systemInfo.name
429
+
430
+ for _, log in systemInfo.logs do
431
+ self:_reportLog(
432
+ systemInfo,
433
+ log,
434
+ `Error occurred in system{string.len(name) > 0 and ` '{name}'` or ""}`
435
+ )
436
+ end
437
+
438
+ table.clear(systemInfo.logs)
439
+ end
440
+
441
+ function Scheduler.runSystem<U...>(
442
+ self: Scheduler<U...>,
443
+ system: InternalSystem<U...>,
444
+ justInitialized: boolean
445
+ )
446
+ local systemInfo = self._systemInfo[system]
447
+ local now = os.clock()
448
+
449
+ if not systemInfo then
450
+ error(
451
+ "Attempted to run a non-registered system, make sure it is added to the Scheduler"
452
+ )
453
+ end
454
+
455
+ if justInitialized ~= true then
456
+ if self:_canRun(system) == false then
457
+ self:_systemTriedRun(systemInfo)
458
+ return
459
+ end
460
+
461
+ systemInfo.deltaTime = now - (systemInfo.lastTime or now)
462
+ end
463
+
464
+ systemInfo.lastTime = now
465
+ self._currentSystem = systemInfo
466
+
467
+ if not self._thread then
468
+ self._thread = coroutine.create(function()
469
+ while true do
470
+ local fn = coroutine.yield()
471
+ self._yielded = true
472
+ fn()
473
+ self._yielded = false
474
+ end
475
+ end)
476
+
477
+ coroutine.resume(self._thread)
478
+ end
479
+
480
+ -- selene: allow(incorrect_standard_library_use)
481
+ assert(self._thread)
482
+
483
+ local didYield = false
484
+ local hasSystem = false
485
+
486
+ local function systemCall()
487
+ local function noYield()
488
+ local success, errOrSys, cleanup
489
+
490
+ coroutine.resume(self._thread, function()
491
+ success, errOrSys, cleanup = xpcall(function()
492
+ return systemInfo.run(table.unpack(self._vargs))
493
+ end, function(e)
494
+ return {
495
+ e = e,
496
+ log = debug.traceback(tostring(e)),
497
+ trace = debug.traceback(),
498
+ }
499
+ end)
500
+ end)
501
+
502
+ if success == false then
503
+ didYield = true
504
+
505
+ -- selene: allow(incorrect_standard_library_use)
506
+ assert(errOrSys)
507
+ table.insert(systemInfo.logs, errOrSys)
508
+ self:_systemError(systemInfo, errOrSys)
509
+
510
+ return
511
+ end
512
+
513
+ if self._yielded then
514
+ didYield = true
515
+
516
+ local source, line = debug.info(self._thread, 1, "sl")
517
+ local errMessage = `{source}:{line}: System yielded`
518
+ local err = debug.traceback(self._thread, errMessage, 2)
519
+
520
+ local log = {
521
+ e = err,
522
+ log = err,
523
+ trace = debug.traceback(self._thread, nil, 2),
524
+ }
525
+
526
+ table.insert(systemInfo.logs, log :: SystemLog<unknown>)
527
+ self:_systemError(systemInfo, log)
528
+ return
529
+ end
530
+
531
+ if not systemInfo.initialized then
532
+ if errOrSys == nil and cleanup == nil then
533
+ systemInfo.initialized = true
534
+ return
535
+ end
536
+
537
+ if type(errOrSys) == "function" then
538
+ systemInfo.run = errOrSys
539
+ systemInfo.initialized = true
540
+ if type(cleanup) == "function" then
541
+ systemInfo.cleanup = cleanup
542
+ end
543
+
544
+ hasSystem = true
545
+ return
546
+ end
547
+
548
+ if type(errOrSys) == "table" then
549
+ hasSystem = type(errOrSys.system) == "function"
550
+ local hasCleanup = type(errOrSys.cleanup) == "function"
551
+
552
+ if hasSystem or hasCleanup then
553
+ if hasSystem then
554
+ systemInfo.run = errOrSys.system
555
+ end
556
+
557
+ if hasCleanup then
558
+ systemInfo.cleanup = errOrSys.cleanup
559
+ end
560
+
561
+ systemInfo.initialized = true
562
+ return
563
+ end
564
+ end
565
+
566
+ local errMessage = `System '{systemInfo.name}' initializer returned invalid type. `
567
+ .. "Expected: function, {system?, cleanup?}, or (function, function). "
568
+ .. `Got: {typeof(errOrSys)}, {typeof(cleanup)}`
569
+
570
+ local err = debug.traceback(errMessage)
571
+ local log = {
572
+ e = err,
573
+ log = err,
574
+ trace = debug.traceback(),
575
+ }
576
+
577
+ table.insert(systemInfo.logs, log :: SystemLog<unknown>)
578
+ self:_systemError(systemInfo, log)
579
+ systemInfo.initialized = true
580
+ end
581
+ end
582
+
583
+ self:_systemCall("SystemCall", systemInfo, noYield)
584
+ end
585
+
586
+ local function inner()
587
+ self:_systemCall("InnerSystemCall", systemInfo, systemCall)
588
+ end
589
+
590
+ local function outer()
591
+ self:_systemCall("OuterSystemCall", systemInfo, inner)
592
+ end
593
+
594
+ if os.clock() - timeLastLogged > 10 then
595
+ timeLastLogged = os.clock()
596
+ recentLogs = {}
597
+ end
598
+
599
+ -- LUAU FUTURE: Better types for pcalls
600
+ local success, err = xpcall(outer :: any, function(e)
601
+ return {
602
+ e = e,
603
+ log = debug.traceback(`Error occurred while running hooks: {e}`),
604
+ trace = debug.traceback(),
605
+ }
606
+ end)
607
+
608
+ if not success then
609
+ self:_systemError(systemInfo, err)
610
+ self:_reportLog(systemInfo, err, `Error occurred while running hooks`)
611
+ end
612
+
613
+ if didYield then
614
+ coroutine.close(self._thread)
615
+
616
+ self._thread = coroutine.create(function()
617
+ while true do
618
+ local fn = coroutine.yield()
619
+ self._yielded = true
620
+ fn()
621
+ self._yielded = false
622
+ end
623
+ end)
624
+
625
+ coroutine.resume(self._thread)
626
+ end
627
+
628
+ self:_handleLogs(systemInfo)
629
+ self._currentSystem = nil
630
+
631
+ if hasSystem and justInitialized ~= true then
632
+ self:runSystem(system, true)
633
+ end
634
+ end
635
+
636
+ function Scheduler.runPhase<U...>(self: Scheduler<U...>, phase: Phase)
637
+ if self:_canRun(phase) == false then
638
+ return
639
+ end
640
+
641
+ self:_phaseBegan(phase)
642
+
643
+ if not self._phaseToSystems[phase] then
644
+ self._phaseToSystems[phase] = {}
645
+ end
646
+
647
+ for _, system in self._phaseToSystems[phase] do
648
+ self:runSystem(system, false)
649
+ end
650
+ end
651
+
652
+ function Scheduler.runPipeline<U...>(self: Scheduler<U...>, pipeline: Pipeline)
653
+ if self:_canRun(pipeline) == false then
654
+ return
655
+ end
656
+
657
+ local orderedList = pipeline.dependencyGraph:getOrderedList()
658
+ assert(
659
+ orderedList,
660
+ `Pipeline {pipeline} contains a circular dependency, check it's Phases`
661
+ )
662
+
663
+ for _, phase in orderedList do
664
+ self:runPhase(phase)
665
+ end
666
+ end
667
+
668
+ function Scheduler._canRun<U...>(self: Scheduler<U...>, dependent: Dependent<U...>)
669
+ local runConditions = self._runIfConditions[dependent :: any]
670
+
671
+ if runConditions then
672
+ for _, runIf in runConditions do
673
+ if runIf and not runIf(table.unpack(self._vargs)) then
674
+ return false
675
+ end
676
+ end
677
+ end
678
+
679
+ return true
680
+ end
681
+
682
+ --- @method run
683
+ --- @within Scheduler
684
+ --- @param phase Phase
685
+ --- @return Scheduler
686
+ ---
687
+ --- Runs all Systems tagged with the Phase in order.
688
+
689
+ --- @method run
690
+ --- @within Scheduler
691
+ --- @param pipeline Pipeline
692
+ --- @return Scheduler
693
+ ---
694
+ --- Runs all Systems tagged with any Phase within the Pipeline in order.
695
+
696
+ --- @method run
697
+ --- @within Scheduler
698
+ --- @param system System
699
+ --- @return Scheduler
700
+ ---
701
+ --- Runs the System, passing in the arguments of the Scheduler, `U...`.
702
+ function Scheduler.run<U...>(self: Scheduler<U...>, dependent: Dependent<U...>)
703
+ assert(
704
+ dependent ~= nil,
705
+ "No dependent specified in Scheduler:run(dependent?)"
706
+ )
707
+
708
+ self:runPipeline(Pipeline.Startup)
709
+
710
+ if isSystem(dependent) then
711
+ self:runSystem(dependent :: any, false)
712
+ elseif isPhase(dependent) then
713
+ self:runPhase(dependent :: any)
714
+ elseif isPipeline(dependent) then
715
+ self:runPipeline(dependent :: any)
716
+ else
717
+ error("Unknown dependent passed into Scheduler:run(unknown)")
718
+ end
719
+
720
+ return self
721
+ end
722
+
723
+ --- @method runAll
724
+ --- @within Scheduler
725
+ --- @return Scheduler
726
+ ---
727
+ --- Runs all Systems within order.
728
+ ---
729
+ --- :::note
730
+ --- When you add a Pipeline or Phase with an event, it will be grouped
731
+ --- with other Pipelines/Phases on that event. Otherwise, it will be
732
+ --- added to the default group.
733
+ ---
734
+ --- When not running systems on Events, such as with the `runAll` method,
735
+ --- the Default group will be ran first, and then each Event Group in the
736
+ --- order created.
737
+ ---
738
+ --- Pipelines/Phases in these groups are still ordered by their dependencies
739
+ --- and by the order of insertion.
740
+ --- :::
741
+ function Scheduler.runAll<U...>(self: Scheduler<U...>)
742
+ local orderedDefaults = self._defaultDependencyGraph:getOrderedList()
743
+ assert(
744
+ orderedDefaults,
745
+ "Default Group contains a circular dependency, check your Pipelines/Phases"
746
+ )
747
+
748
+ for _, dependency in orderedDefaults do
749
+ self:run(dependency)
750
+ end
751
+
752
+ for identifier, dependencyGraph in self._eventDependencyGraphs do
753
+ local orderedList = dependencyGraph:getOrderedList()
754
+ assert(
755
+ orderedList,
756
+ `Event Group '{identifier}' contains a circular dependency, check your Pipelines/Phases`
757
+ )
758
+
759
+ for _, dependency in orderedList do
760
+ self:run(dependency)
761
+ end
762
+ end
763
+
764
+ return self
765
+ end
766
+
767
+ type InsertFn =
768
+ & (<U...>(
769
+ self: Scheduler<U...>,
770
+ dependency: Phase | Pipeline
771
+ ) -> Scheduler<U...>)
772
+ -- RBXScriptSignal & nil
773
+ & (<U...>(
774
+ self: Scheduler<U...>,
775
+ dependency: Phase | Pipeline,
776
+ signal: RBXScriptSignal<U...>
777
+ ) -> Scheduler<U...>)
778
+ -- Instance & RBXScriptSignal
779
+ -- Instance & string
780
+ & (<U...>(
781
+ self: Scheduler<U...>,
782
+ dependency: Phase | Pipeline,
783
+ instance: Instance,
784
+ event: RBXScriptSignal<U...> | string
785
+ ) -> Scheduler<U...>)
786
+ -- SignalLike & nil
787
+ & (<U...>(
788
+ self: Scheduler<U...>,
789
+ dependency: Phase | Pipeline,
790
+ signal: SignalLike<U...>
791
+ ) -> Scheduler<U...>)
792
+ -- table & string
793
+ & (<U...>(
794
+ self: Scheduler<U...>,
795
+ dependency: Phase | Pipeline,
796
+ table: GenericTable,
797
+ event: string
798
+ ) -> Scheduler<U...>)
799
+ -- table & connectable method
800
+ & (<T, U...>(
801
+ self: Scheduler<U...>,
802
+ dependency: Phase | Pipeline,
803
+ instance: GenericTable,
804
+ connectMethod: (GenericTable, Callback<U...>, ...any) -> T
805
+ ) -> Scheduler<U...>)
806
+ -- connectable function
807
+ & (<T, U...>(
808
+ self: Scheduler<U...>,
809
+ dependency: Phase | Pipeline,
810
+ connectFn: (Callback<U...>, ...any) -> T
811
+ ) -> Scheduler<U...>)
812
+
813
+ --- @method insert
814
+ --- @within Scheduler
815
+ --- @param phase Phase
816
+ --- @return Scheduler
817
+ ---
818
+ --- Initializes the Phase within the Scheduler, ordering it implicitly by
819
+ --- setting it as a dependent of the previous Phase/Pipeline.
820
+
821
+ --- @method insert
822
+ --- @within Scheduler
823
+ --- @param pipeline Pipeline
824
+ --- @return Scheduler
825
+ ---
826
+ --- Initializes the Pipeline and it's Phases within the Scheduler,
827
+ --- ordering the Pipeline implicitly by setting it as a dependent
828
+ --- of the previous Phase/Pipeline.
829
+
830
+ --- @method insert
831
+ --- @within Scheduler
832
+ --- @param phase Phase
833
+ --- @param instance Instance | EventLike
834
+ --- @param event string | EventLike
835
+ --- @return Scheduler
836
+ ---
837
+ --- Initializes the Phase within the Scheduler, ordering it implicitly
838
+ --- by setting it as a dependent of the previous Phase/Pipeline, and
839
+ --- scheduling it to be ran on the specified event.
840
+ ---
841
+ --- ```lua
842
+ --- local myScheduler = Scheduler.new()
843
+ --- :insert(myPhase, RunService, "Heartbeat")
844
+ --- ```
845
+
846
+ --- @method insert
847
+ --- @within Scheduler
848
+ --- @param pipeline Pipeline
849
+ --- @param instance Instance | EventLike
850
+ --- @param event string | EventLike
851
+ --- @return Scheduler
852
+ ---
853
+ --- Initializes the Pipeline and it's Phases within the Scheduler,
854
+ --- ordering the Pipeline implicitly by setting it as a dependent of
855
+ --- the previous Phase/Pipeline, and scheduling it to be ran on the
856
+ --- specified event.
857
+ ---
858
+ --- ```lua
859
+ --- local myScheduler = Scheduler.new()
860
+ --- :insert(myPipeline, RunService, "Heartbeat")
861
+ --- ```
862
+ local insertFn: InsertFn = function<U...>(
863
+ self: Scheduler<U...>,
864
+ dependency: Phase | Pipeline,
865
+ instance: any,
866
+ event: any
867
+ ): Scheduler<U...>
868
+ assert(
869
+ isPhase(dependency) or isPipeline(dependency),
870
+ "Unknown dependency passed to Scheduler:insert(unknown, _, _)"
871
+ )
872
+
873
+ if not instance then
874
+ local dependencyGraph = self._defaultDependencyGraph
875
+ dependencyGraph:insertBefore(dependency, self._defaultPhase)
876
+ else
877
+ assert(
878
+ isValidEvent(instance, event),
879
+ "Unknown instance/event passed to Scheduler:insert(_, instance, event)"
880
+ )
881
+
882
+ local dependencyGraph = self:_getEventDependencyGraph(instance, event)
883
+ dependencyGraph:insert(dependency)
884
+ end
885
+
886
+ if isPhase(dependency) then
887
+ self._phaseToSystems[dependency :: Phase] = {}
888
+ self:_phaseAdd(dependency :: Phase)
889
+ end
890
+
891
+ return self
892
+ end
893
+
894
+ Scheduler.insert = insertFn
895
+
896
+ --- @method insertAfter
897
+ --- @within Scheduler
898
+ --- @param phase Phase
899
+ --- @param after Phase | Pipeline
900
+ --- @return Scheduler
901
+ ---
902
+ --- Initializes the Phase within the Scheduler, ordering it
903
+ --- explicitly by setting the after Phase/Pipeline as a dependent.
904
+
905
+ --- @method insertAfter
906
+ --- @within Scheduler
907
+ --- @param pipeline Pipeline
908
+ --- @param after Phase | Pipeline
909
+ --- @return Scheduler
910
+ ---
911
+ --- Initializes the Pipeline and it's Phases within the Scheduler,
912
+ --- ordering the Pipeline explicitly by setting the after Phase/Pipeline
913
+ --- as a dependent.
914
+ function Scheduler.insertAfter<U...>(
915
+ self: Scheduler<U...>,
916
+ dependent: Dependency,
917
+ after: Dependency
918
+ )
919
+ assert(
920
+ isPhase(after) or isPipeline(after),
921
+ "Unknown dependency passed in Scheduler:insertAfter(_, unknown)"
922
+ )
923
+ assert(
924
+ isPhase(dependent) or isPipeline(dependent),
925
+ "Unknown dependent passed in Scheduler:insertAfter(unknown, _)"
926
+ )
927
+
928
+ local dependencyGraph = self:_getGraphOfDependency(after)
929
+ dependencyGraph:insertAfter(dependent, after)
930
+
931
+ if isPhase(dependent) then
932
+ self._phaseToSystems[dependent :: Phase] = {}
933
+ self:_phaseAdd(dependent :: Phase)
934
+ end
935
+
936
+ return self
937
+ end
938
+
939
+ --- @method insertBefore
940
+ --- @within Scheduler
941
+ --- @param phase Phase
942
+ --- @param before Phase | Pipeline
943
+ --- @return Scheduler
944
+ ---
945
+ --- Initializes the Phase within the Scheduler, ordering it
946
+ --- explicitly by setting the before Phase/Pipeline as a dependency.
947
+
948
+ --- @method insertBefore
949
+ --- @within Scheduler
950
+ --- @param pipeline Pipeline
951
+ --- @param before Phase | Pipeline
952
+ --- @return Scheduler
953
+ ---
954
+ --- Initializes the Pipeline and it's Phases within the Scheduler,
955
+ --- ordering the Pipeline explicitly by setting the before Phase/Pipeline
956
+ --- as a dependency.
957
+ function Scheduler.insertBefore<U...>(
958
+ self: Scheduler<U...>,
959
+ dependent: Dependency,
960
+ before: Dependency
961
+ )
962
+ assert(
963
+ isPhase(before) or isPipeline(before),
964
+ "Unknown dependency passed in Scheduler:insertBefore(_, unknown)"
965
+ )
966
+ assert(
967
+ isPhase(dependent) or isPipeline(dependent),
968
+ "Unknown dependent passed in Scheduler:insertBefore(unknown, _)"
969
+ )
970
+
971
+ local dependencyGraph = self:_getGraphOfDependency(before)
972
+ dependencyGraph:insertBefore(dependent, before)
973
+
974
+ if isPhase(dependent) then
975
+ self._phaseToSystems[dependent :: Phase] = {}
976
+ self:_phaseAdd(dependent :: Phase)
977
+ end
978
+
979
+ return self
980
+ end
981
+
982
+ --- @method addSystem
983
+ --- @within Scheduler
984
+ --- @param system System
985
+ --- @param phase Phase?
986
+ --- @return Scheduler
987
+ ---
988
+ --- Adds the System to the Scheduler, scheduling it to be ran
989
+ --- implicitly within the provided Phase or on the default Main phase.
990
+ ---
991
+ --- **Initializer Systems**: Systems can optionally return a function on their
992
+ --- first execution, which becomes the runtime system. This allows one-time
993
+ --- setup logic without creating separate initialization phases.
994
+ ---
995
+ --- ```lua
996
+ --- local function renderSystem(world, state)
997
+ --- -- This runs once on first execution
998
+ --- local renderables = world:query(Transform, Model):cached()
999
+ ---
1000
+ --- -- This runs on each subsequent execution
1001
+ --- return function(world, state)
1002
+ --- for id, transform, model in renderables do
1003
+ --- render(transform, model)
1004
+ --- end
1005
+ --- end, function()
1006
+ --- -- Optional cleanup logic runs on removeSystem
1007
+ --- end
1008
+ --- end
1009
+ --- ```
1010
+ function Scheduler.addSystem<U...>(
1011
+ self: Scheduler<U...>,
1012
+ system: System<U...>,
1013
+ phase: Phase?
1014
+ )
1015
+ local systemFn = getSystem(system)
1016
+ assert(systemFn, "Unknown system passed to Scheduler:addSystem(unknown, _)")
1017
+
1018
+ local name = getSystemName(system)
1019
+
1020
+ local scheduledPhase
1021
+ if phase then
1022
+ scheduledPhase = phase
1023
+ elseif type(system) == "table" and system.phase then
1024
+ scheduledPhase = system.phase
1025
+ else
1026
+ scheduledPhase = self._defaultPhase
1027
+ end
1028
+
1029
+ local systemInfo: SystemInfo<U...> = {
1030
+ system = systemFn,
1031
+ run = systemFn,
1032
+ cleanup = nil,
1033
+ phase = scheduledPhase,
1034
+ name = name,
1035
+ logs = {},
1036
+ initialized = false,
1037
+ }
1038
+
1039
+ self._systemInfo[systemFn] = systemInfo :: any
1040
+
1041
+ if not self._phaseToSystems[systemInfo.phase] then
1042
+ self._phaseToSystems[systemInfo.phase] = {}
1043
+ end
1044
+
1045
+ table.insert(
1046
+ self._phaseToSystems[systemInfo.phase],
1047
+ systemFn :: InternalSystem<...any>
1048
+ )
1049
+
1050
+ self:_systemAdd(systemInfo)
1051
+
1052
+ if type(system) == "table" and system.runConditions then
1053
+ for _, condition in system.runConditions do
1054
+ condition = if typeof(condition) == "table"
1055
+ then condition[1]
1056
+ else condition
1057
+ self:addRunCondition(systemFn, condition)
1058
+ end
1059
+ end
1060
+
1061
+ return self
1062
+ end
1063
+
1064
+ --- @method addSystems
1065
+ --- @within Scheduler
1066
+ --- @param systems { System }
1067
+ --- @param phase Phase?
1068
+ ---
1069
+ --- Adds the Systems to the Scheduler, scheduling them to be ran
1070
+ --- implicitly within the provided Phase or on the default Main phase.
1071
+ function Scheduler.addSystems<U...>(
1072
+ self: Scheduler<U...>,
1073
+ systems: { System<U...> },
1074
+ phase: Phase?
1075
+ )
1076
+ assert(
1077
+ type(systems) == "table",
1078
+ "Unknown value passed to Scheduler:addSystems(unknown, _). This value should be an array."
1079
+ )
1080
+
1081
+ local foundSystem = false
1082
+ local n = 0
1083
+
1084
+ for _, system in systems do
1085
+ n += 1
1086
+ if getSystem(system) then
1087
+ foundSystem = true
1088
+ self:addSystem(system, phase)
1089
+ end
1090
+ end
1091
+
1092
+ assert(n ~= 0, "Empty table passed to Scheduler:addSystems({ }, _)")
1093
+
1094
+ assert(
1095
+ foundSystem,
1096
+ "Table containing unknown values passed to Scheduler:addSystems({ unknown }, _)"
1097
+ )
1098
+
1099
+ return self
1100
+ end
1101
+
1102
+ --- @method editSystem
1103
+ --- @within Scheduler
1104
+ --- @param system System
1105
+ --- @param newPhase Phase
1106
+ ---
1107
+ --- Changes the Phase that this system is scheduled on.
1108
+ function Scheduler.editSystem<U...>(
1109
+ self: Scheduler<U...>,
1110
+ system: System<U...>,
1111
+ newPhase: Phase
1112
+ )
1113
+ local systemFn = getSystem(system)
1114
+ assert(
1115
+ systemFn,
1116
+ "Unknown system passed to Scheduler:editSystem(unknown, _)"
1117
+ )
1118
+
1119
+ local systemInfo = self._systemInfo[systemFn]
1120
+ assert(
1121
+ systemInfo,
1122
+ "Attempt to edit a non-existent system in Scheduler:editSystem(system, _)"
1123
+ )
1124
+
1125
+ assert(
1126
+ newPhase and self._phaseToSystems[newPhase] ~= nil or true,
1127
+ "Phase never initialized before using Scheduler:editSystem(_, phase)"
1128
+ )
1129
+
1130
+ local oldPhase = systemInfo.phase
1131
+ local systems = self._phaseToSystems[oldPhase]
1132
+
1133
+ local index = table.find(systems, systemFn :: InternalSystem<...any>)
1134
+ assert(index, "Unable to find system within phase")
1135
+
1136
+ table.remove(systems, index)
1137
+
1138
+ if not self._phaseToSystems[newPhase] then
1139
+ self._phaseToSystems[newPhase] = {}
1140
+ end
1141
+ table.insert(
1142
+ self._phaseToSystems[newPhase],
1143
+ systemFn :: InternalSystem<...any>
1144
+ )
1145
+
1146
+ systemInfo.phase = newPhase
1147
+ self:_systemEdited(systemInfo :: SystemInfo<U...>, oldPhase, newPhase)
1148
+
1149
+ return self
1150
+ end
1151
+
1152
+ function Scheduler._removeCondition<U...>(
1153
+ self: Scheduler<U...>,
1154
+ dependent: Dependent<U...>,
1155
+ condition: Condition<U...>
1156
+ )
1157
+ self._runIfConditions[dependent :: any] = nil
1158
+
1159
+ for _, _conditions in self._runIfConditions do
1160
+ if table.find(_conditions :: any, condition) then
1161
+ return
1162
+ end
1163
+ end
1164
+
1165
+ conditions.cleanupCondition(condition)
1166
+ end
1167
+
1168
+ --- @method removeSystem
1169
+ --- @within Scheduler
1170
+ --- @param system System
1171
+ --- @return Scheduler
1172
+ ---
1173
+ --- Removes the System from the Scheduler.
1174
+ ---
1175
+ --- If the system provided a cleanup function during initialization,
1176
+ --- that cleanup function will be executed before removal.
1177
+ ---
1178
+ --- ```lua
1179
+ --- -- System with cleanup
1180
+ --- local function networkSystem(world, state)
1181
+ --- local connection = Players.PlayerAdded:Connect(function(player)
1182
+ --- -- Player joined logic
1183
+ --- end)
1184
+ ---
1185
+ --- return function(world, state)
1186
+ --- -- Runtime logic
1187
+ --- end, function()
1188
+ --- -- Cleanup runs on removeSystem
1189
+ --- connection:Disconnect()
1190
+ --- end
1191
+ --- end
1192
+ ---
1193
+ --- scheduler:addSystem(networkSystem, Phase.Update)
1194
+ --- -- Later...
1195
+ --- scheduler:removeSystem(networkSystem) -- Cleanup executes here
1196
+ --- ```
1197
+ function Scheduler.removeSystem<U...>(self: Scheduler<U...>, system: System<U...>)
1198
+ local systemFn = getSystem(system)
1199
+ assert(systemFn, "Invalid system passed to Scheduler:removeSystem(system)")
1200
+
1201
+ local systemInfo = self._systemInfo[systemFn]
1202
+ assert(
1203
+ systemInfo,
1204
+ "Attempt to remove a non-existent system in Scheduler:removeSystem(system)"
1205
+ )
1206
+
1207
+ if systemInfo.cleanup then
1208
+ local success: boolean, err: SystemLog<unknown> = xpcall<<unknown, (nil), (
1209
+ SystemLog<unknown>
1210
+ )>>(systemInfo.cleanup, function(e)
1211
+ local errMessage =
1212
+ `Cleanup failed for system '{systemInfo.name}': {e}`
1213
+
1214
+ return {
1215
+ e = e,
1216
+ log = debug.traceback(errMessage),
1217
+ trace = debug.traceback(),
1218
+ }
1219
+ end, table.unpack(self._vargs))
1220
+
1221
+ if success then
1222
+ self:_systemCleanup(systemInfo, nil)
1223
+ else
1224
+ self:_systemError(systemInfo, err)
1225
+ self:_systemCleanup(systemInfo, err)
1226
+ end
1227
+ end
1228
+
1229
+ local systems = self._phaseToSystems[systemInfo.phase]
1230
+
1231
+ local index = table.find(systems :: any, systemFn)
1232
+ assert(index, "Unable to find system within phase")
1233
+
1234
+ table.remove(systems, index)
1235
+ self._systemInfo[systemFn] = nil
1236
+
1237
+ if self._runIfConditions[system :: any] then
1238
+ for _, condition in self._runIfConditions[system :: any] do
1239
+ self:_removeCondition(system, condition)
1240
+ end
1241
+
1242
+ self._runIfConditions[system :: any] = nil
1243
+ end
1244
+
1245
+ self:_systemRemove(systemInfo :: SystemInfo<U...>)
1246
+
1247
+ return self
1248
+ end
1249
+
1250
+ --- @method replaceSystem
1251
+ --- @within Scheduler
1252
+ --- @param old System
1253
+ --- @param new System
1254
+ ---
1255
+ --- Replaces the System with a new System.
1256
+ function Scheduler.replaceSystem<U...>(
1257
+ self: Scheduler<U...>,
1258
+ old: System<U...>,
1259
+ new: System<U...>
1260
+ )
1261
+ local oldSystemFn = getSystem(old) :: InternalSystem<...any>
1262
+ assert(
1263
+ oldSystemFn,
1264
+ "Attempt to pass non-system in Scheduler:replaceSystem(unknown, _)"
1265
+ )
1266
+
1267
+ local oldSystemInfo = self._systemInfo[oldSystemFn]
1268
+ assert(
1269
+ oldSystemInfo,
1270
+ "Attempt to replace a non-existent system in Scheduler:replaceSystem(system, _)"
1271
+ )
1272
+
1273
+ local newSystemFn = getSystem(new) :: InternalSystem<...any>
1274
+ assert(
1275
+ newSystemFn,
1276
+ "Attempt to pass non-system in Scheduler:replaceSystem(_, unknown)"
1277
+ )
1278
+
1279
+ if oldSystemInfo.cleanup then
1280
+ -- LUAU FUTURE: Better types for xpcalls
1281
+ local success: boolean, err: SystemLog<unknown> = xpcall<<unknown, (nil), (
1282
+ SystemLog<unknown>
1283
+ )>>(oldSystemInfo.cleanup, function(e)
1284
+ local errMessage =
1285
+ `Cleanup failed for system '{oldSystemInfo.name}': {e}`
1286
+
1287
+ return {
1288
+ e = e,
1289
+ log = debug.traceback(errMessage),
1290
+ trace = debug.traceback(),
1291
+ }
1292
+ end, table.unpack(self._vargs))
1293
+
1294
+ if success then
1295
+ self:_systemCleanup(oldSystemInfo, nil)
1296
+ else
1297
+ self:_systemError(oldSystemInfo, err)
1298
+ self:_systemCleanup(oldSystemInfo, err)
1299
+ end
1300
+ end
1301
+
1302
+ local systems = self._phaseToSystems[oldSystemInfo.phase]
1303
+
1304
+ local index = table.find(systems, oldSystemFn)
1305
+ assert(index, "Unable to find system within phase")
1306
+
1307
+ table.remove(systems, index)
1308
+ table.insert(systems, index, newSystemFn)
1309
+
1310
+ local copy = table.clone(oldSystemInfo)
1311
+
1312
+ oldSystemInfo.system = newSystemFn
1313
+ oldSystemInfo.run = newSystemFn
1314
+ oldSystemInfo.cleanup = nil
1315
+ oldSystemInfo.initialized = false
1316
+ oldSystemInfo.name = getSystemName(new)
1317
+
1318
+ self:_systemReplace(
1319
+ copy :: SystemInfo<U...>,
1320
+ oldSystemInfo :: SystemInfo<U...>
1321
+ )
1322
+
1323
+ self._systemInfo[newSystemFn] = self._systemInfo[oldSystemFn]
1324
+ self._systemInfo[oldSystemFn] = nil
1325
+
1326
+ return self
1327
+ end
1328
+
1329
+ --- @method addRunCondition
1330
+ --- @within Scheduler
1331
+ --- @param system System
1332
+ --- @param fn (U...) -> any
1333
+ ---
1334
+ --- Adds a Run Condition which the Scheduler will check before
1335
+ --- this System is ran.
1336
+
1337
+ --- @method addRunCondition
1338
+ --- @within Scheduler
1339
+ --- @param phase Phase
1340
+ --- @param fn (U...) -> any
1341
+ ---
1342
+ --- Adds a Run Condition which the Scheduler will check before
1343
+ --- any Systems within this Phase are ran.
1344
+
1345
+ --- @method addRunCondition
1346
+ --- @within Scheduler
1347
+ --- @param pipeline Pipeline
1348
+ --- @param fn (U...) -> any
1349
+ ---
1350
+ --- Adds a Run Condition which the Scheduler will check before
1351
+ --- any Systems within any Phases apart of this Pipeline are ran.
1352
+ function Scheduler.addRunCondition<U...>(
1353
+ self: Scheduler<U...>,
1354
+ dependent: Dependent<U...>,
1355
+ fn: Condition<U...>
1356
+ )
1357
+ fn = if typeof(fn) == "table" then fn[1] else fn
1358
+
1359
+ local system: SystemFn<...any>? = getSystem(dependent :: any) :: any
1360
+ if system then
1361
+ dependent = system
1362
+ end
1363
+
1364
+ assert(
1365
+ system or isPhase(dependent) or isPipeline(dependent),
1366
+ "Attempt to pass unknown dependent into Scheduler:addRunCondition(unknown, _)"
1367
+ )
1368
+
1369
+ if not self._runIfConditions[dependent :: any] then
1370
+ self._runIfConditions[dependent :: any] = {}
1371
+ end
1372
+
1373
+ table.insert(self._runIfConditions[dependent :: any], fn)
1374
+
1375
+ return self
1376
+ end
1377
+
1378
+ function Scheduler._scheduleEvent<U...>(self: Scheduler<U...>, instance, event)
1379
+ local connect = utils.getConnectFn(instance, event)
1380
+ assert(
1381
+ connect,
1382
+ "Couldn't connect to event as no valid connect methods were found! Ensure the passed event has a 'Connect' or an 'on' method!"
1383
+ )
1384
+
1385
+ local identifier = getEventIdentifier(instance, event)
1386
+
1387
+ local dependencyGraph = DependencyGraph.new()
1388
+
1389
+ local callback = function()
1390
+ local orderedList = dependencyGraph:getOrderedList()
1391
+
1392
+ if orderedList == nil then
1393
+ local err =
1394
+ `Event Group '{identifier}' contains a circular dependency, check your Pipelines/Phases`
1395
+ if not recentLogs[err] then
1396
+ -- LUAU FUTURE: Inference bug
1397
+ task.spawn<<(string, number?)>>(error, err, 0)
1398
+ warn(
1399
+ `Planck: Error occurred while running event, this error will be ignored for 10 seconds`
1400
+ )
1401
+ recentLogs[err] = true
1402
+ end
1403
+
1404
+ return
1405
+ end
1406
+
1407
+ for _, dependency in orderedList do
1408
+ self:run(dependency :: any)
1409
+ end
1410
+ end
1411
+
1412
+ self._connectedEvents[identifier] = connect(callback)
1413
+ self._eventDependencyGraphs[identifier] =
1414
+ dependencyGraph :: DependencyGraph<Dependency>
1415
+ end
1416
+
1417
+ function Scheduler._getEventDependencyGraph<U...>(
1418
+ self: Scheduler<U...>,
1419
+ instance,
1420
+ event
1421
+ )
1422
+ local identifier = getEventIdentifier(instance, event)
1423
+
1424
+ if not self._connectedEvents[identifier] then
1425
+ self:_scheduleEvent(instance, event)
1426
+ end
1427
+
1428
+ return self._eventDependencyGraphs[identifier]
1429
+ end
1430
+
1431
+ function Scheduler._getGraphOfDependency<U...>(
1432
+ self: Scheduler<U...>,
1433
+ dependency: Dependency
1434
+ )
1435
+ if table.find(self._defaultDependencyGraph.nodes, dependency) then
1436
+ return self._defaultDependencyGraph
1437
+ end
1438
+
1439
+ for _, dependencyGraph in self._eventDependencyGraphs do
1440
+ if table.find(dependencyGraph.nodes, dependency) then
1441
+ return dependencyGraph
1442
+ end
1443
+ end
1444
+
1445
+ error("Dependency does not belong to a DependencyGraph")
1446
+ end
1447
+
1448
+ --- @within Scheduler
1449
+ ---
1450
+ --- Disconnects all events, closes all threads, and performs
1451
+ --- other cleanup work.
1452
+ ---
1453
+ --- :::danger
1454
+ --- Only use this if you intend to not use the associated
1455
+ --- Scheduler anymore. It will not work as intended.
1456
+ ---
1457
+ --- You should dereference the scheduler object so that
1458
+ --- it may be garbage collected.
1459
+ --- :::
1460
+ ---
1461
+ --- :::warning
1462
+ --- If you're creating a "throwaway" scheduler, you should
1463
+ --- not add plugins like Jabby or the Matter Debugger to it.
1464
+ --- These plugins are unable to properly be cleaned up, use
1465
+ --- them with caution.
1466
+ --- :::
1467
+ function Scheduler.cleanup<U...>(self: Scheduler<U...>)
1468
+ for _, connection in self._connectedEvents do
1469
+ utils.disconnectEvent(connection)
1470
+ end
1471
+
1472
+ for _, plugin in self._plugins do
1473
+ -- LUAU FUTURE: Type solver doesn't play nice here
1474
+ local cleanup: ((self: Plugin) -> ())? = plugin.cleanup
1475
+ if cleanup then
1476
+ cleanup(plugin)
1477
+ end
1478
+ end
1479
+
1480
+ if self._thread then
1481
+ coroutine.close(self._thread)
1482
+ end
1483
+
1484
+ for _, _conditions in self._runIfConditions do
1485
+ for _, condition in _conditions do
1486
+ conditions.cleanupCondition(condition)
1487
+ end
1488
+ end
1489
+ end
1490
+
1491
+ --- @function new
1492
+ --- @within Scheduler
1493
+ --- @param args U...
1494
+ ---
1495
+ --- Creates a new Scheduler, the args passed will be passed to
1496
+ --- any System anytime it is ran by the Scheduler.
1497
+ function Scheduler.new<U...>(...: U...): Scheduler<U...>
1498
+ local defaultPhase = Phase.new("Default")
1499
+ local defaultDependencyGraph: DependencyGraph<Dependency> =
1500
+ DependencyGraph.new() :: any
1501
+
1502
+ defaultDependencyGraph:insert(Pipeline.Startup)
1503
+ defaultDependencyGraph:insert(defaultPhase)
1504
+
1505
+ local self: Scheduler<U...> = setmetatable({
1506
+ _defaultPhase = defaultPhase,
1507
+ _defaultDependencyGraph = defaultDependencyGraph,
1508
+ _eventDependencyGraphs = {},
1509
+ _connectedEvents = {},
1510
+ _plugins = {},
1511
+ _phaseToSystems = {},
1512
+ _hooks = {},
1513
+ _runIfConditions = {},
1514
+ _systemInfo = {},
1515
+ _thread = nil :: any,
1516
+ _currentSystem = nil :: any,
1517
+ _yielded = false,
1518
+ _vargs = { ... },
1519
+ }, Scheduler)
1520
+
1521
+ for _, hookName in self.Hooks :: any do
1522
+ if not self._hooks[hookName] then
1523
+ self._hooks[hookName] = {}
1524
+ end
1525
+ end
1526
+
1527
+ self:addRunCondition(Pipeline.Startup, conditions.runOnce())
1528
+ for _, phase in Pipeline.Startup.dependencyGraph.nodes do
1529
+ self:addRunCondition(phase, conditions.runOnce())
1530
+ end
1531
+
1532
+ return self :: Scheduler<U...>
1533
+ end
1534
+
1535
+ export type Scheduler<U...> = setmetatable<
1536
+ {
1537
+ _defaultPhase: Phase,
1538
+ _defaultDependencyGraph: DependencyGraph<Dependency>,
1539
+ _eventDependencyGraphs: { [string]: DependencyGraph<Dependency> },
1540
+ _connectedEvents: { [string]: utils.ConnectionLike },
1541
+ _plugins: Vec<Plugin>,
1542
+ -- LUAU FIXME: Should be System<U...>
1543
+ _phaseToSystems: Map<Phase, Vec<InternalSystem<...any>>>,
1544
+ _hooks: Map<HookId, Vec<(PartialHookContext) -> unknown>>,
1545
+ -- LUAU FIXME: Should be <Dependent<U...>, Condition<U...>>
1546
+ _runIfConditions: Map<Dependent<...any>, Vec<Condition<...any>>>,
1547
+ -- LUAU FIXME: Should be SystemInfo<U...>
1548
+ _systemInfo: Map<InternalSystem<U...>, SystemInfo<...any>>,
1549
+ _currentSystem: SystemInfo<...any>,
1550
+ _thread: thread?,
1551
+ _yielded: boolean,
1552
+ _vargs: { any },
1553
+ },
1554
+ typeof(Scheduler)
1555
+ >
1556
+
1557
+ return Scheduler