126 lines
4.6 KiB
GDScript3
126 lines
4.6 KiB
GDScript3
|
## Task Manager[br]
|
||
|
##
|
||
|
## A simple [Task] scheduler. This can take a list of functions and process
|
||
|
## them, either in the background or asynchronously.
|
||
|
## Multiple instances of these can be set at different scopes. [br]
|
||
|
##
|
||
|
## Note that due to the nature of processing tasks, if we are to destroy
|
||
|
## referenced objects within task closures, we should await [method TaskManager await_shutdown]
|
||
|
## before unloading said references (nodes, internal state objects, etc)
|
||
|
## otherwise a task may continue to process and use-after-free.
|
||
|
class_name TaskManager extends RefCounted
|
||
|
|
||
|
## Called when a task starts
|
||
|
signal task_started(task:Task)
|
||
|
## Called when a task finishes.
|
||
|
signal task_complete(task:Task)
|
||
|
|
||
|
## Nice name for distinguishing task processors.
|
||
|
var name:String = "Untitled Task Scheduler"
|
||
|
## Array of tasks that are either processing or paused.
|
||
|
## Halted tasks may get GC'd out at any point.
|
||
|
var tasks:Array[Task] = []
|
||
|
|
||
|
## Internal flag. If true, all tasks are to stop when possible.
|
||
|
## Use [method TaskManager.halt_all] if you want this behavior.
|
||
|
var _halt := false
|
||
|
|
||
|
## Initialize. Give a nice name.
|
||
|
func _init(_name:String)->void:
|
||
|
name = _name
|
||
|
|
||
|
## Execute a list of commands. Returns the wrapped up [Task]. Try not to mutate
|
||
|
## the task or you will be killed by a clown under your bed in seven days.[br]
|
||
|
## [param task_name] Identifier for task.[br]
|
||
|
## [param commands] List of prepared functions.[br]
|
||
|
## [param exit_cond] Exit predicate, which can interrupt a process early.[br]
|
||
|
## [param block] If true, will await until the task is complete.[br]
|
||
|
## Otherwise, it'll just start running the task without waiting to return. [br]
|
||
|
## Returns the generated task instance should the origin want to observe it. [br]
|
||
|
## [codeblock]
|
||
|
## var task_manager := TaskManager.new("EXAMPLE")
|
||
|
## # Better if you have coroutines that take time to call. ie awaiting a timeout.
|
||
|
## var commands:Array[Callable] = [print("1")]
|
||
|
## var active_task := task_manager.execute_commands("prints", commands, false)
|
||
|
## print(task_manager.dump_text)
|
||
|
## await active_task.on_finished
|
||
|
## print("Done")
|
||
|
## [/codeblock]
|
||
|
func execute_commands(
|
||
|
task_name:String,
|
||
|
commands:Array[Callable],
|
||
|
exit_cond:Callable = Callable(),
|
||
|
block:bool = true
|
||
|
)->Task:
|
||
|
var new_task := Task.new(task_name, commands, [p_halt_check.bind(exit_cond)])
|
||
|
tasks.append(new_task)
|
||
|
new_task.on_finished.connect(remove_task, CONNECT_ONE_SHOT)
|
||
|
new_task.execute()
|
||
|
if block:
|
||
|
await new_task.finished
|
||
|
return new_task
|
||
|
|
||
|
## Find a given task in this task reference.
|
||
|
## [param task] Target task.
|
||
|
func remove_task(task:Task)->void:
|
||
|
var task_index := tasks.find(task)
|
||
|
if task_index == -1:
|
||
|
return
|
||
|
tasks.remove_at(task_index)
|
||
|
|
||
|
## Flag all tasks to be stopped as soon as possible.
|
||
|
func halt_all()->void:
|
||
|
_halt = true
|
||
|
|
||
|
## Pause or unpause all tasks with [param to_pause].
|
||
|
func set_pause_on_all(to_pause:bool)->void:
|
||
|
Task.set_pause_many(tasks, to_pause)
|
||
|
|
||
|
## If we want this task manager to tick itself, throw it onto a separate channel.
|
||
|
## This lets us be lazy and probably regret it later.[br]
|
||
|
## [param tick_rate] is how many seconds to wait until ticking again.
|
||
|
## We do GC-ing and such for finished tasks, which can be expensive every frame.
|
||
|
func self_process(tick_rate:float = 0.5)->void:
|
||
|
while not _halt:
|
||
|
tick_status_check()
|
||
|
await Utils.COMMANDS.wait_seconds(tick_rate)
|
||
|
|
||
|
|
||
|
## Effectively garbage-collect finished tasks.
|
||
|
func tick_status_check()->void:
|
||
|
#var _is_ongoing := func _is_ongoing(task:Task)->bool: return not task.finished
|
||
|
var _is_finished := func _is_finished(task:Task)->bool: return task.finished
|
||
|
for task:Task in tasks.map(_is_finished):
|
||
|
remove_task(task)
|
||
|
|
||
|
|
||
|
## Sends a halt signal to all processes and awaits until all tasks are safely stopped. [br]
|
||
|
## Await this call to safely ensure you won't have stray processes ticking
|
||
|
## and possibly calling use-after-free references.
|
||
|
func await_shutdown()->void:
|
||
|
halt_all()
|
||
|
# delay until tasks finish and remove themselves.
|
||
|
while not tasks.is_empty():
|
||
|
await Engine.get_main_loop().root.get_tree().process_frame
|
||
|
|
||
|
|
||
|
## Get a string of this scheduler and all the tasks currently in process.
|
||
|
func dump_text()->String:
|
||
|
var scheduler_text := "[%s]"
|
||
|
|
||
|
var reduce := func(accum:String, current:String)->String:
|
||
|
return accum + current + "\n"
|
||
|
|
||
|
var active_tasks := Task.dump_commands(tasks)
|
||
|
var task_megastring:String = active_tasks.reduce(reduce, "")
|
||
|
|
||
|
return scheduler_text + "\n" + task_megastring
|
||
|
|
||
|
|
||
|
## Recursive predicate for halt checking.[br]
|
||
|
## [param p_halt] if true, the task should stop.[br]
|
||
|
## If no [param p_halt] is set, we only will halt if the scheduler is told to halt.
|
||
|
func p_halt_check(p_halt:=Callable())->bool:
|
||
|
if not p_halt.is_valid(): return _halt
|
||
|
return _halt or p_halt.call()
|