123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145 |
- # Credit to jpate for inspiration to this solution: https://godotengine.org/qa/8656/how-properly-stop-yield-from-resuming-after-the-class-freed?show=86173#a86173
- class_name AsyncHelper
- extends Node
- signal connection_finished
- # Data structure to keep track of connections
- # "completed": bool # If the connection's signal has been emitted yet
- # "results": Variant # The value returned by the signal
- # "id": int # Unique ID to keep track of the connection
- const TRACKER_EMPTY: Dictionary = \
- {"completed": false, "result": null, "id": -1}
- var max_id: int = -1
- # Maps connections to trackers, allowing multiple trackers to track the same
- # connection. Useful for things like waiting on SceneTree "idle_frame" which
- # could be happening in multiple places at the same time
- var connection_tracker_map: Dictionary = {}
- # Maps connection IDs to if they have been marked as cancelled or not
- var cancelled: Dictionary = {}
- func _init(parent: Object) -> void:
- parent.connect("tree_exiting", self, "_on_parent_tree_exiting")
- func connect_wrapped(source: Object, sig: String, returns_value: bool=true
- ) -> Dictionary:
- """
- Wraps the given signal connection, allowing you to `yield` on this object's
- `wait_until_finished` function instead of `source` which prevents errors in
- the case that `source` out lives the caller due to it returning to a
- non-existant object. For this to work this object must be added as a child
- of the caller, or have some other system to ensure it does not out live the
- caller
-
- Args:
- source: object which will emit the signal `sig`
- sig: signal to listen for
- returns_value: whether or not the signal returns a value
-
- Returns:
- A tracker, see `TRACKER_EMPTY` for its structure
- """
- var tracker: Dictionary = TRACKER_EMPTY.duplicate()
- var id: int = max_id + 1
- max_id += 1
- tracker["id"] = id
- cancelled[id] = false
-
- var callback: String = "_on_completion"
- if not returns_value:
- callback = "_on_completion_no_return"
-
- var connection: Array = [source, sig, callback]
- if not connection_tracker_map.has(connection):
- connection_tracker_map[connection] = []
- connection_tracker_map[connection].append(tracker)
-
- if not source.is_connected(sig, self, callback):
- source.connect(sig, self, callback, [connection], CONNECT_ONESHOT)
- return tracker
- func wait_until_finished(trackers: Array) -> Array:
- """
- This function completes when all connections identified in `trackers`
- either complete or are cancelled
-
- Args:
- trackers: array of trackers. See `TRACKER_EMPTY` for the structure of a
- tracker
-
- Returns:
- An array of booleans indicating if the connection (the tracker) at the
- respective index was cancelled or not
- """
- while _are_connections_finished(trackers) == false:
- yield(self, "connection_finished")
-
- var connections_cancelled: Array = []
- for tracker in trackers:
- connections_cancelled.append(cancelled.get(tracker["id"], false))
- cancelled.erase(tracker["id"])
- return connections_cancelled
- func cancel(id: int) -> void:
- """
- Mark a connection as cancelled, meaning `wait_until_finished()` won't
- wait for it to complete
- """
- cancelled[id] = true
- emit_signal("connection_finished")
- func cancel_all() -> void:
- """
- Mark all connections as cancelled, meaning `wait_until_finished()` won't
- wait for them to complete
- """
- for id in cancelled.keys():
- cancelled[id] = true
- emit_signal("connection_finished")
- func _are_connections_finished(trackers: Array) -> bool:
- """
- Returns:
- `true` if all connections are completed or cancelled, `false` otherwise
- """
- var connections_finished = true
- for tracker in trackers:
- if (
- tracker["completed"] == false
- and not cancelled.get(tracker["id"], false)
- ):
- connections_finished = false
- break
- return connections_finished
- func _on_completion(result, connection: Array) -> void:
- """ Called when a wrapped connection completes """
- for tracker in connection_tracker_map[connection]:
- tracker["result"] = result
- tracker["completed"] = true
- cancelled.erase(tracker["id"])
- emit_signal("connection_finished")
- connection_tracker_map.erase(connection)
- func _on_completion_no_return(connection: Array) -> void:
- """ Called when a wrapped connection completes """
- for tracker in connection_tracker_map[connection]:
- tracker["completed"] = true
- cancelled.erase(tracker["id"])
- emit_signal("connection_finished")
- connection_tracker_map.erase(connection)
- func _on_parent_tree_exiting() -> void:
- """ Ensure this object doesn't out live its parent """
- queue_free()
|