Initial commit

This commit is contained in:
Pyral 2024-12-21 13:41:11 -05:00
commit f33b1782f1
4 changed files with 233 additions and 0 deletions

21
LICENSE Normal file
View 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.

0
README.md Normal file
View File

87
task.gd Normal file
View 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
View 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()