async_helper.gd 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. # 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
  2. class_name AsyncHelper
  3. extends Node
  4. signal connection_finished
  5. # Data structure to keep track of connections
  6. # "completed": bool # If the connection's signal has been emitted yet
  7. # "results": Variant # The value returned by the signal
  8. # "id": int # Unique ID to keep track of the connection
  9. const TRACKER_EMPTY: Dictionary = \
  10. {"completed": false, "result": null, "id": -1}
  11. var max_id: int = -1
  12. # Maps connections to trackers, allowing multiple trackers to track the same
  13. # connection. Useful for things like waiting on SceneTree "idle_frame" which
  14. # could be happening in multiple places at the same time
  15. var connection_tracker_map: Dictionary = {}
  16. # Maps connection IDs to if they have been marked as cancelled or not
  17. var cancelled: Dictionary = {}
  18. func _init(parent: Object) -> void:
  19. parent.connect("tree_exiting", self, "_on_parent_tree_exiting")
  20. func connect_wrapped(source: Object, sig: String, returns_value: bool=true
  21. ) -> Dictionary:
  22. """
  23. Wraps the given signal connection, allowing you to `yield` on this object's
  24. `wait_until_finished` function instead of `source` which prevents errors in
  25. the case that `source` out lives the caller due to it returning to a
  26. non-existant object. For this to work this object must be added as a child
  27. of the caller, or have some other system to ensure it does not out live the
  28. caller
  29. Args:
  30. source: object which will emit the signal `sig`
  31. sig: signal to listen for
  32. returns_value: whether or not the signal returns a value
  33. Returns:
  34. A tracker, see `TRACKER_EMPTY` for its structure
  35. """
  36. var tracker: Dictionary = TRACKER_EMPTY.duplicate()
  37. var id: int = max_id + 1
  38. max_id += 1
  39. tracker["id"] = id
  40. cancelled[id] = false
  41. var callback: String = "_on_completion"
  42. if not returns_value:
  43. callback = "_on_completion_no_return"
  44. var connection: Array = [source, sig, callback]
  45. if not connection_tracker_map.has(connection):
  46. connection_tracker_map[connection] = []
  47. connection_tracker_map[connection].append(tracker)
  48. if not source.is_connected(sig, self, callback):
  49. source.connect(sig, self, callback, [connection], CONNECT_ONESHOT)
  50. return tracker
  51. func wait_until_finished(trackers: Array) -> Array:
  52. """
  53. This function completes when all connections identified in `trackers`
  54. either complete or are cancelled
  55. Args:
  56. trackers: array of trackers. See `TRACKER_EMPTY` for the structure of a
  57. tracker
  58. Returns:
  59. An array of booleans indicating if the connection (the tracker) at the
  60. respective index was cancelled or not
  61. """
  62. while _are_connections_finished(trackers) == false:
  63. yield(self, "connection_finished")
  64. var connections_cancelled: Array = []
  65. for tracker in trackers:
  66. connections_cancelled.append(cancelled.get(tracker["id"], false))
  67. cancelled.erase(tracker["id"])
  68. return connections_cancelled
  69. func cancel(id: int) -> void:
  70. """
  71. Mark a connection as cancelled, meaning `wait_until_finished()` won't
  72. wait for it to complete
  73. """
  74. cancelled[id] = true
  75. emit_signal("connection_finished")
  76. func cancel_all() -> void:
  77. """
  78. Mark all connections as cancelled, meaning `wait_until_finished()` won't
  79. wait for them to complete
  80. """
  81. for id in cancelled.keys():
  82. cancelled[id] = true
  83. emit_signal("connection_finished")
  84. func _are_connections_finished(trackers: Array) -> bool:
  85. """
  86. Returns:
  87. `true` if all connections are completed or cancelled, `false` otherwise
  88. """
  89. var connections_finished = true
  90. for tracker in trackers:
  91. if (
  92. tracker["completed"] == false
  93. and not cancelled.get(tracker["id"], false)
  94. ):
  95. connections_finished = false
  96. break
  97. return connections_finished
  98. func _on_completion(result, connection: Array) -> void:
  99. """ Called when a wrapped connection completes """
  100. for tracker in connection_tracker_map[connection]:
  101. tracker["result"] = result
  102. tracker["completed"] = true
  103. cancelled.erase(tracker["id"])
  104. emit_signal("connection_finished")
  105. connection_tracker_map.erase(connection)
  106. func _on_completion_no_return(connection: Array) -> void:
  107. """ Called when a wrapped connection completes """
  108. for tracker in connection_tracker_map[connection]:
  109. tracker["completed"] = true
  110. cancelled.erase(tracker["id"])
  111. emit_signal("connection_finished")
  112. connection_tracker_map.erase(connection)
  113. func _on_parent_tree_exiting() -> void:
  114. """ Ensure this object doesn't out live its parent """
  115. queue_free()