Initial commit
This commit is contained in:
commit
f33b1782f1
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Dinoleaf LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
87
task.gd
Normal file
87
task.gd
Normal file
@ -0,0 +1,87 @@
|
||||
## Task
|
||||
##
|
||||
## [color=yellow][b]Note:[/b] You probably won't be generating these yourself.
|
||||
## Primarily intended to be generated and used by [TaskManager][/color].[br]
|
||||
## A stateful task container, containing a list of functions.
|
||||
|
||||
class_name Task extends RefCounted
|
||||
|
||||
## Called when this task completes, emitting itself as [param task].
|
||||
signal on_finished(task:Task)
|
||||
|
||||
## Nice name.
|
||||
var name:String = ""
|
||||
## List of queued commands.
|
||||
var commands:Array[Callable] = []
|
||||
## List of exit conditions. Any set to true will end this process.
|
||||
var exit_conditions:Array[Callable] = [
|
||||
func()->bool: return halt
|
||||
]
|
||||
## Internal tracker for showing what's running
|
||||
var current_execution:int = 0
|
||||
## Are we done yet?
|
||||
var finished := false
|
||||
## Are we paused?
|
||||
var paused := false
|
||||
## Are we flagged to stop when possible?
|
||||
var halt := false
|
||||
|
||||
## Assign pause state to [param to_pause].
|
||||
func set_pause(to_pause:bool)->void: paused = to_pause
|
||||
|
||||
## Assign pause state to [param to_pause] upon all [param tasks].
|
||||
static func set_pause_many(tasks:Array[Task], to_pause:bool)->void:
|
||||
tasks.map(func(t:Task): t.paused = to_pause)
|
||||
|
||||
## Initialize.[br]
|
||||
## [param _name] Nice name.[br]
|
||||
## [param _commands] List of commands.[br]
|
||||
## [param exit_conds] List of functions that take 0 args and return boolean.
|
||||
## if any of these are true when ticked, the task will terminate.
|
||||
func _init(_name:String, _commands:Array[Callable], exit_conds:Array[Callable])->void:
|
||||
name = _name
|
||||
commands = _commands
|
||||
for p_exit:Callable in exit_conds:
|
||||
if p_exit.is_valid():
|
||||
exit_conditions.append(p_exit)
|
||||
|
||||
|
||||
## Execute all [member Task.commands]. Triggers [signal Task.on_finished] when complete.
|
||||
func execute()->void:
|
||||
finished = false
|
||||
var _check_halt_conds := func(p:Callable)->bool: return p.call()
|
||||
for index:int in commands.size():
|
||||
if exit_conditions.any(_check_halt_conds):
|
||||
break
|
||||
while paused:
|
||||
await Engine.get_main_loop().root.get_tree().process_frame
|
||||
if exit_conditions.any(_check_halt_conds):
|
||||
break
|
||||
current_execution = index
|
||||
await commands[index].call()
|
||||
finished = true
|
||||
on_finished.emit(self)
|
||||
|
||||
## Dump a string of [param task] and all functions in the queue.
|
||||
static func dump_to_text(task:Task)->String:
|
||||
var _name_out := "[%s]\n" % "Unnamed tasklist" \
|
||||
if task.name.is_empty() else task.name
|
||||
var _commands_out:String = ""
|
||||
for index:int in task.commands.size():
|
||||
var cmd := task.commands[index]
|
||||
var _label_id := "[%s]%s" % [
|
||||
str(index).pad_zeros(2),
|
||||
">" if index == task.current_execution else ""]
|
||||
var _broken := "" if cmd.is_valid() else "<Invalid Callable?>"
|
||||
var _cmd_out := cmd.get_method() + "("
|
||||
for arg in cmd.get_bound_arguments():
|
||||
_cmd_out += str(arg) + ","
|
||||
_cmd_out = _cmd_out.trim_suffix(",") + ")\n"
|
||||
_commands_out += _label_id + _broken + _cmd_out
|
||||
return _name_out + _commands_out
|
||||
|
||||
## Dump strings of all given [param tasks].
|
||||
static func dump_commands(tasks:Array[Task])->Array[String]:
|
||||
var task_text:Array[String] = []
|
||||
task_text.assign(tasks.map(dump_to_text))
|
||||
return task_text
|
125
task_manager.gd
Normal file
125
task_manager.gd
Normal file
@ -0,0 +1,125 @@
|
||||
## 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()
|
Loading…
Reference in New Issue
Block a user