stick-the-quick/util/CallableNode.gd

128 lines
3.9 KiB
GDScript

class_name CallableNode extends Node
## A node that can be called like a function.
## Positional arguments the node was invoked with.
var args: Array = []
## Named arguments ("options") the node was invoked with.
var opts: Dictionary = {}
## Node's return value.
var result: Variant = null
## Calls the CallableNode, waits for it to finish, and returns its result.
##
## When a CallableNode is called, the following sequence of events occurs:
## 1. we wait for the parent node to be in the tree
## and self to be out of the tree at the same time
## (if this never happens, then the function will never terminate,
## and thus any await subscribers will never be notified
## and will themselves never terminate);
## 2. the CallableNode is passed the given args and opts;
## 3. the CallableNode is requested to call _ready,
## and then added as a child to the parent;
## 4. if the CallableNode's process_mode is PROCESS_MODE_INHERIT
## or PROCESS_MODE_DISABLED, the most descended process_mode
## in its ancestry not to equal either of those values
## is applied directly to the CallableNode instead
## (falling back to PROCESS_MODE_PAUSABLE if none is found);
## 5. the parent's process mode is set to PROCESS_MODE_DISABLED;
## 6. the function yields until the CallableNode is exiting the tree;
## 7. steps 2-5 are undone;
## 8. The CallableNode's result property is returned.
func invoke_existing(
parent: Node,
p_args: Array = [],
p_opts: Dictionary = {}
) -> Variant:
var prev_args := args
var prev_opts := opts
var prev_process_mode := process_mode
var parent_prev_process_mode := parent.process_mode
# step 1
var ok := func() -> bool:
return parent.is_inside_tree() and not is_inside_tree()
await Wait.until(ok)
# step 2
args = p_args
opts = p_opts
# step 3
request_ready()
parent.add_child(self)
# step 4
var process_mode_resolve := self
while process_mode_resolve and (
process_mode_resolve.process_mode == Node.PROCESS_MODE_INHERIT or
process_mode_resolve.process_mode == Node.PROCESS_MODE_DISABLED
):
process_mode_resolve = process_mode_resolve.get_parent()
if process_mode_resolve:
process_mode = process_mode_resolve.process_mode
else:
process_mode = Node.PROCESS_MODE_PAUSABLE
# step 5
parent.process_mode = Node.PROCESS_MODE_DISABLED
# step 6
if is_inside_tree():
await tree_exiting
# step 7
args = prev_args
opts = prev_opts
process_mode = prev_process_mode
parent.process_mode = parent_prev_process_mode
# step 8
return result
## Invokes an ad-hoc CallableNode.
##
## Can accept CallableNode, PackedScene, GDScript, or resource path String.
## (If and only if CallableNode is given directly,
## its lifetime is not managed by the call to invoke;
## in this case, the call is equivalent
## to calling invoke_existing on the given node.)
static func invoke(
what: Variant,
parent: Node,
p_args: Array = [],
p_opts: Dictionary = {}
) -> Variant:
if what is CallableNode:
return await what.invoke_existing(parent, p_args, p_opts)
elif what is PackedScene:
var inst := what.instantiate() as Node
var cn := inst as CallableNode
if cn:
var p_result = await cn.invoke_existing(parent, p_args, p_opts)
cn.queue_free()
return p_result
elif inst:
push_error(
"Tried to invoke a PackedScene " +
"whose root is not a CallableNode: " +
inst.scene_file_path
)
inst.free()
return null
else:
return null
elif what is GDScript:
var inst = what.new()
var cn := inst as CallableNode
if cn:
var p_result = await cn.invoke_existing(parent, p_args, p_opts)
cn.queue_free()
return p_result
elif inst:
push_error(
"Tried to invoke a GDScript not derived from CallableNode: " +
what.resource_path
)
if inst is Object and not (inst is RefCounted):
inst.free()
return null
else:
return null
elif what is String:
return await CallableNode.invoke(load(what), parent, p_args, p_opts)
else:
push_error("No way to infer a CallableNode from value: %s" % what)
return null