From cd24d33ddcf48bc42756ff70f5ef8388533334b7 Mon Sep 17 00:00:00 2001 From: Laura Andelare Date: Fri, 26 Jul 2024 13:11:41 +0000 Subject: [PATCH] UE4 edition --- .gitattributes | 2 + .gitignore | 22 + COPYING | 30 + Docs/AI.md | 7 + Docs/Async.md | 285 ++++++++++ Docs/Awaiters.md | 399 +++++++++++++ Docs/Cancellation.md | 98 ++++ Docs/GAS.md | 131 +++++ Docs/Generator.md | 118 ++++ Docs/latent_node.png | Bin 0 -> 14008 bytes Docs/pull_request_template.md | 1 + Plugins/UE5Coro/Resources/Icon128.png | Bin 0 -> 16299 bytes .../UE5Coro/Private/AggregateAwaiters.cpp | 165 ++++++ .../UE5Coro/Private/AnimationAwaiters.cpp | 200 +++++++ .../Source/UE5Coro/Private/AsyncAwaiter.cpp | 109 ++++ .../Source/UE5Coro/Private/AsyncAwaiters.cpp | 155 +++++ .../Source/UE5Coro/Private/AsyncPromise.cpp | 39 ++ .../Source/UE5Coro/Private/AwaitableEvent.cpp | 128 +++++ .../UE5Coro/Private/AwaitableSemaphore.cpp | 94 ++++ .../Source/UE5Coro/Private/Cancellation.cpp | 88 +++ .../Source/UE5Coro/Private/Coroutine.cpp | 110 ++++ .../Source/UE5Coro/Private/Generator.cpp | 43 ++ .../Source/UE5Coro/Private/HttpAwaiters.cpp | 104 ++++ .../Source/UE5Coro/Private/LatentAwaiter.cpp | 115 ++++ .../Private/LatentAwaiters_AsyncLoad.cpp | 247 ++++++++ .../Private/LatentAwaiters_AsyncQuery.cpp | 234 ++++++++ .../UE5Coro/Private/LatentAwaiters_Wait.cpp | 200 +++++++ .../UE5Coro/Private/LatentCallbacks.cpp | 68 +++ .../Source/UE5Coro/Private/LatentChain.cpp | 60 ++ .../Source/UE5Coro/Private/LatentExitReason.h | 43 ++ .../Source/UE5Coro/Private/LatentPromise.cpp | 352 ++++++++++++ .../Source/UE5Coro/Private/LatentTimeline.cpp | 128 +++++ .../Source/UE5Coro/Private/Promise.cpp | 174 ++++++ .../Source/UE5Coro/Private/TimerThread.cpp | 113 ++++ .../Source/UE5Coro/Private/TimerThread.h | 63 +++ .../Source/UE5Coro/Private/UE5Coro.cpp | 39 ++ .../Private/UE5CoroAnimCallbackTarget.cpp | 201 +++++++ .../Private/UE5CoroAnimCallbackTarget.h | 86 +++ .../Private/UE5CoroChainCallbackTarget.cpp | 102 ++++ .../Private/UE5CoroChainCallbackTarget.h | 68 +++ .../Private/UE5CoroDelegateCallbackTarget.cpp | 50 ++ .../Private/UE5CoroDelegateCallbackTarget.h | 52 ++ .../UE5Coro/Private/UE5CoroSubsystem.cpp | 130 +++++ .../UE5Coro/Source/UE5Coro/Public/UE5Coro.h | 47 ++ .../Source/UE5Coro/Public/UE5Coro.natvis | 21 + .../Public/UE5Coro/AggregateAwaiters.h | 232 ++++++++ .../Public/UE5Coro/AnimationAwaiters.h | 191 +++++++ .../UE5Coro/Public/UE5Coro/AsyncAwaiters.h | 430 ++++++++++++++ .../UE5Coro/Public/UE5Coro/AsyncCoroutine.h | 489 ++++++++++++++++ .../UE5Coro/Public/UE5Coro/Cancellation.h | 111 ++++ .../Source/UE5Coro/Public/UE5Coro/Coroutine.h | 258 +++++++++ .../UE5Coro/Public/UE5Coro/Coroutine.inl | 182 ++++++ .../Public/UE5Coro/CoroutineAwaiters.h | 117 ++++ .../Public/UE5Coro/CoroutinePrivate.inl | 79 +++ .../UE5Coro/Public/UE5Coro/Definitions.h | 56 ++ .../Source/UE5Coro/Public/UE5Coro/Generator.h | 203 +++++++ .../UE5Coro/Public/UE5Coro/HttpAwaiters.h | 79 +++ .../UE5Coro/Public/UE5Coro/LatentAwaiters.h | 531 ++++++++++++++++++ .../UE5Coro/Public/UE5Coro/LatentCallbacks.h | 88 +++ .../UE5Coro/Public/UE5Coro/LatentChain.inl | 199 +++++++ .../UE5Coro/Public/UE5Coro/LatentTimeline.h | 67 +++ .../Source/UE5Coro/Public/UE5Coro/Private.h | 76 +++ .../Source/UE5Coro/Public/UE5Coro/Threading.h | 125 +++++ .../UE5Coro/Public/UE5Coro/UE5CoroSubsystem.h | 87 +++ .../UE5Coro/Source/UE5Coro/UE5Coro.Build.cs | 63 +++ .../Private/K2Node_UE5CoroCallCoroutine.cpp | 103 ++++ .../Private/K2Node_UE5CoroCallCoroutine.h | 50 ++ .../Source/UE5CoroK2/Private/UE5CoroK2.cpp | 38 ++ .../Source/UE5CoroK2/UE5CoroK2.Build.cs | 45 ++ .../Private/AggregateAwaiterTest.cpp | 300 ++++++++++ .../Private/AnimationAwaiterTest.cpp | 54 ++ .../UE5CoroTests/Private/AsyncAwaiterTest.cpp | 229 ++++++++ .../UE5CoroTests/Private/AsyncLoadTest.cpp | 183 ++++++ .../UE5CoroTests/Private/AsyncQueryTest.cpp | 146 +++++ .../Private/AwaitableEventTest.cpp | 168 ++++++ .../Private/AwaitableSemaphoreTest.cpp | 124 ++++ .../UE5CoroTests/Private/CancellationTest.cpp | 311 ++++++++++ .../Private/CoroutineHandleTest.cpp | 313 +++++++++++ .../Private/DelegateAwaiterTest.cpp | 339 +++++++++++ .../UE5CoroTests/Private/ExceptionTest.cpp | 151 +++++ .../UE5CoroTests/Private/FutureTest.cpp | 151 +++++ .../UE5CoroTests/Private/GeneratorTest.cpp | 93 +++ .../UE5CoroTests/Private/HttpAwaiterTest.cpp | 127 +++++ .../Private/LatentAwaiterTest.cpp | 189 +++++++ .../Private/LatentCallbackTest.cpp | 129 +++++ .../Private/LatentChainCancellationTest.cpp | 123 ++++ .../UE5CoroTests/Private/LatentChainTest.cpp | 183 ++++++ .../UE5CoroTests/Private/ReturnValueTest.cpp | 167 ++++++ .../UE5CoroTests/Private/TestDelegates.h | 55 ++ .../Source/UE5CoroTests/Private/TestWorld.cpp | 88 +++ .../UE5CoroTests/Private/UE5CoroTestObject.h | 76 +++ .../UE5CoroTests/Private/UE5CoroTests.cpp | 38 ++ .../Source/UE5CoroTests/Public/TestWorld.h | 92 +++ .../Source/UE5CoroTests/UE5CoroTests.Build.cs | 45 ++ Plugins/UE5Coro/UE5Coro.uplugin | 35 ++ Plugins/UE5CoroAI/Resources/Icon128.png | Bin 0 -> 17774 bytes .../Source/UE5CoroAI/Private/AIAwaiters.cpp | 333 +++++++++++ .../Source/UE5CoroAI/Private/UE5CoroAI.cpp | 39 ++ .../Private/UE5CoroAICallbackTarget.cpp | 75 +++ .../Private/UE5CoroAICallbackTarget.h | 59 ++ .../Source/UE5CoroAI/Public/UE5CoroAI.h | 35 ++ .../UE5CoroAI/Public/UE5CoroAI/AIAwaiters.h | 135 +++++ .../Source/UE5CoroAI/UE5CoroAI.Build.cs | 47 ++ .../Source/UE5CoroAITests/Private/AITest.cpp | 46 ++ .../UE5CoroAITests/Private/UE5CoroAITests.cpp | 38 ++ .../UE5CoroAITests/UE5CoroAITests.Build.cs | 47 ++ Plugins/UE5CoroAI/UE5CoroAI.uplugin | 36 ++ Plugins/UE5CoroGAS/Resources/Icon128.png | Bin 0 -> 15687 bytes .../UE5CoroGAS/Private/AbilityPromises.cpp | 103 ++++ .../UE5CoroGAS/Private/UE5CoroAbilityTask.cpp | 104 ++++ .../Source/UE5CoroGAS/Private/UE5CoroGAS.cpp | 39 ++ .../Private/UE5CoroGameplayAbility.cpp | 208 +++++++ .../Private/UE5CoroTaskCallbackTarget.cpp | 37 ++ .../Private/UE5CoroTaskCallbackTarget.h | 48 ++ .../Source/UE5CoroGAS/Public/UE5CoroGAS.h | 36 ++ .../Public/UE5CoroGAS/AbilityPromises.h | 125 +++++ .../Public/UE5CoroGAS/UE5CoroAbilityTask.h | 94 ++++ .../UE5CoroGAS/UE5CoroGameplayAbility.h | 102 ++++ .../Source/UE5CoroGAS/UE5CoroGAS.Build.cs | 46 ++ .../Private/AbilityTaskTests.cpp | 96 ++++ .../UE5CoroGASTests/Private/GASTestWorld.cpp | 64 +++ .../UE5CoroGASTests/Private/GASTestWorld.h | 55 ++ .../Private/GameplayAbilityTests.cpp | 137 +++++ .../Private/UE5CoroGASTestAbilityTask.cpp | 70 +++ .../Private/UE5CoroGASTestAbilityTask.h | 52 ++ .../Private/UE5CoroGASTestAvatar.cpp | 49 ++ .../Private/UE5CoroGASTestAvatar.h | 50 ++ .../Private/UE5CoroGASTestGameplayAbility.cpp | 96 ++++ .../Private/UE5CoroGASTestGameplayAbility.h | 60 ++ .../Private/UE5CoroGASTests.cpp | 38 ++ .../UE5CoroGASTests/UE5CoroGASTests.Build.cs | 48 ++ Plugins/UE5CoroGAS/UE5CoroGAS.uplugin | 40 ++ README.md | 212 +++++++ 133 files changed, 15628 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 COPYING create mode 100644 Docs/AI.md create mode 100644 Docs/Async.md create mode 100644 Docs/Awaiters.md create mode 100644 Docs/Cancellation.md create mode 100644 Docs/GAS.md create mode 100644 Docs/Generator.md create mode 100644 Docs/latent_node.png create mode 100644 Docs/pull_request_template.md create mode 100644 Plugins/UE5Coro/Resources/Icon128.png create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/AggregateAwaiters.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/AnimationAwaiters.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/AsyncAwaiter.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/AsyncAwaiters.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/AsyncPromise.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/AwaitableEvent.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/AwaitableSemaphore.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/Cancellation.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/Coroutine.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/Generator.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/HttpAwaiters.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/LatentAwaiter.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/LatentAwaiters_AsyncLoad.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/LatentAwaiters_AsyncQuery.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/LatentAwaiters_Wait.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/LatentCallbacks.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/LatentChain.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/LatentExitReason.h create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/LatentPromise.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/LatentTimeline.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/Promise.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/TimerThread.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/TimerThread.h create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/UE5Coro.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroAnimCallbackTarget.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroAnimCallbackTarget.h create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroChainCallbackTarget.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroChainCallbackTarget.h create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroDelegateCallbackTarget.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroDelegateCallbackTarget.h create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroSubsystem.cpp create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro.h create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro.natvis create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/AggregateAwaiters.h create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/AnimationAwaiters.h create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/AsyncAwaiters.h create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/AsyncCoroutine.h create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Cancellation.h create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Coroutine.h create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Coroutine.inl create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/CoroutineAwaiters.h create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/CoroutinePrivate.inl create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Definitions.h create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Generator.h create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/HttpAwaiters.h create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/LatentAwaiters.h create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/LatentCallbacks.h create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/LatentChain.inl create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/LatentTimeline.h create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Private.h create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Threading.h create mode 100644 Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/UE5CoroSubsystem.h create mode 100644 Plugins/UE5Coro/Source/UE5Coro/UE5Coro.Build.cs create mode 100644 Plugins/UE5Coro/Source/UE5CoroK2/Private/K2Node_UE5CoroCallCoroutine.cpp create mode 100644 Plugins/UE5Coro/Source/UE5CoroK2/Private/K2Node_UE5CoroCallCoroutine.h create mode 100644 Plugins/UE5Coro/Source/UE5CoroK2/Private/UE5CoroK2.cpp create mode 100644 Plugins/UE5Coro/Source/UE5CoroK2/UE5CoroK2.Build.cs create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/Private/AggregateAwaiterTest.cpp create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/Private/AnimationAwaiterTest.cpp create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/Private/AsyncAwaiterTest.cpp create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/Private/AsyncLoadTest.cpp create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/Private/AsyncQueryTest.cpp create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/Private/AwaitableEventTest.cpp create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/Private/AwaitableSemaphoreTest.cpp create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/Private/CancellationTest.cpp create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/Private/CoroutineHandleTest.cpp create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/Private/DelegateAwaiterTest.cpp create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/Private/ExceptionTest.cpp create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/Private/FutureTest.cpp create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/Private/GeneratorTest.cpp create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/Private/HttpAwaiterTest.cpp create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/Private/LatentAwaiterTest.cpp create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/Private/LatentCallbackTest.cpp create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/Private/LatentChainCancellationTest.cpp create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/Private/LatentChainTest.cpp create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/Private/ReturnValueTest.cpp create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/Private/TestDelegates.h create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/Private/TestWorld.cpp create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/Private/UE5CoroTestObject.h create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/Private/UE5CoroTests.cpp create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/Public/TestWorld.h create mode 100644 Plugins/UE5Coro/Source/UE5CoroTests/UE5CoroTests.Build.cs create mode 100644 Plugins/UE5Coro/UE5Coro.uplugin create mode 100644 Plugins/UE5CoroAI/Resources/Icon128.png create mode 100644 Plugins/UE5CoroAI/Source/UE5CoroAI/Private/AIAwaiters.cpp create mode 100644 Plugins/UE5CoroAI/Source/UE5CoroAI/Private/UE5CoroAI.cpp create mode 100644 Plugins/UE5CoroAI/Source/UE5CoroAI/Private/UE5CoroAICallbackTarget.cpp create mode 100644 Plugins/UE5CoroAI/Source/UE5CoroAI/Private/UE5CoroAICallbackTarget.h create mode 100644 Plugins/UE5CoroAI/Source/UE5CoroAI/Public/UE5CoroAI.h create mode 100644 Plugins/UE5CoroAI/Source/UE5CoroAI/Public/UE5CoroAI/AIAwaiters.h create mode 100644 Plugins/UE5CoroAI/Source/UE5CoroAI/UE5CoroAI.Build.cs create mode 100644 Plugins/UE5CoroAI/Source/UE5CoroAITests/Private/AITest.cpp create mode 100644 Plugins/UE5CoroAI/Source/UE5CoroAITests/Private/UE5CoroAITests.cpp create mode 100644 Plugins/UE5CoroAI/Source/UE5CoroAITests/UE5CoroAITests.Build.cs create mode 100644 Plugins/UE5CoroAI/UE5CoroAI.uplugin create mode 100644 Plugins/UE5CoroGAS/Resources/Icon128.png create mode 100644 Plugins/UE5CoroGAS/Source/UE5CoroGAS/Private/AbilityPromises.cpp create mode 100644 Plugins/UE5CoroGAS/Source/UE5CoroGAS/Private/UE5CoroAbilityTask.cpp create mode 100644 Plugins/UE5CoroGAS/Source/UE5CoroGAS/Private/UE5CoroGAS.cpp create mode 100644 Plugins/UE5CoroGAS/Source/UE5CoroGAS/Private/UE5CoroGameplayAbility.cpp create mode 100644 Plugins/UE5CoroGAS/Source/UE5CoroGAS/Private/UE5CoroTaskCallbackTarget.cpp create mode 100644 Plugins/UE5CoroGAS/Source/UE5CoroGAS/Private/UE5CoroTaskCallbackTarget.h create mode 100644 Plugins/UE5CoroGAS/Source/UE5CoroGAS/Public/UE5CoroGAS.h create mode 100644 Plugins/UE5CoroGAS/Source/UE5CoroGAS/Public/UE5CoroGAS/AbilityPromises.h create mode 100644 Plugins/UE5CoroGAS/Source/UE5CoroGAS/Public/UE5CoroGAS/UE5CoroAbilityTask.h create mode 100644 Plugins/UE5CoroGAS/Source/UE5CoroGAS/Public/UE5CoroGAS/UE5CoroGameplayAbility.h create mode 100644 Plugins/UE5CoroGAS/Source/UE5CoroGAS/UE5CoroGAS.Build.cs create mode 100644 Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/AbilityTaskTests.cpp create mode 100644 Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/GASTestWorld.cpp create mode 100644 Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/GASTestWorld.h create mode 100644 Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/GameplayAbilityTests.cpp create mode 100644 Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTestAbilityTask.cpp create mode 100644 Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTestAbilityTask.h create mode 100644 Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTestAvatar.cpp create mode 100644 Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTestAvatar.h create mode 100644 Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTestGameplayAbility.cpp create mode 100644 Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTestGameplayAbility.h create mode 100644 Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTests.cpp create mode 100644 Plugins/UE5CoroGAS/Source/UE5CoroGASTests/UE5CoroGASTests.Build.cs create mode 100644 Plugins/UE5CoroGAS/UE5CoroGAS.uplugin create mode 100644 README.md diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..97fb964c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.uasset binary +*.umap binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..48168620 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Unreal + +Binaries +Build +DerivedDataCache +Intermediate +Saved +*_BuiltData.uasset +.vscode +.vs +*.VC.db +*.opensdf +*.opendb +*.sdf +*.sln +*.suo +*.xcodeproj +*.xcworkspace + +# Tools +bin +obj diff --git a/COPYING b/COPYING new file mode 100644 index 00000000..d29e0509 --- /dev/null +++ b/COPYING @@ -0,0 +1,30 @@ +Copyright © Laura Andelare +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted (subject to the limitations in the disclaimer +below) provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Docs/AI.md b/Docs/AI.md new file mode 100644 index 00000000..d1c24797 --- /dev/null +++ b/Docs/AI.md @@ -0,0 +1,7 @@ +# UE5CoroAI + +This optional extra plugin integrates with the engine's "classic" AI +functionality, such as AIModule, NavigationSystem, ... + +It provides awaiters for various tasks performed with these systems, such as +"AI Move To". diff --git a/Docs/Async.md b/Docs/Async.md new file mode 100644 index 00000000..c45305cc --- /dev/null +++ b/Docs/Async.md @@ -0,0 +1,285 @@ +# Async coroutines + +Returning `UE5Coro::TCoroutine` from a function makes it coroutine-enabled +and lets you co_await various awaiters provided by this library, +found in various namespaces within UE5Coro such as UE5Coro\:\:Async or +UE5Coro\:\:Latent. +Any async coroutine can use any awaiter, but some awaiters are limited to the +game thread. + +`TCoroutine` co_returns T, `TCoroutine<>` co_returns void. +`TCoroutine<>` and `TCoroutine` are perfectly equivalent. +All typed TCoroutines implicitly convert to TCoroutine<>, giving you a common +return-type-erased view to a coroutine that may or may not have a return type. + +Cancellation support is documented [on a separate page](Cancellation.md). + +TCoroutine is thread safe and O(1) copyable (it's a shared pointer inside). +Copies of a TCoroutine refer to the same coroutine as the original. +TCoroutine<T> has all the functionality of TCoroutine<>, plus additional +methods and overloads for the return type. + +TCoroutines are comparable (they have a strict, total, but meaningless order), +and hashable with GetTypeHash and std::hash. +Copies referring to the same coroutine invocation compare equal to each other. + +A non-void coroutine return type must be at least _DefaultConstructible_, +_MoveAssignable_, and _Destructible_. +Full functionality also requires _CopyConstructible_. +It's possible that a coroutine completes without providing a return value. +In this case, reading the return value provides T(). + +`FAsyncCoroutine` in the global namespace is a `USTRUCT` wrapper for +TCoroutine<>, to be used when reflection support is required, e.g., for latent +UFUNCTIONs. +It implicitly converts from/to TCoroutine<>. +Return values from co_return are not supported due to engine limitations, but +"out" reference parameters still work for BP. + +FAsyncCoroutine (but not TCoroutine<>) can be default constructed due to yet +more engine limitations. +Prefer using TCoroutine<> when reflection/BP support is not needed. +Interacting with a default-constructed FAsyncCoroutine or a TCoroutine<> that +was converted from one is undefined behavior. +Obtaining the coroutine's underlying `std::coroutine_handle` directly is not +supported and is extremely likely to break. + +## Debugging + +TCoroutine<>::SetDebugName() applies a debug name to the currently-running +coroutine's promise object, which is otherwise an implementation detail. +This has no effect at runtime (and does nothing in Shipping), but it's useful +for debug viewing these objects. + +You might want to macro `TCoroutine<>::SetDebugName(TEXT(__FUNCTION__))`. + +Looking at these or promise objects in general as part of `__coro_frame_ptr` +seems to be unreliable in practice, moving one level up in the call stack to +Resume() tends to work better when tested with Visual Studio 2022 17.4 and +JetBrains Rider 2022.3. +A .natvis file is provided to automatically display this debug info. + +In debug builds (controlled by `UE5CORO_DEBUG`) a synchronous resume stack is +also kept to aid in debugging complex cases of coroutine resumption, mostly +having to do with WhenAny or WhenAll. + +## Execution modes + +There are two major execution modes of async coroutines: they can either run +autonomously or implement a latent UFUNCTION, tied to the latent action manager. + +### Async mode + +If your function **does not** have a `FLatentActionInfo` or +`FForceLatentCoroutine` parameter, the coroutine is running in "async mode". +You still have access to awaiters in the UE5Coro::Latent namespace (locked to +the game thread) but as far as your callers are concerned, the function returns +at the first co_await and drives itself after that point. + +This mode is mainly a replacement for "fire and forget" AsyncTasks and timers. + +Async mode coroutines _mostly_ run independently, even after major events like +PIE ending. +It's the coroutine's responsibility to detect this and act accordingly, e.g., by +co_returning early. +An exception to this is co_awaiting a latent awaiter, in which case ownership +behind the scenes is temporarily passed to the current world's latent action +manager that **can** destroy the running coroutine. +This manifests as a co_await not resuming, but instead all local variables' +destructors in scope are run as if an exception was thrown. + +### Latent mode + +If your function (probably a UFUNCTION in this case but this is **not** checked +or required) takes `FLatentActionInfo` or `FForceLatentCoroutine`, the coroutine +is running in "latent mode". +The world will be fetched from the first UObject* parameter that returns a valid +pointer from GetWorld(). +If there's a FLatentActionInfo parameter, its callback target will be used with +the highest priority. +The latent info will be registered with that world's latent action manager, +there's no need to call FLatentActionManager::AddNewAction(). + +> [!IMPORTANT] +> A future update will simplify this logic to make it more performant, reliable, +> and match BP behavior even more closely. +> To prepare, make sure your world context object is the first parameter (`this` +> for nonstatic members). + +The detected world context (most often `this` for non-static member UFUNCTIONs) +will act as the latent action's owner, and the coroutine will enjoy a measure of +lifetime tracking and protection from the latent action manager, mostly +eliminating the need to check the validity of `this` after each `co_await`. +There are still situations where the coroutine can resume on an invalid object, +e.g., if the owning object was destroyed and the destructors of the coroutine's +local variables are being run as a response. + +The output exec pin will fire in BP when the coroutine co_returns (most often +this happens naturally as control leaves the scope of the function), but you can +stop this by canceling the coroutine using [any method](Cancellation.md). +The destructors of local variables, etc. will run as usual regardless. + +If the UFUNCTION is called again with the same callback target/UUID while a +coroutine is already running, a second copy will **not** start, matching the +behavior of most of the engine's built-in latent actions. + +You may use awaiters such as UE5Coro\:\:Async\:\:MoveToThread to switch threads. +Finishing the coroutine is allowed on any thread, but note that in C++, the +destructors of locals run on the current thread **before** the coroutine is +considered complete, which might not be desired. + +BP will always continue on the game thread after the coroutine state (locals, +etc.) is cleaned up. + +If the latent action manager decides to delete the latent task and it's +currently running on another thread, it is canceled and may continue until the +next co_await, after which its locals will be destroyed **on the game thread**. + +## Awaiters + +[Click here](Awaiters.md) for an overview of the various awaiters that come +with the plugin. + +Most awaiters from this plugin can only be used once and will `check()` if +reused. +There are a few (notably in the UE5Coro::Async namespace) that may be reused, +but these are so cheap to create – around the cost of an int – that you should +be recreating them for consistency. + +It's recommended to treat every awaiter as "moved-from" or invalid after they've +been co_awaited. This includes being co_awaited through wrappers such as WhenAll. + +The awaiter types that are in the `UE5Coro::Private` namespace are not +documented and subject to change in any future version with no prior deprecation. +Most of the time, you don't even need to know about them, e.g., +`co_await Something();`. +This usage is ideal and recommended for most scenarios. + +If you want to store them in a variable (see next section), use `auto` for +source compatibility. + +If you want to pass them around, these internal types are mostly copyable and +are limited to one active co_await across all copies. +**It's undefined behavior to move an awaiter that's currently being co_awaited.** +Multiple sequential co_awaits are usually allowed, with the second and beyond +succeeding immediately. + +Some of these are locked to the game thread. +Generally speaking, the same rules and limitations apply as the underlying +engine systems that drive the current awaiter and its awaiting coroutine, e.g., +awaiters dealing with UObjects or from the Latent namespace are usually locked +to the game thread. + +### Overlapping awaiters + +It is possible to run multiple awaiters overlapped, which makes sense for +(but isn't limited to) some of them that perform useful actions, not just wait: + +```cpp +FAsyncCoroutine AMyActor::GuaranteedSlowLoad(FLatentActionInfo) +{ + auto Wait1 = UE5Coro::Latent::Seconds(1); // The clock starts now! + auto Wait2 = UE5Coro::Latent::Seconds(0.5); // This starts at the same time! + auto Load1 = UE5Coro::Latent::AsyncLoadObject(MySoftPtr1); + auto Load2 = UE5Coro::Latent::AsyncLoadObject(MySoftPtr2); + co_await UE5Coro::WhenAll(Load1, Load2); // Wait for both to be loaded + co_await Wait1; // Waste the remainder of that 1 second + co_await Wait2; // This is already over, it won't wait half a second +} +``` + +### Other coroutines + +`TCoroutine`s themselves are awaitable, co_awaiting them will resume the +caller when the callee coroutine finishes for any reason, **including** +`UE5Coro::Latent::Cancel()`. + +The return type of co_awaiting TCoroutine<T> is T. +If the coroutine completed without co_returning a value, the result will be T(). + +Async coroutines resume on the thread where the awaited coroutine finished. +Latent coroutines resume on the next tick after the callee ended. +co_awaiting a coroutine that's already complete will not release the current +thread and will continue running with the result obtained synchronously. + +## Coroutines and UObject lifetimes + +While coroutines provide a synchronous-looking interface, they do not run +synchronously (that's kind of the point🙂) and this can lead to problems that +might be harder to spot due to the friendly linear-looking syntax. +Most coroutines will not need to worry about these issues, but for advanced +scenarios it's something you'll need to keep in mind. + +Your function immediately returns when you co_await, which means that the +garbage collector might run before you resume. +Your function parameters and local variables technically live in a "raw C++" +struct with no UPROPERTY declarations (generated by the compiler) and therefore +are eligible for garbage collection. + +The usual solutions for multithreading and UObject access/GC keepalive such as +AddToRoot, FGCObject, TStrongObjectPtr, etc. still apply. +If something would work for std::vector it will probably work for coroutines, too. + +Examples of dangerous code: + +```cpp +using namespace UE5Coro; + +FAsyncCoroutine AMyActor::Latent(UObject* Obj, FLatentActionInfo) +{ + // You're synchronously running before the first co_await, Obj is as your + // caller passed it in. TWeakObjectPtr is safe to keep outside a UPROPERTY. + TWeakObjectPtr ObjPtr(Obj); + + co_await Latent::Seconds(1); // Obj might get garbage collected during this! + + if (IsValid(Obj)) // Dangerous, Obj could be a dangling pointer by now! + Foo(Obj); + if (auto* Obj2 = ObjPtr.Get()) // This is safe, might be nullptr + Foo(Obj2); + + // This is also safe, but only because of the FLatentActionInfo parameter! + // Destroying an actor cancels all of its latent actions at the engine level, + // so you would never reach this point. + if (SomeUPropertyOnAMyActor) + Foo(this); + + // Latent protection extends to other awaiters and thread hopping: + co_await Async::MoveToThread(ENamedThreads::AnyBackgroundThreadNormalTask); + Foo(this); // Not safe, the GC might run on the game thread + co_await Async::MoveToGameThread(); + Foo(this); // But this is OK! The co_await above resumed so `this` is valid. +} +``` + +Especially dangerous if you're running on another thread, `this` protection +and co_await _not_ resuming the coroutine does not apply if you're not latent: + +```cpp +using namespace UE5Coro; + +TCoroutine<> UMyExampleClass::DontDoThisAtHome(UObject* Dangerous) +{ + checkf(IsInGameThread(), TEXT("This example needs to start on the GT")); + + // You can be sure this remains valid until you co_await + UObject* Obj = NewObject(); + if (IsValid(Dangerous)) + Dangerous->Safe(); + + // Latent protection applies when co_awaiting Latent awaiters even if you're + // not latent, this might not resume if ActorObj gets destroyed: + co_await Latent::Chain(&ASomeActor::SomethingLatent, ActorObj, 1.0f); + + // But not here: + co_await Async::MoveToThread(ENamedThreads::AnyBackgroundThreadNormalTask); + // You're no longer synchronously running on the game thread, + // Obj *IS* eligible for garbage collection and Dangerous might be dangling! + co_await Async::MoveToGameThread(); + + // You're back on the game thread, but all of these could be destroyed by now: + Dangerous->OhNo(); + Obj->Ouch(); + SomeMemberVariable++; // Even `this` could be GC'd by now! +} +``` diff --git a/Docs/Awaiters.md b/Docs/Awaiters.md new file mode 100644 index 00000000..9387f5d0 --- /dev/null +++ b/Docs/Awaiters.md @@ -0,0 +1,399 @@ +# Awaiters + +This page gives an overview of the various awaiters that come with the plugin. +This is not meant to be an exhaustive documentation, read the comments in the +header files for that. + +If you're not using the recommended `#include "UE5Coro.h"`, some of these +features require an extra #include (usually `"UE5Coro/AsyncAwaiters.h"`) that's +not immediately apparent. + +Awaiters by necessity expose various snake_case functions as public, such as +`await_ready`, `await_suspend`, or `await_resume`. +Calling these manually instead of through `co_await` is undefined behavior. + +Calling other methods that are inherited from `Private` base classes and aren't +explicitly documented to be callable is also undefined behavior. + +## Coroutines + +`TCoroutine` is co_awaitable, see [this page](Async.md#other-coroutines). + +## Aggregates + +UE5Coro::WhenAny and WhenAll let you combine any type of co_awaitable objects +into one that resumes the coroutine when one or all of them have completed. + +UE5Coro::Race behaves like WhenAny, but it can only take TCoroutines (including +implicitly-converted FAsyncCoroutines), and the first coroutine to complete will +cancel the others. + +When multiple types of awaiters are mixed, it's unspecified whose system will +resume - for example: +```cpp +auto Async = UE5Coro::Async::MoveToThread(...); +auto Latent = UE5Coro::Latent::Seconds(...); +co_await UE5Coro::WhenAll(Async, MoveTemp(Latent)); +``` +The code above might resume in an AsyncTask, or game thread Tick. +WhenAny, Race, and WhenAll are all thread safe. + +Some awaiters (mostly Latent ones) require being moved into the call like in the +example above. +C\+\+20 will let you know that the call's constraints were not satisfied on the +calling line. +C\+\+17 will hit a static_assert inside the function, prompting you to fix it. +The calling line will often be found in the error's notes somewhere. + +Every parameter is consumed and counts as co_awaited by these calls, even if +WhenAny or Race finish early. + +The return values of these functions are copyable and allow one concurrent +co_await across all copies. +Once the initial co_await has finished, further ones continue synchronously. + +## Threading primitives + +UE5Coro::FAwaitableEvent and UE5Coro::FAwaitableSemaphore provide awaitable +versions of these well-known threading primitives. +They're directly co_awaitable, which uses up an auto-reset event, or locks a +semaphore once. +A separate mutex is not provided, FAwaitableSemaphore defaults to being a mutex. + +Awaiters are resumed in an unspecified order, e.g., fairness is not guaranteed. +Events resume coroutines on the thread they're Trigger()ed, semaphores might +resume on the last thread that Unlock()ed them or an earlier thread if multiple +unlocks happen in quick succession. + +## Async awaiters + +The UE5Coro\:\:Async namespace contains awaiters that let you conveniently move +execution between various named threads, notably between the game thread and +everything else. + +Async\:\:PlatformSeconds, and Async\:\:UntilPlatformTime resume +the coroutine on the same kind of thread that it was on (game thread to game +thread, render thread to render thread, background thread to background thread, +etc.). +Async\:\:PlatformSecondsAnyThread and Async\:\:UntilPlatformTime resume the +coroutine on an unspecified thread, and are marginally more efficient. + +The return values of these functions are copyable, thread-safe, and allow any +number of concurrent co_awaits. + +### Delegates + +Delegates that are made by the following macro families are co_awaitable: +* DECLARE_DELEGATE (TDelegate) +* DECLARE_DYNAMIC_DELEGATE (TScriptDelegate) +* DECLARE_DYNAMIC_MULTICAST_DELEGATE (TMulticastScriptDelegate) +* DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE (TSparseDynamicDelegate) +* DECLARE_EVENT (TMulticastDelegate) +* DECLARE_MULTICAST_DELEGATE (TMulticastDelegate) +* ~~DECLARE_TS_DELEGATE~~[^nomacro] +(TDelegate<..., FDefaultTSDelegateUserPolicy>) +* DECLARE_TS_MULTICAST_DELEGATE +(TMulticastScriptDelegate<..., FDefaultTSDelegateUserPolicy>) + +[^nomacro]: There is no `DECLARE_TS_DELEGATE`, but the delegates that it would + define are supported anyway. + +`RetVal` and any number of `Params` are supported. +`RetVal` delegates will receive a default-constructed or zeroed value once the +coroutine co_returns or co_awaits something else. +Return types that aren't _DefaultConstructible_ or `void` are not supported. + +Using the macros is not required: `TDelegate` works directly, etc. +Since this is considered an async awaiter, there are no restrictions on what +delegate can be co_awaited beyond the engine's own limitations. +It's supported to, e.g., co_await a BlueprintAssignable delegate from a +non-UObject on any thread, but you're responsible for avoiding race conditions. +A co_await will implicitly Add to or Bind the delegate behind the scenes. + +The coroutine will resume on the same thread that the delegate is Executed or +Broadcasted from. + +#### Alternatives + +This feature is very convenient, but also very dangerous: +if the delegate never executes, the coroutine will be stuck waiting for it +forever, essentially leaking memory. +Cancellations are also not processed until the delegate executes. + +`Latent::UntilDelegate` may be used as an alternative in latent mode. +It is locked to the game thread, does not process parameters or return values, +but responds to its awaiting coroutine being canceled or aborted even if the +delegate never executes. +It is technically available in async mode due to the usual feature parity +between the two modes, but it's not as beneficial in that case. + +[UE5CoroGAS](GAS.md) has a specialized awaiter for delegates in BP tasks. + +Many engine functions copy the delegate that's passed in, preventing a direct +co_await: +```c++ +TDelegate Delegate; +SomeEngineFunction(Delegate); // Delegate's unbound state gets copied +co_await Delegate; // The delegate is only bound here +``` + +Other features may be used as a workaround so that the delegate is bound before +it is copied, such as: +```c++ +FAwaitableEvent Event; +TDelegate Delegate; +Delegate.BindWeakLambda(this, [&]{Event.Trigger();}); +SomeEngineFunction(Delegate); +co_await Event; +``` + +#### Parameters and return values + +If the delegate has no parameters, the type of the co_await expression is void. +If it has parameters, the co_await expression will result in an object of an +unspecified internal type that can be used with structured bindings. +References will match the delegate caller's references, and can be written to. +They will remain valid until the next co_await. + +```c++ +// Assume this is declared somewhere else +DECLARE_DYNAMIC_DELEGATE_RetVal_TwoParams(double, FExample, int, int&); +FExample Delegate; + +// ... + +co_await Delegate; // Delegate.Execute() returns 0.0 to its caller +auto X = co_await Delegate; // Not supported, this type is internal to UE5Coro +auto [A, B] = co_await Delegate; // Caller gets 0.0, A is int, B is int +auto&& [C, D] = co_await Delegate; // Caller gets 0.0, C is int, D is int& +// auto& [E, F] = co_await Delegate; // Does not compile, E can't bind to int + +// D is valid until the next co_await +D = 1; // The caller of Delegate.Execute() will see this write + +if (bSomething) + co_await Latent::NextTick(); +// D might or might not be valid here depending on bSomething + +co_await Latent::NextTick(); +// D is definitely stale, using it here is undefined behavior +``` + +### TFuture + +TFuture\ is directly co_awaitable. +The co_await expression returns the result of the future and consumes the +TFuture object similarly to its Then and Next members. +As a result, if your future is not a temporary it will require being moved: +```c++ +TPromise Promise; +co_await Promise.GetFuture(); // OK, temporary future +TFuture Future = Promise.GetFuture(); +// co_await Future; // Won't compile +co_await MoveTemp(Future); // OK +``` + +Unlike TFuture::Next, the co_await expression will correctly match the expected +type, i.e., TFuture\ will result in void instead of int and co_awaiting a +TFuture\ will result in T& instead of T*. + +co_await resumes the coroutine on the same thread that TFuture::Then or Next +would use. + +TSharedFuture\ is not supported due to the underlying implementation of it +lacking completion callbacks. + +TFuture\ itself is movable and can only be used (including co_await) once. + +## Latent awaiters + +UE5Coro::Latent awaiters are locked to the game thread. +Their lifetime is tied to the world and the latent action manager can decide to +cancel them, so it's possible, e.g., if PIE ends or its owning AActor is +destroyed, that co_awaiting them will not resume your coroutine. +In this case the coroutine's state and locals are still destroyed normally +(similarly to if an exception was thrown) and their destructors called, so it's +safe to use `FScopeLock`s, smart pointers, etc. across a co_await, but something +like this could cause problems: + +```cpp +T* Thing = new T(); +co_await UE5Coro::Latent::Something(); // This may not resume +delete Thing; +``` + +It's undefined when exactly the coroutine's state is cleared, it might be, e.g., +when the latent action is destroyed or when the co_await would normally resume +the coroutine. +In practice, for awaiters in this namespace it will usually happen within 2 +ticks. + +Note that while async mode coroutines normally drive and own themselves, if +they're currently co_awaiting a latent awaiter, the world **can** decide to +destroy the coroutine, in which case the same cancellation/cleanup happens. + +The return values of these functions are movable and some of them support +multiple concurrent co_awaits, but relying on the latter is not recommended. + +### Latent callbacks + +To help with the example code from the previous section above, the engine's own +`ON_SCOPE_EXIT` can be used to place code in a destructor, ensuring that it will +always run even if the coroutine is canceled. + +The types in UE5Coro/LatentCallbacks.h provide specialized versions of this that +only execute the provided function/lambda if the coroutine is canceled by the +latent action manager for a certain reason. +Note that a coroutine canceling itself with `UE5Coro::Latent::Cancel()` counts +as neither of these but a normal completion. + +### Chained latent actions + +Most existing latent actions in the engine return void so there's nothing +that you could take or store to co_await them. +There are two wrappers provided in UE5Coro::Latent to make this work, +one of which is available even in C++17: + +```cpp +using namespace std::placeholders; // for ChainEx +using namespace UE5Coro; + +// Automatic parameter matching (C++20 only): skip WorldContextObject and LatentInfo +co_await Latent::Chain(&UKismetSystemLibrary::Delay, 1.0f); + +// For members, provide the object as the first parameter (C++20 only): +co_await Latent::Chain(&UMediaPlayer::OpenSourceLatent, MediaPlayer /*this*/, + MediaSource, Options, bSuccess); + +// Manual parameter matching, _1 is WorldContextObject and _2 is LatentInfo: +co_await Latent::ChainEx(&UKismetSystemLibrary::Delay, _1, 1.0f, _2); +co_await Latent::ChainEx(&UMediaPlayer::OpenSourceLatent, MediaPlayer, _1, _2, + MediaSource, Options, bSuccess); +``` + +As it is impossible to read UFUNCTION(Meta) information at C++ compile time +to figure out which parameter truly is the world context object, Chain uses +**compile-time** heuristics to handle most latent functions found in the engine: +* The first `UObject*` or `UWorld*` that's not `this` is the world context. +* The first `FLatentActionInfo` is the latent info. +* All other parameters of these types are treated as regular parameters and are + expected to be passed in. + +If this doesn't apply to your function or you're using C++17, use +`Latent::ChainEx` and explicitly provide `_1` for the world context (if needed) +and `_2` for the latent info (mandatory) where they belong. +They work exactly like they do in +[std::bind](https://en.cppreference.com/w/cpp/utility/functional/bind). + +The return values of these functions are movable, game thread only, and support +multiple concurrent co_awaits. + + +There are known issues with Latent::Chain on older versions of MSVC (VS2019) +that result in incorrectly-compiled code. +Calling Chain will issue compile-time warnings if this is detected. +VS2022 and Clang seem to be unaffected and are recommended for C++20 overall. +ChainEx may be used as a workaround if you cannot update. + + +#### Debugging/implementation notes + +In popular debuggers (Visual Studio 2022 and JetBrains Rider 2022.1 tested) +`Chain` tends to result in very long call stacks if the chained function is +getting debugged. These are marked `[Inline Frame]` or `[Inlined]` (if your +code is optimized, `Development` or above) and all of them tend to point at the +exact same assembly instruction; this entire segment of the call stack is for +display only and can be safely disregarded. + +It cannot be guaranteed but it's been verified that the `Chain` wrappers do get +optimized and turn into a regular function call or get completely inlined in +`Shipping`. + +#### Rvalue references + +Although this doesn't apply to UFUNCTIONs, passing rvalue references to +Chain() is **not** equivalent to passing them straight to the chained function: +the reference that the function will receive will be to a move-constructed +object, not the original. +This matters in extremely unusual scenarios where the caller wants to still +access the rvalue object after the latent function has returned. +If for some reason you need exactly this, refer to the implementation of +ChainEx to see how to register yourself with UUE5CoroSubsystem and call the +function taking rvalue references directly as an unsupported last-resort option. + +## Animation + +UE5Coro\:\:Anim contains numerous functions to interact with animation montages, +notifies, etc. +All of these functions "snapshot" the currently-playing instance of the montage +when called and ignore every other instance. + +If the calling coroutine is suspended while the animation notify happens, its +notify payload is retrieved and returned as the value of the co_await expression. +Otherwise if, e.g., you call one of these functions but only co_await the return +value later, it will immediately continue if the notify has happened in between +with no payload. + +This limitation is due to `FBranchingPointNotifyPayload` in the engine +containing pointers to UObjects without an accompanying UPROPERTY(). +If you need information from the payload, make sure to read it before the next +co_await and store values appropriately, e.g., in TStrongObjectPtr local +variables. + +FNames or bInterrupted flags from other functions in this namespace are always +valid, none of this is a concern if you don't actually use the payload. + +```cpp +using namespace UE5Coro::Anim; +using namespace UE5Coro::Latent; + +// Example 1: +// Guaranteed-valid payload. No time passes on the game thread between the call +// and its co_await. If a notify happened before calling the function, this will +// wait until the next one. +auto [Name, Payload] = co_await PlayMontageNotifyBegin(MyInstance, MyMontage); +auto Awaiter = PlayMontageNotifyBegin(MyInstance, MyMontage); +LengthySubroutine(); // The game thread is not released, notifies cannot happen. +Tie(Name, Payload) = co_await Awaiter; // Still a guaranteed-valid payload +co_await NextTick(); // The game thread is released, invalidating Payload +// Payload is a dangling pointer now! + +// Example 2: +auto Awaiter = PlayMontageNotifyBegin(MyInstance, MyMontage); +co_await Seconds(1); // Game time passes, a notify may or may not happen here +auto [Name, Payload] = co_await Awaiter; +if (Payload) +{ + // The notify happened after co_await Awaiter. Payload is valid on this line. + co_await NextTick(); // The game thread is released, invalidating Payload + // Game time has passed, Payload is a dangling pointer now! +} +else + ; // The notify happened after PlayMontageNotifyBegin, before co_await Awaiter +``` + +The return values of these functions are copyable, game thread only, support one +concurrent co_await, any number of sequential ones, and it's guaranteed that the +second and further co_awaits will NOT have a valid payload pointer. + +## HTTP + +UE5Coro\:\:Http\:\:ProcessAsync wraps a FHttpRequestRef in an awaiter that +resumes your coroutine when the request is done (including errors). +Like OnProcessRequestComplete(), it forces its caller back to the game thread. + +co_awaiting an already-complete request continues synchronously. + +The return type of this function is copyable, thread-safe, supports one +concurrent co_await across all copies, and any number of sequential ones after +that. + +The type of the co_await expression is `TTuple`. +The bool indicates success, and is retrieved from +FHttpRequestCompleteDelegate's `bConnectedSuccessfully` parameter. +The tuple can be used as is, or more conveniently with structured bindings: +```c++ +using namespace UE5Coro::Http; + +auto [Response, bConnectedSuccessfully] = co_await ProcessAsync(Request); +``` diff --git a/Docs/Cancellation.md b/Docs/Cancellation.md new file mode 100644 index 00000000..ced1f405 --- /dev/null +++ b/Docs/Cancellation.md @@ -0,0 +1,98 @@ +# Coroutine cancellation + +Coroutines returning [UE5Coro::TCoroutine](Async.md) come with integrated +cancellation support. +A canceled coroutine will **NOT** resume at the next co_await, but instead +complete unsuccessfully: destructors of locals will be called (including +`ON_SCOPE_EXIT`), and the return value of the coroutine is `T()`. + +* Async mode coroutines clean up on the thread that would've resumed them. +* Latent mode coroutines always clean up on the game thread. + +Cancellations are usually processed at the point of a coroutine resuming, not +suspending. +A latent mode coroutine awaiting a latent awaiter (in the `UE5Coro::Latent` +namespace) is an exception and will react to the cancellation at the next tick. + +Nothing on this page applies to `UE5Coro::TGenerator`, which is controlled by +its caller, and can be canceled by simply destroying it. + +## Coroutine-initiated + +For latent coroutines **only**, `co_await UE5Coro::Latent::Cancel()` never +resumes the coroutine but instead self-cancels and proceeds to cleanup. +Its output latent exec pin in BP will **NOT** trigger. + +If you're running a coroutine in latent mode that is BlueprintCallable but not +latent (which is supported), this makes no difference in BP. +The exec pin triggers synchronously at the first co_await or co_return. + +Coroutines running in async mode do not need this at all and can simply +co_return. +They're never latent in BP. + +## User-requested + +TCoroutine::Cancel() requests the underlying coroutine to stop running, which +will be served at the next co_await as usual. + +Canceling a coroutine that has completed is safe to do and has no effect. + +## Engine-initiated + +Latent mode coroutines are owned by their UWorld's latent action manager, +that may decide to `delete` them while they're running. +This is translated to a forced cancellation of the coroutine. + +If this happens while an async mode coroutine is co_awaiting a latent action, +that causes a normal cancellation that may be guarded against (see below). + +# Auxiliary features + +There are some additional features to support cancellation: + +## Cancellation guard + +For advanced use, `UE5Coro::FCancellationGuard` acts as a RAII guard against +cancellation. +Before using it, consider if your coroutine is still valid if its `this` is +destroyed. +FCancellationGuard objects are only valid to have as local variables in a +coroutine returning TCoroutine; using them anywhere else is undefined behavior. + +* If one or more of these objects are alive within a coroutine body, +TCoroutine::Cancel() requests are deferred until the first co_await that resumes +after the last one has gone out of scope. +* Latent::Cancel() will `check()` if used with an active FCancellationGuard. +* The latent action manager's `delete` ignores cancellation guards. + +## Manual cancellation check + +If you're running in a tight loop without a natural co_await but want to poll +for incoming cancellation requests, `co_await UE5Coro::FinishNowIfCanceled()` +lets you process them manually. + +* If the coroutine was not canceled, this will continue running **synchronously** +and instantly. +* If the coroutine was canceled, co_await will instead divert to cleanup, +as usual. + +The return value of `FinishNowIfCanceled()` is copyable, reusable, but +meaningless. +co_awaiting it will behave the same regardless of which object is used, you +cannot use it to listen to the cancellation of another coroutine. + +Cancellation will be processed normally: FCancellationGuard is respected, except +for incoming `delete`s, etc. It's usually pointless to co_await this if +there's an active FCancellationGuard. + +Async mode cancellations are processed on the thread that co_awaited. +Latent mode cancellations are always processed on the game thread. + +`UE5Coro::IsCurrentCoroutineCanceled()` is also available that simply returns a +bool but does not process the cancellation. +This function "sees through" FCancellationGuards and will return `true` if an +incoming cancellation request is currently deferred. + +Prefer `co_await FinishNowIfCanceled();` to +`if (IsCurrentCoroutineCanceled()) co_return;`. diff --git a/Docs/GAS.md b/Docs/GAS.md new file mode 100644 index 00000000..8f819468 --- /dev/null +++ b/Docs/GAS.md @@ -0,0 +1,131 @@ +# UE5CoroGAS + +This optional extra plugin integrates with the Gameplay Ability System. +It provides coroutine-based replacements of various GAS-related classes with +tighter integration than what would be possible using the public UE5Coro API. + +This plugin also introduces the `UE5Coro::GAS::FAbilityCoroutine` return type. +Coroutines returning this run in a special [latent mode](Async.md#latent-mode) +that handles ability-related events depending on what class they're in. + +The **ONLY** use of this type is to mark UE5CoroGAS-provided pure virtuals for +C++. +Only use it as the return type when you're overriding these methods, it does +nothing useful anywhere else. + +## Gameplay abilities + +`UUE5CoroGameplayAbility` provides a convenient base class to implement +abilities using a C++ coroutine. +Instead of overriding ActivateAbility, override the new ExecuteAbility instead. +**Overriding ExecuteAbility with a subroutine is undefined behavior.** + +Every instancing policy is supported, including it dynamically changing at +runtime. + +The following events are turned into interactions with the ExecuteAbility +coroutine: + +* The coroutine completing calls EndAbility. +* Self-canceling with Latent::Cancel() acts as calling EndAbility(..., true). +It's recommended to self-cancel this way. +Calling CancelAbility won't be processed until the next co_await. +* Incoming CancelAbility calls cancel the coroutine, which will lead to an +EndAbility(..., true) call once processed. +* Incoming EndAbility calls (including from CancelAbility) cancel the coroutine. +* EndAbility replication is controlled by a property on UUE5CoroGameplayAbility +that may be freely changed at any time and defaults to replicated. +* FCancellationGuard does **NOT** affect CanBeCanceled (but it may be overridden). +Cancellations will be received and (unforced ones) deferred until the last guard +goes out of scope. +* See [below](#garbage-collection-considerations) for notes on unusual garbage +collection behavior driven by GAS itself that might lead to forced cancellations. + +You'll need to call CommitAbility normally from the coroutine as appropriate. +You're free to override any other method not marked `final`. +It is assumed that your overrides will call their Super counterparts for correct +operation. + +UUE5CoroGameplayAbility is marked NotBlueprintable, but you may reverse this in +your subclasses. +You're responsible for interacting correctly with ExecuteAbility from BP. +The BlueprintImplementableEvents for ActivateAbility(FromEvent) will not be +called. + +### Task awaiter + +`UUE5CoroGameplayAbility::Task` takes a UObject* with a single +BlueprintAssignable delegate and wraps it in an awaitable object. +`UGameplayTask`s and `UBlueprintAsyncActionBase`s are automatically activated. + +This wrapper is locked to the game thread, responds to cancellations +immediately, but also discards parameters. +It is the preferred way to await single-delegate tasks from a gameplay ability +coroutine. +Consider using something like `Latent::UntilDelegate` instead of co_awaiting the +delegate directly if you need to deal with multiple UPROPERTYs. +co_awaiting the delegate is fully supported, but it can lead to memory leaks if +it never activates. + +## Ability tasks + +`UUE5CoroAbilityTask` lets you implement an ability task with a coroutine. +Instead of overriding Activate, override Execute with a coroutine to perform the +task and Succeded/Failed to broadcast the delegates your task needs.
+**It is undefined behavior to override Execute with a subroutine.** + +Due to UnrealHeaderTool limitations, it's not possible to provide a ready-to-go +generic ability task. +Your subclass will need to provide the static UFUNCTION to create the task and +UPROPERTY delegates for completion events. + +`UUE5CoroSimpleAbilityTask` provides a generic pair of delegates corresponding +to Succeeded and Failed, and is recommended as the base class for coroutine +tasks that don't need additional delegates. +You only need to provide the static UFUNCTION. + +Unreal expects a gameplay task to call its delegates _after_ EndTask, so make +sure that they are broadcast from Succeeded or Failed, not Execute. +GAS itself will mark the task as garbage in EndTask, so `this` will not be valid +(but also not garbage collected yet) when Succeeded or Failed runs. + +Execute will run in latent mode with the following additional integrations: + +* The coroutine completing will call EndTask and one of Succeeded or Failed, +the latter two being virtuals on `UUE5CoroAbilityTask` with no-op default +implementations. +`UUE5CoroSimpleAbilityTask` broadcasts the delegate corresponding to the method. +Self-cancel with `Latent::Cancel` to trigger Failed instead of Succeeded. +* OnDestroy (e.g., from EndTask) will cancel the coroutine. +* See [below](#garbage-collection-considerations) for notes on unusual garbage +collection behavior driven by GAS itself that might lead to forced cancellations. + +# Garbage collection considerations + +GAS itself sometimes marks abilities and tasks as garbage. +This is usually in response to EndAbility, EndTask, or other forms of +engine-initiated cancellation, such as PIE ending. +Notably, non-instanced gameplay abilities are not subject to this, since they +run on the CDO. + +If GAS marks your object as garbage, that will prompt the latent action manager +to remove its latent actions when it next ticks, including the one that drives +the coroutine behind the scenes. +Since the coroutine can no longer run in this case, it is force canceled, +ignoring FCancellationGuards. +`!IsValid(this)` will be observable, e.g., in destructors called on local +variables inside the coroutine, or other guards such as ON_SCOPE_EXIT. +`Latent::FOnObjectDestroyed` responds to this kind of forced cancellation. + +If the coroutine is not on the game thread when it's force canceled, the +cancellation's processing is delayed until the next `co_await` as usual, but it +is possible that `this` is deleted by the time that happens. + +To better align with the engine's expectations and match how BP abilites/tasks +would behave, FAbilityCoroutine's cancellation processing slightly differs from +a regular latent TCoroutine's. + +In case of a forced cancellation: +* UUE5CoroGameplayAbility does not call EndAbility on itself. +* UUE5CoroAbilityTask does not call EndTask, Succeeded, or Failed. +* UUE5CoroSimpleAbilityTask doesn't Broadcast any of its delegates. diff --git a/Docs/Generator.md b/Docs/Generator.md new file mode 100644 index 00000000..ecf6fd4d --- /dev/null +++ b/Docs/Generator.md @@ -0,0 +1,118 @@ +# Generators + +Returning TGenerator\ allows you to co_yield an arbitrary number of values +through it (even infinite!) with the caller having control over when and how +many to fetch. +This can be more straightforward and efficient than creating a TArray, as the +values are generated and returned one at a time on demand. + +There are also significant compiler optimizations available if, e.g., the +generator is immediately used in a for loop, iterated over, and discarded +(mostly inlining and HALO). +The underlying C++ language feature was designed to scale down to embedded +devices. + +Given this example generator... + +```cpp +using namespace UE5Coro; + +TGenerator CountToThree() +{ + co_yield 1; + co_yield 2; + co_yield 3; +} +``` + +## Manual API + +...you can run it manually using the full API, giving you complete control over +retrieving the value and when the coroutine resumes. +Make sure you check for generator validity if it's not guaranteed that it will +co_yield something, reading the current value when there isn't any will crash! + +```cpp +TGenerator G1 = CountToThree(); // Runs to co_yield 1; then returns +do +{ + if (G1) // Check! + int Value = G1.Current(); // will receive 1, 2, 3 +// Resume() continues the function. For convenience it returns validity. +} while (G1.Resume()); +``` + +Attempting to Resume() a generator that has completely co_returned is valid, a +no-op, and returns false. + +Current() returns a reference to the expression that's evaluated for the current +co_yield. +Unusually for C++ references, this **can** be an lvalue reference to a temporary +value (e.g. `co_yield 1 + 2;` would give you an int& to 3, not an int&&). +Even though the value is temporary from the generator coroutine's perspective, +"time is frozen" until it's resumed so the value is guaranteed to be alive and +valid for the caller of Current. +You can of course decide to MoveTemp/std::move it out of the expression. + +## Iterators + +...you can also use generators with UE-style iterators: + +```cpp +TGenerator G2 = CountToThree(); +for (auto It = G2.CreateIterator(); It; ++It) + DoSomethingWith(*It); +``` + +...or STL-style, including range-based for: + +```cpp +TGenerator G3 = CountToThree(); +for (int Value : G3) + DoSomethingWith(Value); +``` + +Using these common patterns to write a for loop naturally includes the necessary +checks before reading the generator's current value. +Note that the postfix `++` returns void to avoid copying the entire function +state. +It behaves exactly like the prefix `++` for generators. + +## Advanced usage + +Your caller can stop you at any point so feel free to go wild: +```cpp +TGenerator Every64BitPrime() +{ + // You probably don't want to run this on tick... + for (uint64 i = 2; i <= UINT64_MAX; ++i) + if (IsPrime(i)) + co_yield i; +} + +TGenerator Primes = Every64BitPrime(); +uint64 Two = Primes.Current(); +Primes.Resume(); +uint64 Three = Primes.Current(); +Primes.Resume(); +uint64 Five = Primes.Current(); +// Done! Primes going out of scope will destroy the coroutine, i never becomes 6. +``` + +This can be used for `bKeepGoing`-style generators returning `true`, `true`, ... +until some event happens but see also latent actions and awaiters that +encapsulate this kind of logic at a higher level in case you're tick-based on +the game thread. + +## Remarks + +If you're used to Unity coroutines or just regular .NET IEnumerable\ and +yield, note that unlike C# iterators TGenerators start immediately when +called, not at the first Resume. + +This behavior fits the semantics of STL and UE iterators better that expect +begin()/CreateIterator() to already be on the first element as opposed to +IEnumerator\.MoveNext() moving onto the first element. + +This also means that you don't have to create an "inner" generator coroutine in +order to do parameter validation, like it's often suggested in C#. diff --git a/Docs/latent_node.png b/Docs/latent_node.png new file mode 100644 index 0000000000000000000000000000000000000000..d1c97a339a02448c3a5cc8dc479780fef1ce50c2 GIT binary patch literal 14008 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 literal 0 HcmV?d00001 diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/AggregateAwaiters.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/AggregateAwaiters.cpp new file mode 100644 index 00000000..9a418b9d --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/AggregateAwaiters.cpp @@ -0,0 +1,165 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5Coro/AggregateAwaiters.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private; + +int FAggregateAwaiter::GetResumerIndex() const +{ + checkf(Data->Count <= 0, TEXT("Internal error: resuming too early")); + checkf(Data->Index != -1, TEXT("Internal error: resuming with no result")); + return Data->Index; +} + +template +FAggregateAwaiter::FAggregateAwaiter(T All, const TArray>& Coroutines) + : Data(std::make_shared(All.value ? Coroutines.Num() : !!Coroutines.Num())) +{ + for (int i = 0; i < Coroutines.Num(); ++i) + Consume(Data, i, Coroutines[i]); +} +template UE5CORO_API FAggregateAwaiter::FAggregateAwaiter( + std::false_type, const TArray>&); +template UE5CORO_API FAggregateAwaiter::FAggregateAwaiter( + std::true_type, const TArray>&); + +bool FAggregateAwaiter::await_ready() +{ + checkf(Data, TEXT("Attempting to await moved-from aggregate awaiter")); + Data->Lock.lock(); + checkf(!Data->Promise, TEXT("Attempting to reuse aggregate awaiter")); + + // Unlock if ready and resume immediately by returning true, + // otherwise carry the lock to await_suspend/Suspend + bool bReady = Data->Count <= 0; + if (bReady) + Data->Lock.unlock(); + return bReady; +} + +void FAggregateAwaiter::Suspend(FPromise& Promise) +{ + checkf(!Data->Lock.try_lock(), TEXT("Internal error: lock was not taken")); + checkf(!Data->Promise, TEXT("Attempting to reuse aggregate awaiter")); + + Data->Promise = &Promise; + Data->Lock.unlock(); +} + +#if UE5CORO_CPP20 +FAnyAwaiter UE5Coro::WhenAny(const TArray>& Coroutines) +{ + return FAnyAwaiter(std::false_type(), Coroutines); +} +#endif + +FRaceAwaiter UE5Coro::Race(TArray> Array) +{ + return FRaceAwaiter(std::move(Array)); +} + +#if UE5CORO_CPP20 +FAllAwaiter UE5Coro::WhenAll(const TArray>& Coroutines) +{ + return FAllAwaiter(std::true_type(), Coroutines); +} +#endif + +FRaceAwaiter::FRaceAwaiter(TArray>&& Array) + : Data(std::make_shared(std::move(Array))) +{ + // Add a continuation to every coroutine, but any one of them might + // invalidate the array + for (int i = 0; i < Data->Handles.Num(); ++i) + { + TCoroutine<>* Coro; + { + // Must be limited in scope because ContinueWith may be synchronous + // and the lock is not recursive + std::scoped_lock _(Data->Lock); + if (Data->Index != -1) // Did a coroutine finish during this loop? + return; // Don't bother asking the others, they've all canceled + Coro = &Data->Handles[i]; + } + + Coro->ContinueWith([Data2 = Data, i] + { + std::unique_lock _(Data2->Lock); + + // Nothing to do if this wasn't the first one + if (Data2->Index != -1) + return; + Data2->Index = i; + + for (int j = 0; j < Data2->Handles.Num(); ++j) + if (j != i) // Cancel the others + Data2->Handles[j].Cancel(); + + if (auto* Promise = Data2->Promise) + { + _.unlock(); + Promise->Resume(); + } + }); + } +} + +bool FRaceAwaiter::await_ready() +{ + Data->Lock.lock(); + if (Data->Index != -1) + { + Data->Lock.unlock(); + return true; + } + else + return false; // Passing the lock to Suspend +} + +void FRaceAwaiter::Suspend(FPromise& Promise) +{ + // Expecting a lock from await_ready + checkf(!Data->Lock.try_lock(), TEXT("Internal error: lock not held")); + checkf(!Data->Promise, TEXT("Unexpected double race await")); + Data->Promise = &Promise; + Data->Lock.unlock(); +} + +int FRaceAwaiter::await_resume() noexcept +{ + // This will be read on the same thread that wrote Index, or after + // await_ready determined its value; no lock needed + checkf(Data->Index != -1, + TEXT("Internal error: resuming with unknown result")); + return Data->Index; +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/AnimationAwaiters.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/AnimationAwaiters.cpp new file mode 100644 index 00000000..5ad302ce --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/AnimationAwaiters.cpp @@ -0,0 +1,200 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5Coro/AnimationAwaiters.h" +#include "UE5CoroAnimCallbackTarget.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private; + +FAnimAwaiterBool Anim::MontageBlendingOut(UAnimInstance* Instance, + UAnimMontage* Montage) +{ + return {std::false_type(), Instance, Montage}; +} + +FAnimAwaiterBool Anim::MontageEnded(UAnimInstance* Instance, + UAnimMontage* Montage) +{ + return {std::true_type(), Instance, Montage}; +} + +FAnimAwaiterVoid Anim::NextNotify(UAnimInstance* Instance, FName NotifyName) +{ + return {std::monostate(), Instance, nullptr, NotifyName}; +} + +FAnimAwaiterTuple Anim::PlayMontageNotifyBegin(UAnimInstance* Instance, + UAnimMontage* Montage) +{ + return {std::false_type(), Instance, Montage}; +} + +FAnimAwaiterTuple Anim::PlayMontageNotifyEnd(UAnimInstance* Instance, + UAnimMontage* Montage) +{ + return {std::true_type(), Instance, Montage}; +} + +FAnimAwaiterPayload Anim::PlayMontageNotifyBegin(UAnimInstance* Instance, + UAnimMontage* Montage, + FName NotifyName) +{ + return {std::false_type(), Instance, Montage, NotifyName}; +} + +FAnimAwaiterPayload Anim::PlayMontageNotifyEnd(UAnimInstance* Instance, + UAnimMontage* Montage, + FName NotifyName) +{ + return {std::true_type(), Instance, Montage, NotifyName}; +} + +FAnimAwaiter::FAnimAwaiter(UAnimInstance* Instance, UAnimMontage*) +{ + checkf(IsInGameThread(), + TEXT("Animation awaiters may only be used on the game thread")); + checkf(Instance, TEXT("Attempting to wait on a null anim instance")); + // A null montage is valid, meaning "any montage" + + auto* Obj = NewObject(); + Target = TStrongObjectPtr(Obj); +} + +FAnimAwaiter::~FAnimAwaiter() +{ + checkf(IsInGameThread(), + TEXT("Unexpected anim awaiter destruction off the game thread")); + if (UNLIKELY(bSuspended)) + Target->CancelResume(); +} + +// These cannot be defaulted in the .h, they use UUE5CoroAnimCallbackTarget +FAnimAwaiter::FAnimAwaiter(const FAnimAwaiter&) = default; +FAnimAwaiter& FAnimAwaiter::operator=(const FAnimAwaiter&) = default; + +void FAnimAwaiter::Suspend(FPromise& Promise) +{ + bSuspended = true; + Target->RequestResume(Promise); +} + +template +template +TAnimAwaiter::TAnimAwaiter(TEnd, UAnimInstance* Instance, + UAnimMontage* Montage) + : FAnimAwaiter(Instance, Montage) +{ + if constexpr (Type == Bool) + Target->ListenForMontageEvent(Instance, Montage, TEnd::value); + else + { + static_assert(Type == NameAndPayload); + Target->ListenForPlayMontageNotify(Instance, Montage, {}, TEnd::value); + } +} + +template +template +TAnimAwaiter::TAnimAwaiter(TEnd, UAnimInstance* Instance, + UAnimMontage* Montage, FName NotifyName) + : FAnimAwaiter(Instance, Montage) +{ + if constexpr (Type == Void) + { + static_assert(std::is_same_v); + Target->ListenForNotify(Instance, Montage, NotifyName); + } + else + { + static_assert(Type == Payload); + Target->ListenForPlayMontageNotify(Instance, Montage, NotifyName, + TEnd::value); + } +} + +template +TAnimAwaiter::~TAnimAwaiter() = default; + +template +bool TAnimAwaiter::await_ready() +{ + checkf(IsInGameThread(), + TEXT("Animation awaiters may only be used on the game thread")); + auto* Obj = Target.Get(); + if (!std::holds_alternative(Obj->Result)) + { + // If Result is or contains a payload pointer, that has expired by now + if constexpr (Type == Payload) + std::get(Obj->Result) = nullptr; + else if constexpr (Type == NameAndPayload) + std::get(Obj->Result).Value = nullptr; + return true; + } + return false; +} + +template +auto TAnimAwaiter::await_resume() + -> std::conditional_t +{ + // bSuspended can be false + checkf(Target, TEXT("Internal error: resuming without a callback target")); + bSuspended = false; + + // The only reason we get here without a result is that the anim instance + // was destroyed early. + // The only clean way without to communicate this back to the caller without + // memory management nightmares or forcing everything through a TOptional is + // through an exception, which won't work for most UE projects. + // Interested callers can use IsValid(Instance) after co_await to determine + // if this happened, but this is expected to be a very rare situation. + // Usually, the caller is also getting destroyed and the stack will be + // unwound (by coroutine_handle::destroy()) instead of resuming. + auto& Result = Target->Result; + bool bDestroyed = std::holds_alternative(Result); + + if constexpr (Type == Bool) + return std::get(bDestroyed ? true : Result); + else if constexpr (Type == Payload) + return bDestroyed ? nullptr : std::get(Result); + else if constexpr (Type == NameAndPayload) + return bDestroyed ? FPayloadTuple(NAME_None, nullptr) + : std::get(Result); +} + +namespace UE5Coro::Private +{ +template class TAnimAwaiter; +template class TAnimAwaiter; +template class TAnimAwaiter; +template class TAnimAwaiter; +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/AsyncAwaiter.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/AsyncAwaiter.cpp new file mode 100644 index 00000000..d52de2c2 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/AsyncAwaiter.cpp @@ -0,0 +1,109 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5Coro/AsyncAwaiters.h" +#include "TimerThread.h" + +using namespace UE5Coro::Private; + +namespace +{ +class FResumeTask +{ + ENamedThreads::Type Thread; + FPromise& Promise; + +public: + explicit FResumeTask(ENamedThreads::Type Thread, FPromise& Promise) + : Thread(Thread), Promise(Promise) { } + + void DoTask(ENamedThreads::Type, FGraphEvent*) { Promise.Resume(); } + + ENamedThreads::Type GetDesiredThread() const { return Thread; } + + TStatId GetStatId() const + { + RETURN_QUICK_DECLARE_CYCLE_STAT(FResumeTask, + STATGROUP_ThreadPoolAsyncTasks); + } + + static ESubsequentsMode::Type GetSubsequentsMode() + { + return ESubsequentsMode::FireAndForget; + } +}; +} + +bool FAsyncAwaiter::await_ready() +{ + // Don't move threads if we're already on the target thread + auto ThisThread = FTaskGraphInterface::Get().GetCurrentThreadIfKnown(); + return (ThisThread & ThreadTypeMask) == (Thread & ThreadTypeMask); +} + +void FAsyncAwaiter::Suspend(FPromise& Promise) +{ + TGraphTask::CreateTask().ConstructAndDispatchWhenReady(Thread, + Promise); +} + +FAsyncTimeAwaiter::FAsyncTimeAwaiter(const FAsyncTimeAwaiter& Other) + : TargetTime(Other.TargetTime), U(Other.U) +{ +} + +FAsyncTimeAwaiter::~FAsyncTimeAwaiter() +{ + if (UNLIKELY(Promise)) + FTimerThread::Get().TryUnregister(this); +} + +bool FAsyncTimeAwaiter::await_ready() +{ + return FPlatformTime::Seconds() >= TargetTime; +} + +void FAsyncTimeAwaiter::Suspend(FPromise& InPromise) +{ + checkf(!Promise, TEXT("Internal error: double resume")); + if (U.bAnyThread) + U.Thread = ENamedThreads::AnyThread; + else + U.Thread = FTaskGraphInterface::Get().GetCurrentThreadIfKnown(); + Promise = &InPromise; + FTimerThread::Get().Register(this); +} + +void FAsyncTimeAwaiter::Resume() +{ + checkf(Promise, TEXT("Internal error: spurious resume without suspension")); + AsyncTask(U.Thread, [this] { Promise.exchange(nullptr)->Resume(); }); +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/AsyncAwaiters.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/AsyncAwaiters.cpp new file mode 100644 index 00000000..30c7cd97 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/AsyncAwaiters.cpp @@ -0,0 +1,155 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5Coro/AsyncAwaiters.h" +#include "UE5CoroDelegateCallbackTarget.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private; + +namespace +{ +struct FAutoStartResumeRunnable final : FRunnable +{ + FPromise& Promise; + std::atomic Thread; + + explicit FAutoStartResumeRunnable(FPromise& Promise, + EThreadPriority Priority, uint64 Affinity, + EThreadCreateFlags Flags) + : Promise(Promise), Thread(nullptr) + { + // This has to start as nullptr and get overwritten + Thread = FRunnableThread::Create(this, + TEXT("UE5Coro::Async::MoveToNewThread"), + 0, Priority, Affinity, Flags); + checkf(Thread, TEXT("Internal error: could not create thread")); + } + + virtual uint32 Run() override + { + Promise.Resume(); + return 0; + } + + virtual void Exit() override + { + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this] + { + while (!Thread.load()) + FPlatformProcess::Yield(); + Thread.load()->WaitForCompletion(); + delete Thread; + delete this; + }); + } +}; +} + +FAsyncAwaiter Async::MoveToThread(ENamedThreads::Type Thread) +{ + return FAsyncAwaiter(Thread); +} + +FAsyncAwaiter Async::MoveToGameThread() +{ + return FAsyncAwaiter(ENamedThreads::GameThread); +} + +FAsyncAwaiter Async::MoveToSimilarThread() +{ + return FAsyncAwaiter(FTaskGraphInterface::Get().GetCurrentThreadIfKnown()); +} + +FNewThreadAwaiter Async::MoveToNewThread(EThreadPriority Priority, + uint64 Affinity, + EThreadCreateFlags Flags) +{ + return FNewThreadAwaiter(Priority, Affinity, Flags); +} + +FAsyncTimeAwaiter Async::PlatformSeconds(double Seconds) +{ + return FAsyncTimeAwaiter(FPlatformTime::Seconds() + Seconds, false); +} + +FAsyncTimeAwaiter Async::PlatformSecondsAnyThread(double Seconds) +{ + return FAsyncTimeAwaiter(FPlatformTime::Seconds() + Seconds, true); +} + +FAsyncTimeAwaiter Async::UntilPlatformTime(double Time) +{ + return FAsyncTimeAwaiter(Time, false); +} + +FAsyncTimeAwaiter Async::UntilPlatformTimeAnyThread(double Time) +{ + return FAsyncTimeAwaiter(Time, true); +} + +void FNewThreadAwaiter::Suspend(FPromise& Promise) +{ + new FAutoStartResumeRunnable(Promise, Priority, Affinity, Flags); +} + +FDelegateAwaiter::~FDelegateAwaiter() +{ + Cleanup(); +} + +void FDelegateAwaiter::Suspend(FPromise& InPromise) +{ + checkf(!Promise, TEXT("Internal error: Unexpected double suspend")); + Promise = &InPromise; +} + +void FDelegateAwaiter::TryResumeOnce() +{ + if (Promise) + std::exchange(Promise, nullptr)->Resume(); +} + +UObject* FDelegateAwaiter::SetupCallbackTarget(std::function Fn) +{ + FGCScopeGuard _; + auto* Target = NewObject(); + Target->SetInternalFlags(EInternalObjectFlags::Async); + Target->Init(std::move(Fn)); + checkf(!Cleanup, TEXT("Internal error: double setup")); + Cleanup = [Target] + { + FGCScopeGuard _; + Target->ClearInternalFlags(EInternalObjectFlags::Async); + Target->MarkPendingKill(); + }; + return Target; +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/AsyncPromise.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/AsyncPromise.cpp new file mode 100644 index 00000000..e4edf379 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/AsyncPromise.cpp @@ -0,0 +1,39 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5Coro/AsyncAwaiters.h" + +using namespace UE5Coro::Private; + +bool FAsyncPromise::IsEarlyDestroy() const +{ + return ShouldCancel(); +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/AwaitableEvent.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/AwaitableEvent.cpp new file mode 100644 index 00000000..1a46feb8 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/AwaitableEvent.cpp @@ -0,0 +1,128 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5Coro/Threading.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private; + +FAwaitableEvent::FAwaitableEvent(EEventMode Mode, bool bInitialState) + : Mode(Mode), bActive(bInitialState) +{ + checkf(Mode == EEventMode::AutoReset || Mode == EEventMode::ManualReset, + TEXT("Invalid event mode")); +} + +#if UE5CORO_DEBUG +FAwaitableEvent::~FAwaitableEvent() +{ + ensureMsgf(!Awaiters, TEXT("Awaitable event destroyed with active awaiters")); +} +#endif + +void FAwaitableEvent::Trigger() +{ + Lock.lock(); + if (Mode == EEventMode::ManualReset) + { + bActive = true; + TryResumeAll(); + } + else if (Awaiters) + ResumeOne(); // AutoReset: don't set bActive + else + { + bActive = true; + Lock.unlock(); + } +} + +void FAwaitableEvent::Reset() +{ + std::scoped_lock _(Lock); + bActive = false; +} + +bool FAwaitableEvent::IsManualReset() const +{ + return Mode == EEventMode::ManualReset; +} + +bool FAwaitableEvent::await_ready() noexcept +{ + Lock.lock(); + bool bValue = bActive; + if (Mode == EEventMode::AutoReset) + bActive = false; + if (bValue) + { + Lock.unlock(); + return true; + } + else // Leave it locked + return false; +} + +void FAwaitableEvent::Suspend(FPromise& Promise) +{ + checkf(!Lock.try_lock(), TEXT("Internal error: suspension without lock")); + checkf(!bActive, TEXT("Internal error: suspending with active event")); + Awaiters = new FAwaitingPromise{&Promise, Awaiters}; + Lock.unlock(); +} + +void FAwaitableEvent::ResumeOne() +{ + checkf(!Lock.try_lock(), TEXT("Internal error: resuming without lock")); + checkf(Awaiters, TEXT("Internal error: attempting to resume nothing")); + auto* Node = std::exchange(Awaiters, Awaiters->Next); + Lock.unlock(); // The coroutine might want the lock + + auto* Promise = Node->Promise; + delete Node; // Do this first to help tail calls + Promise->Resume(); +} + +void FAwaitableEvent::TryResumeAll() +{ + checkf(!Lock.try_lock(), TEXT("Internal error: resuming without lock")); + + // Start a new awaiter list to make sure everything active at this point + // gets resumed eventually, even if the event is reset + auto* Node = std::exchange(Awaiters, nullptr); + Lock.unlock(); + + while (Node) + { + Node->Promise->Resume(); + delete std::exchange(Node, Node->Next); + } +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/AwaitableSemaphore.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/AwaitableSemaphore.cpp new file mode 100644 index 00000000..c1cee13e --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/AwaitableSemaphore.cpp @@ -0,0 +1,94 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5Coro/Threading.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private; + +FAwaitableSemaphore::FAwaitableSemaphore(int Capacity, int InitialCount) + : Capacity(Capacity), Count(InitialCount) +{ + checkf(Capacity > 0 && InitialCount >= 0 && InitialCount <= Capacity, + TEXT("Initial semaphore values out of range")); +} + +#if UE5CORO_DEBUG +FAwaitableSemaphore::~FAwaitableSemaphore() +{ + ensureMsgf(!Awaiters, + TEXT("Awaitable semaphore destroyed with active awaiters")); +} +#endif + +void FAwaitableSemaphore::Unlock(int InCount) +{ + checkf(InCount > 0, TEXT("Invalid count")); + Lock.lock(); + verifyf((Count += InCount) <= Capacity, + TEXT("Semaphore unlocked above maximum")); + TryResumeAll(); +} + +bool FAwaitableSemaphore::await_ready() +{ + Lock.lock(); + if (Count > 0) + { + verifyf(--Count >= 0, TEXT("Internal error: semaphore went negative")); + Lock.unlock(); + return true; + } + else // Leave it locked + return false; +} + +void FAwaitableSemaphore::Suspend(FPromise& Promise) +{ + checkf(!Lock.try_lock(), TEXT("Internal error: suspension without lock")); + Awaiters = new FAwaitingPromise{&Promise, Awaiters}; + Lock.unlock(); +} + +void FAwaitableSemaphore::TryResumeAll() +{ + checkf(!Lock.try_lock(), TEXT("Internal error: resuming without lock held")); + while (Awaiters && Count > 0) + { + auto* Node = std::exchange(Awaiters, Awaiters->Next); + verifyf(--Count >= 0, TEXT("Internal error: semaphore went negative")); + Lock.unlock(); + Node->Promise->Resume(); + delete Node; + Lock.lock(); + } + Lock.unlock(); +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/Cancellation.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/Cancellation.cpp new file mode 100644 index 00000000..fe8bc6d7 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/Cancellation.cpp @@ -0,0 +1,88 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5Coro/Cancellation.h" +#include "UE5Coro/AsyncCoroutine.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private; + +namespace +{ +// lvalue ref to work around TScopeGuard +void CleanupIfCanceled(std::function& Fn) +{ + if (GDestroyedEarly) + Fn(); +} +} + +FCancellationGuard::FCancellationGuard() +#if UE5CORO_DEBUG + : Promise(&FPromise::Current()) +#endif +{ + FPromise::Current().HoldCancellation(); +} + +FCancellationGuard::~FCancellationGuard() +{ +#if UE5CORO_DEBUG + checkf(Promise == &FPromise::Current(), TEXT("Hold/Release mismatch")); +#endif + FPromise::Current().ReleaseCancellation(); +} + +FOnCoroutineCanceled::FOnCoroutineCanceled(std::function Fn) + : TScopeGuard(std::bind(&CleanupIfCanceled, std::move(Fn))) +{ +} + +FCancellationAwaiter UE5Coro::FinishNowIfCanceled() +{ + return {}; +} + +bool UE5Coro::IsCurrentCoroutineCanceled() +{ + return FPromise::Current().ShouldCancel(true); +} + +bool FCancellationAwaiter::await_ready() +{ + return !FPromise::Current().ShouldCancel(false); +} + +void FCancellationAwaiter::Suspend(FPromise& Promise) +{ + // Resume is also responsible for cancellation-induced self-destruction + Promise.Resume(); +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/Coroutine.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/Coroutine.cpp new file mode 100644 index 00000000..e8958f7e --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/Coroutine.cpp @@ -0,0 +1,110 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5Coro/Coroutine.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private; + +const TCoroutine<> TCoroutine<>::CompletedCoroutine = []() -> TCoroutine<> +{ + co_return; +}(); + +void TCoroutine::Cancel() +{ + std::scoped_lock _(Extras->Lock); + // Holding the lock guarantees that Promise is active in the union + if (Extras->Promise) + Extras->Promise->Cancel(); +} + +bool TCoroutine<>::Wait(uint32 WaitTimeMilliseconds, + bool bIgnoreThreadIdleStats) const +{ + return Extras->Completed->Wait(WaitTimeMilliseconds, bIgnoreThreadIdleStats); +} + +bool TCoroutine<>::IsDone() const +{ + return Wait(0, true); +} + +bool TCoroutine<>::WasSuccessful() const +{ + return Extras->bWasSuccessful; +} + +void TCoroutine<>::SetDebugName(const TCHAR* Name) +{ +#if UE5CORO_DEBUG + if (ensureMsgf(GCurrentPromise, + TEXT("Attempting to set a debug name outside a coroutine"))) + GCurrentPromise->Extras->DebugName = Name; +#endif +} + +bool TCoroutine<>::operator==(const TCoroutine<>& Other) const noexcept +{ + return Extras == Other.Extras; +} + +#if UE5CORO_CPP20 +auto TCoroutine<>::operator<=>(const TCoroutine<>& Other) const noexcept + -> std::strong_ordering +{ +#if UE5CORO_PRIVATE_LIBCPP_IS_BROKEN + return Extras.get() <=> Other.Extras.get(); +#else + return Extras <=> Other.Extras; +#endif +} +#else +bool TCoroutine<>::operator!=(const TCoroutine<>& Other) const noexcept +{ + return !(*this == Other); +} + +bool TCoroutine<>::operator<(const TCoroutine<>& Other) const noexcept +{ + return Extras < Other.Extras; +} +#endif + +uint32 UE5Coro::GetTypeHash(const TCoroutine<>& Handle) noexcept +{ + return static_cast(std::hash>()(Handle)); +} + +size_t std::hash>::operator()(const TCoroutine<>& Handle) const noexcept +{ + return std::hash()(Handle.Extras.get()); +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/Generator.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/Generator.cpp new file mode 100644 index 00000000..7de780de --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/Generator.cpp @@ -0,0 +1,43 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5Coro/Generator.h" + +using namespace UE5Coro::Private; + +void FGeneratorPromise::unhandled_exception() +{ +#if PLATFORM_EXCEPTIONS_DISABLED + check(!"Exceptions are not supported"); +#else + throw; +#endif +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/HttpAwaiters.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/HttpAwaiters.cpp new file mode 100644 index 00000000..103ed4c2 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/HttpAwaiters.cpp @@ -0,0 +1,104 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5Coro/HttpAwaiters.h" +#include "UE5Coro/AsyncAwaiters.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private; + +FHttpAwaiter Http::ProcessAsync(FHttpRequestRef Request) +{ + return FHttpAwaiter(std::move(Request)); +} + +FHttpAwaiter::FState::FState(FHttpRequestRef&& Request) + : Request(std::move(Request)) +{ +} + +FHttpAwaiter::FHttpAwaiter(FHttpRequestRef&& Request) + : State(new FState(std::move(Request))) +{ + State->Request->OnProcessRequestComplete().BindSP(State.ToSharedRef(), + &FState::RequestComplete); + State->Request->ProcessRequest(); +} + +bool FHttpAwaiter::await_ready() +{ + std::unique_lock _(State->Lock); + + // Skip suspension if the request finished first + if (State->Result.has_value()) + return true; + + _.release(); // Carry the lock into Suspend() + checkf(!State->bSuspended, TEXT("Attempted second concurrent co_await")); + State->bSuspended = true; + return false; +} + +void FHttpAwaiter::Suspend(FPromise& Promise) +{ + // This should be locked from await_ready + checkf(!State->Lock.try_lock(), TEXT("Internal error: lock wasn't taken")); + State->Promise = &Promise; + State->Lock.unlock(); +} + +void FHttpAwaiter::FState::Resume() +{ + ensureMsgf(IsInGameThread(), + TEXT("Internal error: expected HTTP callback on the game thread")); + // leave bSuspended true to prevent any further suspensions (not co_awaits) + Promise->Resume(); +} + +void FHttpAwaiter::FState::RequestComplete(FHttpRequestPtr, + FHttpResponsePtr Response, + bool bConnectedSuccessfully) +{ + std::unique_lock _(Lock); + Result = MakeTuple(std::move(Response), bConnectedSuccessfully); + if (bSuspended) + { + _.unlock(); + Resume(); + } +} + +TTuple FHttpAwaiter::await_resume() +{ + checkf(State->Result.has_value(), + TEXT("Internal error: resuming with no value")); + return *State->Result; +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/LatentAwaiter.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/LatentAwaiter.cpp new file mode 100644 index 00000000..640cd5cf --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/LatentAwaiter.cpp @@ -0,0 +1,115 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "LatentActions.h" +#include "UE5Coro/AsyncCoroutine.h" +#include "UE5Coro/LatentAwaiters.h" +#include "UE5Coro/UE5CoroSubsystem.h" + +using namespace UE5Coro::Private; + +namespace +{ +struct [[nodiscard]] FPendingAsyncCoroutine final : FPendingLatentAction +{ + FAsyncPromise* Promise; + FLatentAwaiter* Awaiter; + + FPendingAsyncCoroutine(FAsyncPromise& Promise, FLatentAwaiter* Awaiter) + : Promise(&Promise), Awaiter(Awaiter) { } + UE_NONCOPYABLE(FPendingAsyncCoroutine); + + virtual ~FPendingAsyncCoroutine() override + { + if (Promise) + { + // This class doesn't own the coroutine (its Latent counterpart does) + Promise->Cancel(); + Promise->Resume(false); // No need to bypass cancellation holds + } + } + + virtual void UpdateOperation(FLatentResponse& Response) override + { + if (!Awaiter->ShouldResume()) + return; + + Response.DoneIf(true); + + // Ownership moves back to the coroutine itself + checkf(Promise, TEXT("Internal error: resuming null coroutine")); + std::exchange(Promise, nullptr)->Resume(); + } +}; +} + +FLatentAwaiter::FLatentAwaiter(FLatentAwaiter&& Other) noexcept + : State(Other.State), Resume(Other.Resume) +{ + Other.State = nullptr; + Other.Resume = nullptr; +} + +FLatentAwaiter::~FLatentAwaiter() +{ + if (LIKELY(Resume)) + (*Resume)(State, true); + State = nullptr; + Resume = nullptr; +} + +bool FLatentAwaiter::ShouldResume() +{ + checkf(IsInGameThread(), + TEXT("Latent awaiters may only be used on the game thread")); + checkf(State, TEXT("Attempting to poll invalid latent awaiter")); + return (*Resume)(State, false); +} + +void FLatentAwaiter::Suspend(FAsyncPromise& Promise) +{ + checkf(IsInGameThread(), + TEXT("Latent awaiters may only be used on the game thread")); + checkf(GWorld, + TEXT("Awaiting this can only be done in the context of a world")); + + // Prepare a latent action on the subsystem and transfer ownership to that + auto* Sys = GWorld->GetSubsystem(); + auto* Latent = new FPendingAsyncCoroutine(Promise, this); + auto LatentInfo = Sys->MakeLatentInfo(); + GWorld->GetLatentActionManager().AddNewAction(LatentInfo.CallbackTarget, + LatentInfo.UUID, Latent); +} + +void FLatentAwaiter::Suspend(FLatentPromise& Promise) +{ + Promise.SetCurrentAwaiter(this); +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/LatentAwaiters_AsyncLoad.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/LatentAwaiters_AsyncLoad.cpp new file mode 100644 index 00000000..fc8b415d --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/LatentAwaiters_AsyncLoad.cpp @@ -0,0 +1,247 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "Engine/AssetManager.h" +#include "UE5Coro/LatentAwaiters.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private; + +namespace +{ +template +struct TLatentLoader +{ + T Manager; + TArray Sources; + TSharedPtr Handle; + + explicit TLatentLoader(TArray Paths, TAsyncLoadPriority Priority) +#if UE5CORO_CPP20 + requires std::is_same_v +#endif + : Sources(std::move(Paths)) + { + static_assert(std::is_same_v); + checkf(IsInGameThread(), + TEXT("Latent awaiters may only be used on the game thread")); + Handle = Manager.RequestAsyncLoad(Sources, FStreamableDelegate(), + Priority); + } + + explicit TLatentLoader(TArray AssetIds, const TArray& Bundles, + TAsyncLoadPriority Priority) +#if UE5CORO_CPP20 + requires std::is_same_v +#endif + : Manager(UAssetManager::Get()), Sources(std::move(AssetIds)) + { + static_assert(std::is_same_v); + checkf(IsInGameThread(), + TEXT("Latent awaiters may only be used on the game thread")); + Handle = Manager.LoadPrimaryAssets(Sources, Bundles, + FStreamableDelegate(), Priority); + } + + ~TLatentLoader() + { + checkf(IsInGameThread(), TEXT("Unexpected cleanup off the game thread")); + if (Handle) + Handle->ReleaseHandle(); + } + + TArray ResolveItems() + { + checkf(IsInGameThread(), + TEXT("Unexpected object resolve request off the game thread")); + // Handle->GetLoadedAssets() is unreliable, the async loading BP nodes + // re-resolve the sources instead once loading is done. Let's do that. + TArray Items; + for (auto& i : Sources) + { + UObject* Obj = nullptr; + if constexpr (std::is_same_v) + Obj = i.ResolveObject(); + else if constexpr (std::is_same_v) + Obj = Manager.GetPrimaryAssetObject(i); + else + // This needs to depend on a template parameter + static_assert(false && std::is_void_v, "Unknown type"); + + // Null filtering matches how the array BP nodes behave + if (IsValid(Obj)) + Items.Add(Obj); + } + return Items; + } +}; +using FLatentLoader = TLatentLoader; +using FPrimaryLoader = TLatentLoader; + +template +bool ShouldResume(void* Loader, bool bCleanup) +{ + auto* This = static_cast(Loader); + + if (UNLIKELY(bCleanup)) + { + delete This; + return false; + } + + // This is the same logic that FLoadAssetActionBase::UpdateOperation() uses. + // !Handle is how UAssetManager communicates an instant/synchronous finish. + auto& Handle = This->Handle; + return !Handle || Handle->HasLoadCompleted() || Handle->WasCanceled(); +} +} + +template +TArray AsyncLoad::InternalResume(void* State) +{ + using T = std::conditional_t; + checkf(ShouldResume(State, false), + TEXT("Internal error: resuming with !ShouldResume")); + + return static_cast(State)->ResolveItems(); +} +template UE5CORO_API TArray AsyncLoad::InternalResume<0>(void*); +template UE5CORO_API TArray AsyncLoad::InternalResume<1>(void*); + +FLatentAwaiter Latent::AsyncLoadObjects(TArray Paths, + TAsyncLoadPriority Priority) +{ + return FLatentAwaiter(new FLatentLoader(std::move(Paths), Priority), + &ShouldResume); +} + +FLatentAwaiter Latent::AsyncLoadPrimaryAsset(const FPrimaryAssetId& AssetToLoad, + const TArray& LoadBundles, + TAsyncLoadPriority Priority) +{ + return AsyncLoadPrimaryAssets(TArray{AssetToLoad}, LoadBundles, Priority); +} + +FLatentAwaiter Latent::AsyncLoadPrimaryAssets(TArray AssetsToLoad, + const TArray& LoadBundles, + TAsyncLoadPriority Priority) +{ + return FLatentAwaiter( + new FPrimaryLoader(std::move(AssetsToLoad), LoadBundles, Priority), + &ShouldResume); +} + +auto Latent::AsyncLoadClass(TSoftClassPtr Ptr, + TAsyncLoadPriority Priority) + -> TAsyncLoadAwaiter +{ + return TAsyncLoadAwaiter( + AsyncLoadObjects(TArray{Ptr.ToSoftObjectPath()}, Priority)); +} + +auto Latent::AsyncLoadClasses(const TArray>& Ptrs, + TAsyncLoadPriority Priority) + -> TAsyncLoadAwaiter, 0> +{ + TArray Paths; + Paths.Reserve(Ptrs.Num()); + for (const auto& Ptr : Ptrs) + Paths.Add(Ptr.ToSoftObjectPath()); + + return TAsyncLoadAwaiter, 0>( + AsyncLoadObjects(std::move(Paths), Priority)); +} + +auto Latent::AsyncLoadPackage(const FString& Path, + const FGuid* Guid, const TCHAR* Package, + EPackageFlags PackageFlags, int32 PIEInstanceID, + TAsyncLoadPriority PackagePriority, + const FLinkerInstancingContext* InstancingContext) + -> FPackageLoadAwaiter +{ + checkf(IsInGameThread(), + TEXT("Latent awaiters may only be used on the game thread")); + return FPackageLoadAwaiter(Path, Guid, Package, PackageFlags, + PIEInstanceID, PackagePriority, + InstancingContext); +} + +FPackageLoadAwaiter::FPackageLoadAwaiter( + const FString& Path, const FGuid* Guid, const TCHAR* Package, + EPackageFlags PackageFlags, int32 PIEInstanceID, + TAsyncLoadPriority PackagePriority, + const FLinkerInstancingContext* InstancingContext) + : State(new FState) +{ + auto Delegate = FLoadPackageAsyncDelegate::CreateSP(State.ToSharedRef(), + &FState::Loaded); + LoadPackageAsync(Path, Guid, Package, std::move(Delegate), + PackageFlags, PIEInstanceID, PackagePriority, + InstancingContext); +} + +void FPackageLoadAwaiter::FState::Loaded(const FName&, UPackage* Package, + EAsyncLoadingResult::Type) +{ + checkf(IsInGameThread(), + TEXT("Internal error: expected callback on the game thread")); + Result.Reset(Package); // Store the result + + // Promise being nullptr indicates that the load finished between + // AsyncLoadPackage() and co_await + if (Promise) + Promise->Resume(); +} + +bool FPackageLoadAwaiter::await_ready() +{ + checkf(IsInGameThread(), + TEXT("Latent awaiters may only be used on the game thread")); + checkf(State, TEXT("Attempting to use invalid awaiter")); + return State->Result.IsValid(); +} + +void FPackageLoadAwaiter::Suspend(FPromise& Promise) +{ + checkf(IsInGameThread(), + TEXT("Latent awaiters may only be used on the game thread")); + checkf(!State->Promise, TEXT("Attempted second concurrent co_await")); + + State->Promise = &Promise; +} + +UPackage* FPackageLoadAwaiter::await_resume() +{ + checkf(IsInGameThread(), + TEXT("Internal error: expected to resume on the game thread")); + checkf(State, TEXT("Internal error: resuming without a result")); + return State->Result.Get(); +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/LatentAwaiters_AsyncQuery.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/LatentAwaiters_AsyncQuery.cpp new file mode 100644 index 00000000..93a21f8d --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/LatentAwaiters_AsyncQuery.cpp @@ -0,0 +1,234 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5Coro/LatentAwaiters.h" +#include + +using namespace UE5Coro; +using namespace UE5Coro::Private; + +namespace +{ +template +using TQueryDelegate = std::conditional_t, + FTraceDelegate, FOverlapDelegate>; +template +using TQueryDatum = std::conditional_t, + FTraceDatum, FOverlapDatum>; +} + +namespace UE5Coro::Private +{ +template +class TAsyncQueryAwaiter::TImpl +{ +public: + FPromise* Promise = nullptr; + std::optional> Result; + + void ReceiveResult(const FTraceHandle&, TQueryDatum& Datum) + { + // Receive results + if constexpr (std::is_same_v) + Result = std::move(Datum.OutHits); + else + Result = std::move(Datum.OutOverlaps); + + // If the coroutine is suspended (Promise is valid), resume it now + if (Promise) + Promise->Resume(); + } +}; + +template +template +TAsyncQueryAwaiter::TAsyncQueryAwaiter(UWorld* World, + FTraceHandle (UWorld::*Fn)(P...), + A... Params) + : Impl(new TImpl) +{ + checkf(IsInGameThread(), + TEXT("Async queries may only be started from the game thread.")); + auto Delegate = TQueryDelegate::CreateSP(Impl.ToSharedRef(), + &TImpl::ReceiveResult); + (World->*Fn)(Params..., &Delegate, 0); +} + +template +TAsyncQueryAwaiter::~TAsyncQueryAwaiter() = default; + +template +TAsyncQueryAwaiter& TAsyncQueryAwaiter::operator co_await() & +{ + return *this; +} + +template +TAsyncQueryAwaiterRV& TAsyncQueryAwaiter::operator co_await() && +{ + static_assert(sizeof(*this) == sizeof(TAsyncQueryAwaiterRV)); + // Technically, this object is not a TAsyncQueryAwaiterRV + return *std::launder(static_cast*>(this)); +} + +template +bool TAsyncQueryAwaiter::await_ready() +{ + checkf(IsInGameThread(), + TEXT("Async queries may only be awaited on the game thread.")); + return Impl->Result.has_value(); +} + +template +void TAsyncQueryAwaiter::Suspend(FPromise& Promise) +{ + checkf(IsInGameThread(), + TEXT("Async queries may only be awaited on the game thread.")); + checkf(!Impl->Promise, TEXT("Attempted second concurrent co_await")); + Impl->Promise = &Promise; +} + +template +const TArray& TAsyncQueryAwaiter::await_resume() +{ + checkf(IsInGameThread(), + TEXT("Internal error: expected to resume on the game thread")); + checkf(Impl->Result.has_value(), + TEXT("Internal error: resuming without a query result")); + return *Impl->Result; +} + +template +TArray TAsyncQueryAwaiterRV::await_resume() +{ + return const_cast&&>(TAsyncQueryAwaiter::await_resume()); +} + +template class UE5CORO_API TAsyncQueryAwaiter; +template class UE5CORO_API TAsyncQueryAwaiterRV; +template class UE5CORO_API TAsyncQueryAwaiter; +template class UE5CORO_API TAsyncQueryAwaiterRV; +} + +TAsyncQueryAwaiter Latent::AsyncLineTraceByChannel( + const UObject* WorldContextObject, EAsyncTraceType InTraceType, + const FVector& Start, const FVector& End, ECollisionChannel TraceChannel, + const FCollisionQueryParams& Params, + const FCollisionResponseParams& ResponseParam) +{ + return TAsyncQueryAwaiter( + GEngine->GetWorldFromContextObjectChecked(WorldContextObject), + &UWorld::AsyncLineTraceByChannel, InTraceType, Start, End, TraceChannel, + Params, ResponseParam); +} + +TAsyncQueryAwaiter Latent::AsyncLineTraceByObjectType( + const UObject* WorldContextObject, EAsyncTraceType InTraceType, + const FVector& Start, const FVector& End, + const FCollisionObjectQueryParams& ObjectQueryParams, + const FCollisionQueryParams& Params) +{ + return TAsyncQueryAwaiter( + GEngine->GetWorldFromContextObjectChecked(WorldContextObject), + &UWorld::AsyncLineTraceByObjectType, InTraceType, Start, End, + ObjectQueryParams, Params); +} + +TAsyncQueryAwaiter Latent::AsyncLineTraceByProfile( + const UObject* WorldContextObject, EAsyncTraceType InTraceType, + const FVector& Start, const FVector& End, FName ProfileName, + const FCollisionQueryParams& Params) +{ + return TAsyncQueryAwaiter( + GEngine->GetWorldFromContextObjectChecked(WorldContextObject), + &UWorld::AsyncLineTraceByProfile, InTraceType, Start, End, ProfileName, + Params); +} + +TAsyncQueryAwaiter Latent::AsyncSweepByChannel( + const UObject* WorldContextObject, EAsyncTraceType InTraceType, + const FVector& Start, const FVector& End, const FQuat& Rot, + ECollisionChannel TraceChannel, const FCollisionShape& CollisionShape, + const FCollisionQueryParams& Params, + const FCollisionResponseParams& ResponseParam) +{ + return TAsyncQueryAwaiter( + GEngine->GetWorldFromContextObjectChecked(WorldContextObject), + &UWorld::AsyncSweepByChannel, InTraceType, Start, End, Rot, + TraceChannel, CollisionShape, Params, ResponseParam); +} + +TAsyncQueryAwaiter Latent::AsyncSweepByObjectType( + const UObject* WorldContextObject, EAsyncTraceType InTraceType, + const FVector& Start, const FVector& End, const FQuat& Rot, + const FCollisionObjectQueryParams& ObjectQueryParams, + const FCollisionShape& CollisionShape, const FCollisionQueryParams& Params) +{ + return TAsyncQueryAwaiter( + GEngine->GetWorldFromContextObjectChecked(WorldContextObject), + &UWorld::AsyncSweepByObjectType, InTraceType, Start, End, Rot, + ObjectQueryParams, CollisionShape, Params); +} + +TAsyncQueryAwaiter Latent::AsyncSweepByProfile( + const UObject* WorldContextObject, EAsyncTraceType InTraceType, + const FVector& Start, const FVector& End, const FQuat& Rot, + FName ProfileName, const FCollisionShape& CollisionShape, + const FCollisionQueryParams& Params) +{ + return TAsyncQueryAwaiter( + GEngine->GetWorldFromContextObjectChecked(WorldContextObject), + &UWorld::AsyncSweepByProfile, InTraceType, Start, End, Rot, ProfileName, + CollisionShape, Params); +} + +TAsyncQueryAwaiter Latent::AsyncOverlapByChannel( + const UObject* WorldContextObject, const FVector& Pos, const FQuat& Rot, + ECollisionChannel TraceChannel, const FCollisionShape& CollisionShape, + const FCollisionQueryParams& Params, + const FCollisionResponseParams& ResponseParam) +{ + return TAsyncQueryAwaiter( + GEngine->GetWorldFromContextObjectChecked(WorldContextObject), + &UWorld::AsyncOverlapByChannel, Pos, Rot, TraceChannel, CollisionShape, + Params, ResponseParam); +} + +TAsyncQueryAwaiter Latent::AsyncOverlapByObjectType( + const UObject* WorldContextObject, const FVector& Pos, const FQuat& Rot, + const FCollisionObjectQueryParams& ObjectQueryParams, + const FCollisionShape& CollisionShape, const FCollisionQueryParams& Params) +{ + return TAsyncQueryAwaiter( + GEngine->GetWorldFromContextObjectChecked(WorldContextObject), + &UWorld::AsyncOverlapByObjectType, Pos, Rot, ObjectQueryParams, + CollisionShape, Params); +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/LatentAwaiters_Wait.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/LatentAwaiters_Wait.cpp new file mode 100644 index 00000000..08466242 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/LatentAwaiters_Wait.cpp @@ -0,0 +1,200 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5Coro/LatentAwaiters.h" +#include "Engine/World.h" +#include "UE5CoroDelegateCallbackTarget.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private; + +namespace +{ +bool WaitUntilFrame(void* State, bool) +{ + return GFrameCounter >= reinterpret_cast(State); +} + +template +bool WaitUntilTime(void* State, bool bCleanup) +{ + // Don't attempt to access GWorld in this case, it could be nullptr + if (UNLIKELY(bCleanup)) + return false; + + auto& TargetTime = reinterpret_cast(State); + checkf(GWorld, TEXT("Internal error: Latent poll outside of a world")); + return (GWorld->*GetTime)() >= TargetTime; +} + +bool WaitUntilPredicate(void* State, bool bCleanup) +{ + auto* Function = static_cast*>(State); + if (UNLIKELY(bCleanup)) + { + delete Function; + return false; + } + + return (*Function)(); +} + +template +FLatentAwaiter GenericUntil(float Time) +{ +#if ENABLE_NAN_DIAGNOSTIC + if (FMath::IsNaN(Time)) + { + logOrEnsureNanError(TEXT("Latent wait started with NaN time")); + } +#endif + checkf(IsInGameThread(), + TEXT("Latent awaiters may only be used on the game thread")); + checkf(GWorld, + TEXT("This function may only be used in the context of a world")); + + if constexpr (bTimeIsOffset) + Time += (GWorld->*GetTime)(); + + ensureMsgf((GWorld->*GetTime)() <= Time, + TEXT("Latent wait will finish immediately")); + + void* State = nullptr; + reinterpret_cast(State) = Time; + return FLatentAwaiter(State, &WaitUntilTime); +} + +class [[nodiscard]] FUntilDelegateState + : public std::enable_shared_from_this +{ + TStrongObjectPtr Target; + // This object is on the game thread, but the delegate might not be + std::atomic bExecuted = false; + +public: + explicit FUntilDelegateState(UUE5CoroDelegateCallbackTarget* Target) + : Target(Target) { } + + void Init() + { + Target->Init([Weak = weak_from_this()](void*) + { + if (auto Strong = Weak.lock()) + Strong->bExecuted = true; + }); + } + + static bool ShouldResume(void* State, bool bCleanup) + { + auto& This = *static_cast*>(State); + if (UNLIKELY(bCleanup)) + { + if (This->Target.IsValid()) + This->Target->MarkPendingKill(); + delete &This; + return false; + } + return This->bExecuted; + } +}; +} + +FLatentAwaiter Latent::NextTick() +{ + return Ticks(1); +} + +FLatentAwaiter Latent::Ticks(int64 Ticks) +{ + ensureMsgf(Ticks >= 0, TEXT("Invalid number of ticks %lld"), Ticks); + static_assert(sizeof(void*) == sizeof(uint64), + "32-bit platforms are not supported"); + uint64 Target = GFrameCounter + Ticks; + return FLatentAwaiter(reinterpret_cast(Target), &WaitUntilFrame); +} + +FLatentAwaiter Latent::Until(std::function Function) +{ + checkf(Function, TEXT("Provided function is empty")); + return FLatentAwaiter(new std::function(std::move(Function)), + &WaitUntilPredicate); +} + +std::tuple Private::UntilDelegateCore() +{ + checkf(IsInGameThread(), TEXT("") + "Awaiting delegates this way is only available on the game thread. " + "co_awaiting delegates directly works on any thread."); + auto* Target = NewObject(); + auto* State = new auto(std::make_shared(Target)); + (*State)->Init(); + return {FLatentAwaiter(State, &FUntilDelegateState::ShouldResume), Target}; +} + +FLatentAwaiter Latent::Seconds(float Seconds) +{ + return GenericUntil<&UWorld::GetTimeSeconds, true>(Seconds); +} + +FLatentAwaiter Latent::UnpausedSeconds(float Seconds) +{ + return GenericUntil<&UWorld::GetUnpausedTimeSeconds, true>(Seconds); +} + +FLatentAwaiter Latent::RealSeconds(float Seconds) +{ + return GenericUntil<&UWorld::GetRealTimeSeconds, true>(Seconds); +} + +FLatentAwaiter Latent::AudioSeconds(float Seconds) +{ + return GenericUntil<&UWorld::GetAudioTimeSeconds, true>(Seconds); +} + +FLatentAwaiter Latent::UntilTime(float Seconds) +{ + return GenericUntil<&UWorld::GetTimeSeconds, false>(Seconds); +} + +FLatentAwaiter Latent::UntilUnpausedTime(float Seconds) +{ + return GenericUntil<&UWorld::GetUnpausedTimeSeconds, false>(Seconds); +} + +FLatentAwaiter Latent::UntilRealTime(float Seconds) +{ + return GenericUntil<&UWorld::GetRealTimeSeconds, false>(Seconds); +} + +FLatentAwaiter Latent::UntilAudioTime(float Seconds) +{ + return GenericUntil<&UWorld::GetAudioTimeSeconds, false>(Seconds); +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/LatentCallbacks.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/LatentCallbacks.cpp new file mode 100644 index 00000000..d3c60b2e --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/LatentCallbacks.cpp @@ -0,0 +1,68 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5Coro/LatentCallbacks.h" +#include "LatentExitReason.h" + +using namespace UE5Coro::Latent; +using namespace UE5Coro::Private; + +ELatentExitReason UE5Coro::Private::GLatentExitReason = ELatentExitReason::Normal; + +namespace +{ +template +void CleanupIf(std::function& Fn) // lvalue ref to work around TScopeGuard +{ + // This race condition is explicitly allowed due to the IsInGameThread check + if (static_cast(GLatentExitReason) & static_cast(Reason) && + IsInGameThread()) + Fn(); +} +} + +FOnAbnormalExit::FOnAbnormalExit(std::function Fn) + : TScopeGuard(std::bind(&CleanupIf, + std::move(Fn))) +{ +} + +FOnActionAborted::FOnActionAborted(std::function Fn) + : TScopeGuard(std::bind(&CleanupIf, + std::move(Fn))) +{ +} + +FOnObjectDestroyed::FOnObjectDestroyed(std::function Fn) + : TScopeGuard(std::bind(&CleanupIf, + std::move(Fn))) +{ +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/LatentChain.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/LatentChain.cpp new file mode 100644 index 00000000..34513ba8 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/LatentChain.cpp @@ -0,0 +1,60 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5Coro/LatentAwaiters.h" +#include "UE5Coro/UE5CoroSubsystem.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private; + +std::tuple Private::MakeLatentInfo() +{ + checkf(GWorld, TEXT("Internal error: Unguarded world access")); + auto* Sys = GWorld->GetSubsystem(); + // Will be Released by the FLatentAwaiter from the caller + // and UUE5CoroSubsystem on the latent action's completion. + auto* Done = new FTwoLives; + return {Sys->MakeLatentInfo(Done), Done}; +} + +FLatentChainAwaiter::FLatentChainAwaiter(FTwoLives* Done) noexcept + : FLatentAwaiter(Done, &FTwoLives::ShouldResume) +{ +} + +bool FLatentChainAwaiter::await_resume() +{ + // This function being called implies that there's a reference on State. + const int& UserData = static_cast(State)->UserData; + checkf(UserData == 0 || UserData == 1, TEXT("Unexpected user data")); + // Core() sets this, otherwise it's 0. This is the only usage currently. + return UserData == 1; +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/LatentExitReason.h b/Plugins/UE5Coro/Source/UE5Coro/Private/LatentExitReason.h new file mode 100644 index 00000000..b22e4385 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/LatentExitReason.h @@ -0,0 +1,43 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +namespace UE5Coro::Private +{ +extern enum class ELatentExitReason : uint8 +{ + Normal = 0, + ActionAborted = 1, + ObjectDestroyed = 2, + AnyAbnormal = ActionAborted | ObjectDestroyed, +} GLatentExitReason; +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/LatentPromise.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/LatentPromise.cpp new file mode 100644 index 00000000..a4577b24 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/LatentPromise.cpp @@ -0,0 +1,352 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include +#include "LatentActions.h" +#include "LatentExitReason.h" +#include "UE5Coro/AsyncAwaiters.h" +#include "UE5Coro/AsyncCoroutine.h" +#include "UE5Coro/LatentAwaiters.h" + +using namespace UE5Coro::Private; + +namespace +{ +class [[nodiscard]] FPendingLatentCoroutine final : public FPendingLatentAction +{ + // The coroutine may move to other threads, but this object only interacts + // with its promise on the game thread. + // Since latent promises are destroyed on the game thread, there's nothing + // to synchronize and the lock is not used to access Extras->Promise. + std::shared_ptr Extras; + bool bTriggerLink = false; + FLatentActionInfo LatentInfo; + FLatentAwaiter* CurrentAwaiter = nullptr; + +public: + explicit FPendingLatentCoroutine(std::shared_ptr Extras, + FLatentActionInfo LatentInfo) + : Extras(std::move(Extras)), LatentInfo(LatentInfo) { } + + UE_NONCOPYABLE(FPendingLatentCoroutine); + + virtual ~FPendingLatentCoroutine() override + { + checkf(IsInGameThread(), + TEXT("Unexpected latent action off the game thread")); + if (auto* LatentPromise = static_cast(Extras->Promise); + LIKELY(LatentPromise)) + { + LatentPromise->Cancel(); + // Process the cancellation right away, there might be no resumption + LatentPromise->Resume(true); + } + } + + virtual void UpdateOperation(FLatentResponse& Response) override + { + checkf(IsInGameThread(), + TEXT("Internal error: expected game thread update")); + auto* LatentPromise = static_cast(Extras->Promise); + + if (UNLIKELY(!LatentPromise)) + { + FinishNow(Response); + return; + } + + if (CurrentAwaiter && CurrentAwaiter->ShouldResume()) + { + CurrentAwaiter = nullptr; + // This might set the awaiter for next time + LatentPromise->Resume(); + } + + // Resume() might have deleted LatentPromise, check it again + if (LIKELY(Extras->Promise)) + { + checkf(!Extras->IsComplete(), + TEXT("Internal error: completed promise was not cleared")); + + // If ownership is with the game thread, check if the promise is + // waiting to be completed + if (LatentPromise->IsOnGameThread()) + { + using FLatentHandle = stdcoro::coroutine_handle; + if (auto Handle = FLatentHandle::from_promise(*LatentPromise); + Handle.done() || LatentPromise->ShouldCancel(false)) + FinishNow(Response); + } + } + else + FinishNow(Response); + } + + virtual void NotifyActionAborted() override + { + checkf(IsInGameThread(), + TEXT("Internal error: expected callback from the game thread")); + if (auto* LatentPromise = static_cast(Extras->Promise); + LIKELY(LatentPromise)) + LatentPromise->SetExitReason(ELatentExitReason::ActionAborted); + } + + virtual void NotifyObjectDestroyed() override + { + checkf(IsInGameThread(), + TEXT("Internal error: expected callback from the game thread")); + if (auto* LatentPromise = static_cast(Extras->Promise); + LIKELY(LatentPromise)) + LatentPromise->SetExitReason(ELatentExitReason::ObjectDestroyed); + } + + const FLatentActionInfo& GetLatentInfo() const { return LatentInfo; } + + void RequestLink() { bTriggerLink = true; } + + void FinishNow(FLatentResponse& Response) + { + if (bTriggerLink) + Response.TriggerLink(LatentInfo.ExecutionFunction, + LatentInfo.Linkage, LatentInfo.CallbackTarget); + Response.DoneIf(true); + } + + void SetCurrentAwaiter(FLatentAwaiter* Awaiter) + { + checkf(IsInGameThread(), + TEXT("Latent awaiters may only be used on the game thread")); + if (Awaiter) + ensureMsgf(!CurrentAwaiter, TEXT("Unexpected double await")); + + CurrentAwaiter = Awaiter; + } +}; +} + +void FLatentPromise::CreateLatentAction() +{ + checkf(IsInGameThread(), + TEXT("Latent coroutines may only be started on the game thread")); + checkf(Context.IsValid() && IsValid(Context.Get()->GetWorld()), + TEXT("Could not determine world for latent coroutine")); + + auto* World = Context.Get()->GetWorld(); + auto* Sys = World->GetSubsystem(); + checkf(IsValid(Sys), TEXT("Internal error: couldn't find UE5Coro subsystem")); + CreateLatentAction(Sys->MakeLatentInfo()); +} + +// This is a separate function so that template Init() doesn't need the type +void FLatentPromise::CreateLatentAction(FLatentActionInfo&& LatentInfo) +{ + // The static_assert on coroutine_traits and Init() logic prevent this + checkf(!PendingLatentCoroutine, + TEXT("Internal error: multiple latent infos were not prevented")); + + PendingLatentCoroutine = new FPendingLatentCoroutine(Extras, LatentInfo); +} + +void FLatentPromise::Init() +{ + // Provide a valid world context parameter to your coroutine if this gets hit! + checkf(Context.IsValid() && IsValid(Context.Get()->GetWorld()), + TEXT("Could not determine world for latent coroutine")); + + // Handle being forced to latent without a FLatentActionInfo + if (!PendingLatentCoroutine) + CreateLatentAction(); +} + +FLatentPromise::~FLatentPromise() +{ + checkf(IsInGameThread(), + TEXT("Unexpected latent coroutine destruction off the game thread")); + GLatentExitReason = ELatentExitReason::Normal; +} + +bool FLatentPromise::IsEarlyDestroy() const +{ + // Destruction can come before or after final_suspend, but the only reason + // it can come before is a cancellation, both regular and forced + return !(LatentFlags & LF_Successful); +} + +void FLatentPromise::Resume(bool bBypassCancellationHolds) +{ + if (UNLIKELY(bBypassCancellationHolds)) + { + // This can only happen from a game thread latent update + checkf(IsInGameThread() && ShouldCancel(true), + TEXT("Internal error: wrong state for bypass request")); + + // If ownership is borrowed, let the guaranteed future Resume call + // handle this + if (LatentFlags & LF_Detached) + return; + + // Otherwise, proceed with re-attaching and destruction + } + + // Return ownership to the game thread and the latent action manager + // once the multi-threaded adventure is over + if (LatentFlags & LF_Detached && IsInGameThread()) + AttachToGameThread(false); + + FPromise::Resume(bBypassCancellationHolds); +} + +void FLatentPromise::CancelFromWithin() +{ + // Force move the coroutine back to the game thread + AttachToGameThread(true); + + Cancel(); + + checkf(ShouldCancel(false), + TEXT("Latent coroutines may only be canceled from within if no " + "FCancellationGuards are present")); + + // If the self-cancellation arrived on the game thread, don't wait for the + // next FPendingLatentCoroutine tick to start cleaning up + if (IsInGameThread()) + ThreadSafeDestroy(); +} + +void FLatentPromise::ThreadSafeDestroy() +{ + // Latent coroutines always end on the game thread + if (!IsInGameThread()) + { + AsyncTask(ENamedThreads::GameThread, [this] { ThreadSafeDestroy(); }); + return; + } + + // Since we're on the game thread now, there's no possibility of a race with + // ~FPendingLatentCoroutine requesting another deletion + GLatentExitReason = ExitReason; + FPromise::ThreadSafeDestroy(); // Counts as delete this; + checkf(GLatentExitReason == ELatentExitReason::Normal, + TEXT("Internal error: latent exit reason not restored")); +} + +void FLatentPromise::AttachToGameThread(bool bFromAnyThread) +{ + checkf(bFromAnyThread || IsInGameThread(), + TEXT("Internal error: expected to be on the game thread")); + LatentFlags &= ~LF_Detached; +} + +void FLatentPromise::DetachFromGameThread() +{ + // Calling this method "pins" the promise and coroutine state, deferring any + // destruction requests from the latent action manager. + // This is useful for threading or callback-based awaiters to ensure that + // there will be a valid promise and coroutine state to return to. + // FLatentAwaiters use a dedicated code path and do not call this, as they + // support destruction while being co_awaited. + LatentFlags |= LF_Detached; +} + +bool FLatentPromise::IsOnGameThread() const +{ + return !(LatentFlags & LF_Detached); +} + +void FLatentPromise::SetExitReason(ELatentExitReason Reason) +{ + checkf(ExitReason == ELatentExitReason::Normal, + TEXT("Internal error: setting conflicting exit reasons")); + ExitReason = Reason; +} + +void FLatentPromise::SetCurrentAwaiter(FLatentAwaiter* Awaiter) +{ + checkf(IsInGameThread(), + TEXT("Latent awaiters may only be used on the game thread")); + // How is a new latent awaiter getting added in these states? + checkf(LatentFlags == 0, TEXT("Unexpected state in latent coroutine")); + auto* Pending = static_cast(PendingLatentCoroutine); + Pending->SetCurrentAwaiter(Awaiter); +} + +FInitialSuspend FLatentPromise::initial_suspend() +{ + checkf(IsInGameThread(), + TEXT("Latent coroutines may only be started on the game thread")); + checkf(Context.IsValid() && IsValid(Context.Get()->GetWorld()), + TEXT("Internal error: latent coroutine starts in invalid/stale world")); + + auto* Owner = Context.Get(); + auto& LAM = Owner->GetWorld()->GetLatentActionManager(); + auto* Pending = static_cast(PendingLatentCoroutine); + auto& LatentInfo = Pending->GetLatentInfo(); + + // Don't let the coroutine run and clean up if this is a duplicate + if (LAM.FindExistingAction(Owner, LatentInfo.UUID)) + return {FInitialSuspend::Destroy}; + + // Also refuse to run if there's no callback target + if (!ensureMsgf(IsValid(LatentInfo.CallbackTarget), + TEXT("Not starting latent coroutine with invalid target"))) + return {FInitialSuspend::Destroy}; + + // Make the latent action owned by the context instead of the callback + // target to provide a better match for the "latent `this` protection" + // offered by FLatentActionManager. + // These will usually be the same object for latent UFUNCTIONs, but + // FForceLatentCoroutine uses UUE5CoroSubsystem as a helper callback target. + LAM.AddNewAction(Owner, LatentInfo.UUID, Pending); + + // Let the coroutine start immediately on its calling thread + return {FInitialSuspend::Resume}; +} + +template +FFinalSuspend FLatentPromise::final_suspend() noexcept +{ + // Too late for cancellations now, continue with BP if requested + if constexpr (bTriggerBP) + static_cast(PendingLatentCoroutine) + ->RequestLink(); + + // Flags are overwritten, i.e., the coroutine is unconditionally reattached + LatentFlags = LF_Successful; + + // Due to the free-threaded attachment, there's a potential data race now, + // including another thread deleting `this`, so it may not be used anymore + + // If running on the game thread, complete (self-destruct) now. + // Otherwise, let FPendingLatentCoroutine deal with it when it's ticked. + return {IsInGameThread()}; +} +template UE5CORO_API FFinalSuspend FLatentPromise::final_suspend(); +template UE5CORO_API FFinalSuspend FLatentPromise::final_suspend(); diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/LatentTimeline.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/LatentTimeline.cpp new file mode 100644 index 00000000..20ea94e5 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/LatentTimeline.cpp @@ -0,0 +1,128 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5Coro/LatentTimeline.h" +#include "UE5Coro/Coroutine.h" +#include "UE5Coro/LatentAwaiters.h" + +using namespace UE5Coro; +using namespace UE5Coro::Latent; + +namespace +{ +// Force to latent, otherwise it would keep running even after the world is gone. +template +TCoroutine<> CommonTimeline(const UObject* WCO, float From, float To, + float Length, std::function Fn, + bool bRunWhenPaused, FForceLatentCoroutine = {}) +{ +#if ENABLE_NAN_DIAGNOSTIC + if (FMath::IsNaN(From) || FMath::IsNaN(To) || FMath::IsNaN(Length)) + { + logOrEnsureNanError(TEXT("Latent timeline started with NaN parameter")); + } + // Not a NaN right now but it could lead to one after division + if (Length < SMALL_NUMBER) + { + logOrEnsureNanError( + TEXT("Latent timeline started with very short length")); + } +#endif + // Clamp negative and small lengths to something that can be divided by + Length = FMath::Max(Length, SMALL_NUMBER); + + checkf(IsInGameThread(), + TEXT("Latent coroutines may only be started on the game thread")); + checkf(IsValid(WCO) && IsValid(WCO->GetWorld()), + TEXT("Latent timeline started without valid world")); + auto* World = WCO->GetWorld(); + + float Start = (World->*GetTime)(); + for (;;) + { + // Make sure the last call is exactly at Length + float Time = FMath::Min((World->*GetTime)() - Start, Length); + // If the world is paused, only evaluate the function if asked. + if (bRunWhenPaused || !World->IsPaused()) + { + float Value = FMath::Lerp(From, To, Time / Length); +#if ENABLE_NAN_DIAGNOSTIC + // Incredibly high Time values could cause this to go wrong + if (UNLIKELY(!FMath::IsFinite(Value))) + { + logOrEnsureNanError(TEXT("Latent timeline derailed")); + } +#endif + Fn(Value); + if (Time == Length) // This hard == will work due to Min() + co_return; + } + co_await NextTick(); + + // How and why is the latent action manager still ticking this? + checkf(IsValid(World), + TEXT("Internal error: timeline still running on invalid world")); + } +} +} + +TCoroutine<> Latent::Timeline(const UObject* WCO, float From, float To, + float Length, std::function Fn, + bool bRunWhenPaused) +{ + return CommonTimeline<&UWorld::GetTimeSeconds>( + WCO, From, To, Length, std::move(Fn), bRunWhenPaused); +} + +TCoroutine<> Latent::UnpausedTimeline(const UObject* WCO, float From, + float To, float Length, + std::function Fn, + bool bRunWhenPaused) +{ + return CommonTimeline<&UWorld::GetUnpausedTimeSeconds>( + WCO, From, To, Length, std::move(Fn), bRunWhenPaused); +} + +TCoroutine<> Latent::RealTimeline(const UObject* WCO, float From, float To, + float Length, std::function Fn, + bool bRunWhenPaused) +{ + return CommonTimeline<&UWorld::GetRealTimeSeconds>( + WCO, From, To, Length, std::move(Fn), bRunWhenPaused); +} + +TCoroutine<> Latent::AudioTimeline(const UObject* WCO, float From, float To, + float Length, std::function Fn, + bool bRunWhenPaused) +{ + return CommonTimeline<&UWorld::GetAudioTimeSeconds>( + WCO, From, To, Length, std::move(Fn), bRunWhenPaused); +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/Promise.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/Promise.cpp new file mode 100644 index 00000000..a4f14295 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/Promise.cpp @@ -0,0 +1,174 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5Coro/AsyncCoroutine.h" +#include "Misc/ScopeExit.h" + +using namespace UE5Coro::Private; + +#if UE5CORO_DEBUG +std::atomic UE5Coro::Private::GLastDebugID = -1; // -1 = no coroutines yet +#endif + +thread_local FPromise* UE5Coro::Private::GCurrentPromise = nullptr; +thread_local bool UE5Coro::Private::GDestroyedEarly = false; + +bool FPromiseExtras::IsComplete() const +{ + return Completed->Wait(0, true); +} + +bool FCancellationTracker::ShouldCancel(bool bBypassHolds) const +{ + return bCanceled && (bBypassHolds || CancellationHolds == 0); +} + +FPromise::FPromise(std::shared_ptr InExtras, + const TCHAR* PromiseType) + : Extras(std::move(InExtras)) +{ +#if UE5CORO_DEBUG + Extras->DebugID = ++GLastDebugID; + Extras->DebugPromiseType = PromiseType; +#endif +} + +FPromise::~FPromise() +{ + // Expecting the lock to be taken by a derived destructor + checkf(!Extras->Lock.try_lock(), TEXT("Internal error: lock not held")); + checkf(!Extras->IsComplete(), + TEXT("Unexpected late/double coroutine destruction")); +#if PLATFORM_EXCEPTIONS_DISABLED + Extras->bWasSuccessful = !GDestroyedEarly; +#else + Extras->bWasSuccessful = !GDestroyedEarly && !bUnhandledException; +#endif + GDestroyedEarly = false; + + // The coroutine is considered completed NOW + Extras->Completed->Trigger(); + Extras->Lock.unlock(); + + for (auto& Fn : OnCompleted) + Fn(Extras->ReturnValuePtr); + Extras->ReturnValuePtr = nullptr; +} + +void FPromise::ThreadSafeDestroy() +{ + auto Handle = stdcoro::coroutine_handle::from_promise(*this); + GDestroyedEarly = IsEarlyDestroy(); + Handle.destroy(); // counts as delete this; + checkf(!GDestroyedEarly, + TEXT("Internal error: early destroy flag not reset")); +} + +FPromise& FPromise::Current() +{ + checkf(GCurrentPromise, + TEXT("This operation is only available from inside a TCoroutine")); + return *GCurrentPromise; +} + +void FPromise::Cancel() +{ + CancellationTracker.Cancel(); +} + +bool FPromise::ShouldCancel(bool bBypassHolds) const +{ + return CancellationTracker.ShouldCancel(bBypassHolds); +} + +void FPromise::HoldCancellation() +{ + CancellationTracker.Hold(); +} + +void FPromise::ReleaseCancellation() +{ + CancellationTracker.Release(); +} + +void FPromise::Resume(bool bBypassCancellationHolds) +{ + checkf(this, TEXT("Corruption")); // Still useful on some compilers + checkf(!Extras->IsComplete(), + TEXT("Attempting to resume completed coroutine")); + auto* CallerPromise = GCurrentPromise; + GCurrentPromise = this; + ON_SCOPE_EXIT + { + // Coroutine resumption might result in `this` having been freed already + checkf(GCurrentPromise == this, + TEXT("Internal error: coroutine resume tracking derailed")); + GCurrentPromise = CallerPromise; + }; + + // Self-destruct instead of resuming if a cancellation was received. + // As an exception, the latent action manager destroying the latent action + // bypasses cancellation holds. + if (UNLIKELY(ShouldCancel(bBypassCancellationHolds))) + ThreadSafeDestroy(); + else + stdcoro::coroutine_handle::from_promise(*this).resume(); +} + +void FPromise::ResumeFast() +{ + checkf(!Extras->IsComplete() && !ShouldCancel(true), + TEXT("Internal error: Fast resume preconditions not met")); + // If this is a FLatentPromise, !LF_Detached is also assumed + auto* CallerPromise = GCurrentPromise; + GCurrentPromise = this; + ON_SCOPE_EXIT { GCurrentPromise = CallerPromise; }; + stdcoro::coroutine_handle::from_promise(*this).resume(); +} + +void FPromise::AddContinuation(std::function Fn) +{ + // Expecting a non-empty function and the lock to be held by the caller + checkf(!Extras->Lock.try_lock(), TEXT("Internal error: lock not held")); + checkf(Fn, TEXT("Internal error: adding empty function as continuation")); + + OnCompleted.Add(std::move(Fn)); +} + +void FPromise::unhandled_exception() +{ +#if PLATFORM_EXCEPTIONS_DISABLED + check(!"Exceptions are not supported"); +#else + bUnhandledException = true; + throw; +#endif +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/TimerThread.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/TimerThread.cpp new file mode 100644 index 00000000..c215114c --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/TimerThread.cpp @@ -0,0 +1,113 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "TimerThread.h" +#include "UE5Coro/AsyncAwaiters.h" +#include + +using namespace UE5Coro::Private; + +std::once_flag FTimerThread::Once; +FTimerThread* FTimerThread::Instance; + +namespace +{ +// TArray auto-dereferencing requires an explicit predicate +bool Less(const FAsyncTimeAwaiter& A, const FAsyncTimeAwaiter& B) +{ + return A < B; +} +} + +FTimerThread& FTimerThread::Get() +{ + std::call_once(Once, [] { Instance = new FTimerThread; }); + return *Instance; +} + +void FTimerThread::Register(FAsyncTimeAwaiter* Awaiter) +{ + std::scoped_lock _(Lock); + Queue.HeapPush(Awaiter, &Less); + Event->Trigger(); +} + +void FTimerThread::TryUnregister(FAsyncTimeAwaiter* Awaiter) +{ + std::scoped_lock _(Lock); + // Slow, but this function is called extremely rarely + auto Idx = Queue.Find(Awaiter); + if (Idx == INDEX_NONE) + return; + Queue.HeapRemoveAt(Idx, &Less); +} + +FTimerThread::FTimerThread() + : Event(FPlatformProcess::GetSynchEventFromPool()) + , Thread(TEXT("UE5Coro Timer Thread"), [this] { Run(); }) +{ +} + +void FTimerThread::Run() +{ + for (;;) + RunOnce(); +} + +void FTimerThread::RunOnce() +{ + auto Wait = FTimespan::MaxValue(); + { + std::scoped_lock _(Lock); + if (Queue.Num() > 0) + Wait = FMath::Max(FTimespan::Zero(), + FTimespan::FromSeconds(Queue.HeapTop()->TargetTime - + FPlatformTime::Seconds())); + } + Event->Wait(Wait); + std::scoped_lock _(Lock); + auto Now = FPlatformTime::Seconds(); + while (Queue.Num() > 0) + { + if (auto& Next = Queue.HeapTop(); Next->TargetTime <= Now) + { + Resume(Next); + Queue.HeapPopDiscard(&Less); + } + else + break; + } +} + +void FTimerThread::Resume(FAsyncTimeAwaiter* Awaiter) +{ + Awaiter->Resume(); +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/TimerThread.h b/Plugins/UE5Coro/Source/UE5Coro/Private/TimerThread.h new file mode 100644 index 00000000..19729e78 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/TimerThread.h @@ -0,0 +1,63 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#include "HAL/Thread.h" +#include "UE5Coro/Private.h" + +namespace UE5Coro::Private +{ +class UE5CORO_API FTimerThread final +{ + static std::once_flag Once; + static FTimerThread* Instance; + + FEvent* Event; + FMutex Lock; + TArray Queue; + FThread Thread; // Must come last + +public: + static FTimerThread& Get(); + void Register(FAsyncTimeAwaiter*); + void TryUnregister(FAsyncTimeAwaiter*); + +private: + explicit FTimerThread(); + ~FTimerThread() = delete; + void Run(); + void RunOnce(); + void Resume(FAsyncTimeAwaiter*); +}; +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/UE5Coro.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/UE5Coro.cpp new file mode 100644 index 00000000..1d13272b --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/UE5Coro.cpp @@ -0,0 +1,39 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5Coro.h" +#include "Modules/ModuleManager.h" + +class FUE5CoroModule : public IModuleInterface +{ +}; + +IMPLEMENT_MODULE(FUE5CoroModule, UE5Coro); diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroAnimCallbackTarget.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroAnimCallbackTarget.cpp new file mode 100644 index 00000000..c007eefb --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroAnimCallbackTarget.cpp @@ -0,0 +1,201 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5CoroAnimCallbackTarget.h" +#include "UE5Coro/AnimationAwaiters.h" + +using namespace UE5Coro::Private; + +namespace +{ +using FPrivateMapPtr = TMap UAnimInstance::*; + +FPrivateMapPtr GExternalNotifyHandlersPtr; + +template +class TPrivateSpy +{ + TPrivateSpy() { GExternalNotifyHandlersPtr = Ptr; } + static TPrivateSpy Instance; // MSVC doesn't compile this as inline +}; + +template +TPrivateSpy TPrivateSpy::Instance; + +template class TPrivateSpy<&UAnimInstance::ExternalNotifyHandlers>; +} + +void UUE5CoroAnimCallbackTarget::TryResume() +{ + checkf(IsInGameThread(), + TEXT("Internal error: attempting to resume from wrong thread")); + if (Promise) // Is there anything suspended? + { + WeakInstance = nullptr; // Stop watching the instance + std::exchange(Promise, nullptr)->Resume(); // Resume exactly once + } +} + +void UUE5CoroAnimCallbackTarget::ListenForMontageEvent(UAnimInstance* Instance, + UAnimMontage* Montage, + bool bEnd) +{ + checkf(IsInGameThread(), + TEXT("Internal error: animation montage event received outside GT")); + checkf(Instance, + TEXT("Internal error: anim montage event without anim instance")); + WeakInstance = Instance; + auto Callback = FOnMontageEnded::CreateUObject( + this, &ThisClass::BoolProperty); + if (bEnd) + Instance->Montage_SetEndDelegate(Callback, Montage); + else + Instance->Montage_SetBlendingOutDelegate(Callback, Montage); +} + +void UUE5CoroAnimCallbackTarget::ListenForNotify(UAnimInstance* Instance, + UAnimMontage* Montage, + FName NotifyName) +{ + checkf(IsInGameThread(), + TEXT("Internal error: notify event received outside GT")); + checkf(Instance, + TEXT("Internal error: anim montage event without anim instance")); + checkf(GExternalNotifyHandlersPtr, + TEXT("Internal error: anim instance spy failed")); + WeakInstance = Instance; + + // UAnimInstance::AddExternalNotifyHandler() ties the notify name and the + // called UFUNCTION's name together. :( + auto& ExternalNotifyHandlers = Instance->*GExternalNotifyHandlersPtr; + FName HandlerName = *(TEXT("AnimNotify_") + NotifyName.ToString()); + auto& Delegate = ExternalNotifyHandlers.FindOrAdd(HandlerName); + Delegate.AddUObject(this, &ThisClass::Core); +} + +void UUE5CoroAnimCallbackTarget::ListenForPlayMontageNotify( + UAnimInstance* Instance, UAnimMontage* Montage, + std::optional NotifyName, bool bEnd) +{ + checkf(IsInGameThread(), + TEXT("Internal error: play montage event received outside GT")); + checkf(Instance, + TEXT("Internal error: play montage event without anim instance")); + checkf(MontageIDFilter == INDEX_NONE && !NotifyFilter.has_value(), + TEXT("Internal error: montage filter already set up")); + WeakInstance = Instance; + + if (auto* MontageInstance = Instance->GetActiveInstanceForMontage(Montage)) + MontageIDFilter = MontageInstance->GetInstanceID(); + NotifyFilter = NotifyName; + + (bEnd ? Instance->OnPlayMontageNotifyEnd + : Instance->OnPlayMontageNotifyBegin) + .AddDynamic(this, &ThisClass::NameProperty); +} + +void UUE5CoroAnimCallbackTarget::RequestResume(FPromise& InPromise) +{ + checkf(!Promise, TEXT("Attempted second concurrent co_await")); + // await_ready should've prevented suspension if there's already a result + checkf(IsInGameThread(), TEXT("Internal error: suspending on wrong thread")); + checkf(std::holds_alternative(Result), + TEXT("Internal error: reused callback target")); + Promise = &InPromise; +} + +void UUE5CoroAnimCallbackTarget::CancelResume() +{ + checkf(IsInGameThread(), TEXT("Internal error: canceling on wrong thread")); + // Promise can be nullptr already if this is a deferred destruction + Promise = nullptr; +} + +void UUE5CoroAnimCallbackTarget::Core() +{ + checkf(IsInGameThread(), + TEXT("Internal error: expected notify callback on game thread")); + + Result = true; // This is for the void awaiter + TryResume(); +} + +void UUE5CoroAnimCallbackTarget::BoolProperty(UAnimMontage*, bool bInterrupted) +{ + checkf(IsInGameThread(), + TEXT("Internal error: expected montage callback on game thread")); + + Result = bInterrupted; + TryResume(); +} + +void UUE5CoroAnimCallbackTarget::NameProperty( + FName NotifyName, const FBranchingPointNotifyPayload& Payload) +{ + checkf(IsInGameThread(), + TEXT("Internal error: expected montage callback on game thread")); + + // Apply filters + if (MontageIDFilter != INDEX_NONE && + Payload.MontageInstanceID != MontageIDFilter) + return; + if (NotifyFilter.has_value() && *NotifyFilter != NotifyName) + return; + + // This callback passed all filters. Store the result, then resume. + if (NotifyFilter.has_value()) + Result = &Payload; + else + Result = FPayloadTuple(NotifyName, &Payload); + TryResume(); +} + +ETickableTickType UUE5CoroAnimCallbackTarget::GetTickableTickType() const +{ + return IsTemplate() ? ETickableTickType::Never : ETickableTickType::Always; +} + +void UUE5CoroAnimCallbackTarget::Tick(float DeltaTime) +{ + // Keep trying to resume on tick, because it's possible that an animation + // awaiter exists but it will only get co_awaited in the future. + // A successful resume will explicitly null WeakInstance. + // TAnimAwaiter is dealing with unexpected/early resumptions from this + // code path, mainly by checking if Result is still std::monostate. + if (WeakInstance.IsStale()) + TryResume(); +} + +TStatId UUE5CoroAnimCallbackTarget::GetStatId() const +{ + RETURN_QUICK_DECLARE_CYCLE_STAT(UUE5CoroAnimCallbackTarget, + STATGROUP_Tickables); +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroAnimCallbackTarget.h b/Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroAnimCallbackTarget.h new file mode 100644 index 00000000..f0fcd56c --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroAnimCallbackTarget.h @@ -0,0 +1,86 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#include +#include +#include "UE5Coro/AsyncCoroutine.h" +#include "UE5CoroAnimCallbackTarget.generated.h" + +UCLASS() +class UE5CORO_API UUE5CoroAnimCallbackTarget : public UObject, + public FTickableGameObject +{ + GENERATED_BODY() + + TWeakObjectPtr WeakInstance; + UE5Coro::Private::FPromise* Promise = nullptr; + // UPlayMontageCallbackProxy uses this value as the default + int32 MontageIDFilter = INDEX_NONE; + std::optional NotifyFilter; // "None" is a valid name for a notify + + void TryResume(); + +public: + // Void's result is indicated by this holding a bool, not monostate + std::variant> Result; + + void ListenForMontageEvent(UAnimInstance*, UAnimMontage*, bool); + void ListenForNotify(UAnimInstance*, UAnimMontage*, FName); + void ListenForPlayMontageNotify(UAnimInstance*, UAnimMontage*, + std::optional, bool); + void RequestResume(UE5Coro::Private::FPromise&); + void CancelResume(); + +#pragma region Callbacks + // These function names are chosen to match predefined FNames + UFUNCTION() + void Core(); // void + UFUNCTION() + void BoolProperty(UAnimMontage* Montage, bool bInterrupted); + UFUNCTION() + void NameProperty(FName NotifyName, const FBranchingPointNotifyPayload& Payload); +#pragma endregion + +#pragma region FTickableGameObject overrides + // These are needed to catch the anim instance getting destroyed without + // a callback. Editor tick is needed to handle Persona and the end of PIE. + virtual ETickableTickType GetTickableTickType() const override; + virtual bool IsTickableInEditor() const override { return true; } + virtual bool IsTickableWhenPaused() const override { return true; } + virtual void Tick(float DeltaTime) override; + virtual TStatId GetStatId() const override; +#pragma endregion +}; diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroChainCallbackTarget.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroChainCallbackTarget.cpp new file mode 100644 index 00000000..e56e24a8 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroChainCallbackTarget.cpp @@ -0,0 +1,102 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5CoroChainCallbackTarget.h" +#include "UE5Coro/UE5CoroSubsystem.h" + +using namespace UE5Coro::Private; + +void UUE5CoroChainCallbackTarget::Activate(int32 InExpectedLink, + FTwoLives* InState) +{ + check(IsInGameThread()); + checkf(!State, TEXT("Unexpected double activation")); + ExpectedLink = InExpectedLink; + State = InState; +} + +void UUE5CoroChainCallbackTarget::Deactivate() +{ + check(IsInGameThread()); + checkf(State, TEXT("Unexpected deactivation while not active")); + // Leave ExpectedLink stale for the check in Core() + if (!State->Release()) + State = nullptr; // The other side is not interested anymore +} + +int32 UUE5CoroChainCallbackTarget::GetExpectedLink() const +{ + check(IsInGameThread()); + checkf(State, TEXT("Unexpected linkage query on inactive object")); + return ExpectedLink; +} + +void UUE5CoroChainCallbackTarget::Core(int32 Link) +{ + check(IsInGameThread()); + checkf(Link == ExpectedLink, TEXT("Unexpected linkage")); + if (State) + { + State->UserData = 1; + State = nullptr; + } +} + +ETickableTickType UUE5CoroChainCallbackTarget::GetTickableTickType() const +{ + return IsTemplate() ? ETickableTickType::Never : ETickableTickType::Always; +} + +void UUE5CoroChainCallbackTarget::Tick(float DeltaTime) +{ + if (!State) + return; + + // ProcessLatentActions refuses to work on non-BP classes. + GetClass()->ClassFlags |= CLASS_CompiledFromBlueprint; + GetWorld()->GetLatentActionManager().ProcessLatentActions(this, DeltaTime); + GetClass()->ClassFlags &= ~CLASS_CompiledFromBlueprint; +} + +TStatId UUE5CoroChainCallbackTarget::GetStatId() const +{ + RETURN_QUICK_DECLARE_CYCLE_STAT(UUE5CoroChainCallbackTarget, + STATGROUP_Tickables); +} + +// This is only used by tests +namespace UE5Coro::Private +{ +UE5CORO_API UClass* ChainCallbackTarget_StaticClass() +{ + return UUE5CoroChainCallbackTarget::StaticClass(); +} +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroChainCallbackTarget.h b/Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroChainCallbackTarget.h new file mode 100644 index 00000000..6b01800e --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroChainCallbackTarget.h @@ -0,0 +1,68 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#include "UE5CoroChainCallbackTarget.generated.h" + +namespace UE5Coro::Private +{ +class FTwoLives; +} + +UCLASS(Within = UE5CoroSubsystem) +class UE5CORO_API UUE5CoroChainCallbackTarget : public UObject, + public FTickableGameObject +{ + GENERATED_BODY() + + int32 ExpectedLink = 0; + UE5Coro::Private::FTwoLives* State = nullptr; + +public: + void Activate(int32 InExpectedLink, UE5Coro::Private::FTwoLives* InState); + void Deactivate(); + [[nodiscard]] int32 GetExpectedLink() const; + + /** Signals the coroutine suspended with this linkage that it may resume. */ + UFUNCTION() + void Core(int32 Link); + +#pragma region FTickableGameObject overrides + virtual ETickableTickType GetTickableTickType() const override; + virtual bool IsTickableWhenPaused() const override { return true; } + virtual bool IsTickableInEditor() const override { return true; } + virtual void Tick(float DeltaTime) override; + virtual TStatId GetStatId() const override; +#pragma endregion +}; diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroDelegateCallbackTarget.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroDelegateCallbackTarget.cpp new file mode 100644 index 00000000..e6df609a --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroDelegateCallbackTarget.cpp @@ -0,0 +1,50 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5CoroDelegateCallbackTarget.h" + +void UUE5CoroDelegateCallbackTarget::Init(std::function InFn) +{ + Fn = std::move(InFn); +} + +void UUE5CoroDelegateCallbackTarget::ProcessEvent(UFunction*, void* Parms) +{ + // This might also be caused by a multithreaded race condition + checkf(Fn, TEXT("Internal error: Unexpected early or double callback")); + std::exchange(Fn, nullptr)(Parms); + MarkPendingKill(); // Prevent further calls from dynamic delegates +} + +void UUE5CoroDelegateCallbackTarget::Core() +{ + check(!"Internal error: This function should never run"); +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroDelegateCallbackTarget.h b/Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroDelegateCallbackTarget.h new file mode 100644 index 00000000..096295a6 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroDelegateCallbackTarget.h @@ -0,0 +1,52 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#include "UE5Coro/AsyncAwaiters.h" +#include "UE5CoroDelegateCallbackTarget.generated.h" + +UCLASS() +class UUE5CoroDelegateCallbackTarget : public UObject +{ + GENERATED_BODY() + + std::function Fn; + +public: + void Init(std::function); + virtual void ProcessEvent(UFunction*, void*) override; + + UFUNCTION() + void Core(); +}; diff --git a/Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroSubsystem.cpp b/Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroSubsystem.cpp new file mode 100644 index 00000000..3146e53b --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Private/UE5CoroSubsystem.cpp @@ -0,0 +1,130 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5Coro/UE5CoroSubsystem.h" +#include "UE5CoroChainCallbackTarget.h" + +using namespace UE5Coro::Private; + +bool FTwoLives::Release() +{ + // The <= 2 part should help catch use-after-free bugs in full debug builds. + checkf(RefCount > 0 && RefCount <= 2, + TEXT("Internal error: misused two-lives tracker")); + if (--RefCount == 0) + { + delete this; + return false; + } + return true; +} + +bool FTwoLives::ShouldResume(void* State, bool bCleanup) +{ + auto* This = static_cast(State); + if (UNLIKELY(bCleanup)) + { + This->Release(); + return false; + } + return This->RefCount < 2; +} + +FLatentActionInfo UUE5CoroSubsystem::MakeLatentInfo() +{ + checkf(IsInGameThread(), TEXT("Unexpected latent info off the game thread")); + // Using INDEX_NONE linkage and next as the UUID is marginally faster due + // to an early exit in FLatentActionManager::TickLatentActionForObject. + return {INDEX_NONE, NextLinkage++, TEXT("None"), this}; +} + +FLatentActionInfo UUE5CoroSubsystem::MakeLatentInfo(FTwoLives* State) +{ + checkf(IsInGameThread(), TEXT("Unexpected latent info off the game thread")); + + // Lazy delegate binding in order to not affect + // projects that never use Chain/ChainEx. + if (UNLIKELY(!LatentActionsChangedHandle.IsValid())) + LatentActionsChangedHandle = + FLatentActionManager::OnLatentActionsChanged().AddUObject( + this, &ThisClass::LatentActionsChanged); + + int32 Linkage = NextLinkage++; + checkf(!ChainCallbackTargets.Contains(Linkage), + TEXT("Unexpected linkage collision")); + // Pooling these objects was found to be consistently slower + // than making new ones every time. + auto* Target = NewObject(this); + Target->Activate(Linkage, State); + ChainCallbackTargets.Add(Linkage, Target); + return {Linkage, Linkage, TEXT("Core"), Target}; +} + +void UUE5CoroSubsystem::Deinitialize() +{ + Super::Deinitialize(); + + if (LatentActionsChangedHandle.IsValid()) + FLatentActionManager::OnLatentActionsChanged().Remove( + LatentActionsChangedHandle); +} + +void UUE5CoroSubsystem::Tick(float DeltaTime) +{ + Super::Tick(DeltaTime); + + // ProcessLatentActions refuses to work on non-BP classes. + GetClass()->ClassFlags |= CLASS_CompiledFromBlueprint; + GetWorld()->GetLatentActionManager().ProcessLatentActions(this, DeltaTime); + GetClass()->ClassFlags &= ~CLASS_CompiledFromBlueprint; +} + +TStatId UUE5CoroSubsystem::GetStatId() const +{ + RETURN_QUICK_DECLARE_CYCLE_STAT(UUE5CoroSubsystem, STATGROUP_Tickables); +} + +void UUE5CoroSubsystem::LatentActionsChanged(UObject* Object, + ELatentActionChangeType Change) +{ + checkf(IsInGameThread(), + TEXT("Unexpected latent action update off the game thread")); + + if (Change != ELatentActionChangeType::ActionsRemoved) + return; + + if (auto* Target = Cast(Object); + IsValid(Target) && Target->GetOuter() == this) + { + verify(ChainCallbackTargets.Remove(Target->GetExpectedLink()) == 1); + Target->Deactivate(); + } +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro.h b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro.h new file mode 100644 index 00000000..dc799c11 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro.h @@ -0,0 +1,47 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#include "UE5Coro/AggregateAwaiters.h" +#include "UE5Coro/AnimationAwaiters.h" +#include "UE5Coro/AsyncAwaiters.h" +#include "UE5Coro/Cancellation.h" +#include "UE5Coro/Coroutine.h" +#include "UE5Coro/CoroutineAwaiters.h" +#include "UE5Coro/Generator.h" +#include "UE5Coro/HttpAwaiters.h" +#include "UE5Coro/LatentAwaiters.h" +#include "UE5Coro/LatentCallbacks.h" +#include "UE5Coro/LatentTimeline.h" +#include "UE5Coro/Threading.h" diff --git a/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro.natvis b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro.natvis new file mode 100644 index 00000000..5bb375c2 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro.natvis @@ -0,0 +1,21 @@ + + + + + FPromise {*Extras._Ptr} + + *Extras._Ptr + + + + {{{DebugPromiseType,sub}, ID={DebugID}, Name={DebugName,sub}}} + + DebugPromiseType,sub + DebugID + DebugName,su + bWasSuccessful + Lock.bFlag + Promise + + + diff --git a/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/AggregateAwaiters.h b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/AggregateAwaiters.h new file mode 100644 index 00000000..e16e438e --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/AggregateAwaiters.h @@ -0,0 +1,232 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#include +#include "Misc/ScopeExit.h" +#include "UE5Coro/AsyncCoroutine.h" +#include "UE5Coro/CoroutineAwaiters.h" +#include "UE5Coro/Private.h" + +namespace UE5Coro::Private +{ +class FAnyAwaiter; +class FAllAwaiter; +class FRaceAwaiter; + +#if UE5CORO_CPP20 +// If your WhenAny/WhenAll call doesn't satisfy this concept, you'll need to +// move the affected parameter into the function call with MoveTemp/std::move/etc. +template +concept TAggregateAwaitable = + TAwaitable && std::is_constructible_v, T&&>; +#endif +} + +#if UE5CORO_CPP20 + #define UE5CORO_PRIVATE_AWAITABLE UE5Coro::Private::TAggregateAwaitable +#else + #define UE5CORO_PRIVATE_AWAITABLE typename +#endif + +namespace UE5Coro +{ +/** co_awaits all parameters, resumes its own awaiting coroutine when the first + * one of them finishes.
+ * The result of the co_await expression is the index of the parameter that + * finished first. */ +template +Private::FAnyAwaiter WhenAny(T&&...); + +#if UE5CORO_CPP20 +/** Resumes the awaiting coroutine when all other coroutines have completed.
+ * The result of the co_await expression is the index of the parameter that + * finished first. */ +UE5CORO_API Private::FAnyAwaiter WhenAny(const TArray>&); +#endif + +/** co_awaits all coroutines in the array. + * The first one to finish cancels the others and resumes the caller. + * The result of the co_await expression is the array index of the coroutine + * that finished first. */ +UE5CORO_API Private::FRaceAwaiter Race(TArray>); + +/** co_awaits all of the coroutines provided. + * The first one to finish cancels the others and resumes the caller. + * The result of the co_await expression is the index of the parameter that + * finished first. */ +template +Private::FRaceAwaiter Race(TCoroutine... Args); + +/** co_awaits all parameters, resumes its own awaiting coroutine when all + * of them finish. */ +template +Private::FAllAwaiter WhenAll(T&&...); + +#if UE5CORO_CPP20 +/** Resumes the awaiting coroutine when all other coroutines have completed. */ +UE5CORO_API Private::FAllAwaiter WhenAll(const TArray>&); +#endif +} + +namespace UE5Coro::Private +{ +class [[nodiscard]] UE5CORO_API FAggregateAwaiter + : public TAwaiter +{ + struct FData + { + FMutex Lock; + int Count; + int Index = -1; + FPromise* Promise = nullptr; + + explicit FData(int Count) : Count(Count) { } + }; + + std::shared_ptr Data; + + template + static TCoroutine<> Consume(std::shared_ptr, int, T&&); + +protected: + int GetResumerIndex() const; + +public: + template + explicit FAggregateAwaiter(int Count, T&&... Awaiters) + : Data(std::make_shared(Count)) + { + int Idx = 0; + (Consume(Data, Idx++, std::forward(Awaiters)), ...); + } + + template + explicit FAggregateAwaiter(T, const TArray>& Coroutines); + + bool await_ready(); + void Suspend(FPromise&); +}; + +class [[nodiscard]] FAnyAwaiter : public FAggregateAwaiter +{ +public: + template + explicit FAnyAwaiter(T&&... Args) + : FAggregateAwaiter(std::forward(Args)...) { } + int await_resume() { return GetResumerIndex(); } +}; + +class [[nodiscard]] FAllAwaiter : public FAggregateAwaiter +{ +public: + template + explicit FAllAwaiter(T&&... Args) + : FAggregateAwaiter(std::forward(Args)...) { } + void await_resume() noexcept { } +}; + +class [[nodiscard]] UE5CORO_API FRaceAwaiter : public TAwaiter +{ + struct FData + { + FMutex Lock; + TArray> Handles; + int Index = -1; + FPromise* Promise = nullptr; + + explicit FData(TArray>&& Array) + : Handles(std::move(Array)) { } + }; + std::shared_ptr Data; + +public: + explicit FRaceAwaiter(TArray>&&); + bool await_ready(); + void Suspend(FPromise&); + int await_resume() noexcept; +}; +} + +template +UE5Coro::Private::FAnyAwaiter UE5Coro::WhenAny(T&&... Args) +{ + static_assert( + (... && std::is_constructible_v, T&&>), + "Attempted to copy a noncopyable awaiter, move it instead"); + return Private::FAnyAwaiter(sizeof...(Args) ? 1 : 0, + std::forward(Args)...); +} + +template +UE5Coro::Private::FRaceAwaiter UE5Coro::Race(TCoroutine... Args) +{ + return Race(TArray>{std::move(Args)...}); +} + +template +UE5Coro::Private::FAllAwaiter UE5Coro::WhenAll(T&&... Args) +{ + static_assert( + (... && std::is_constructible_v, T&&>), + "Attempted to copy a noncopyable awaiter, move it instead"); + return Private::FAllAwaiter(sizeof...(Args), std::forward(Args)...); +} + +template +UE5Coro::TCoroutine<> UE5Coro::Private::FAggregateAwaiter::Consume( + std::shared_ptr Data, int Index, T&& Awaiter) +{ + auto AwaiterCopy = std::forward(Awaiter); // If this line doesn't compile, + // you'll need to fix your usage of WhenAny/WhenAll and move the affected + // noncopyable parameter into the call with MoveTemp/std::move/etc. + + ON_SCOPE_EXIT + { + std::unique_lock _(Data->Lock); + if (--Data->Count != 0) + return; + Data->Index = Index; // Mark that this index was the one reaching 0 + auto* Promise = Data->Promise; + _.unlock(); + + // Not co_awaited yet if this is nullptr, await_ready deals with this + if (Promise != nullptr) + Promise->Resume(); + }; + + co_await std::move(AwaiterCopy); +} + +#undef UE5CORO_PRIVATE_AWAITABLE diff --git a/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/AnimationAwaiters.h b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/AnimationAwaiters.h new file mode 100644 index 00000000..d901be86 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/AnimationAwaiters.h @@ -0,0 +1,191 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#include +#include "Animation/AnimNotifies/AnimNotify.h" +#include "UE5Coro/AsyncCoroutine.h" + +class UUE5CoroAnimCallbackTarget; + +namespace UE5Coro::Private +{ +using FPayloadPtr = const FBranchingPointNotifyPayload*; +using FPayloadTuple = TTuple; +template class TAnimAwaiter; +using FAnimAwaiterVoid = TAnimAwaiter; +using FAnimAwaiterBool = TAnimAwaiter; +using FAnimAwaiterPayload = TAnimAwaiter; +using FAnimAwaiterTuple = TAnimAwaiter; +} + +namespace UE5Coro::Anim +{ +/** Waits for the provided montage's current instance to blend out on the given + * anim instance.
+ * The result of the co_await expression is true if this was caused by an + * interruption, false otherwise.
+ * The return value of this function is copyable but only one copy may be + * co_awaited at the same time.
+ * The anim instance getting destroyed early counts as an interruption. + * Use IsValid(Instance) to handle this separately, if desired. + * @see FOnMontageBlendingOutStarted */ +UE5CORO_API Private::FAnimAwaiterBool MontageBlendingOut(UAnimInstance* Instance, + UAnimMontage* Montage); + +/** Waits for the provided montage's current instance to end on the given + * anim instance.
+ * The result of the co_await expression is true if this was caused by an + * interruption, false otherwise.
+ * The return value of this function is copyable but only one copy may be + * co_awaited at the same time.
+ * The anim instance getting destroyed early counts as an interruption. + * Use IsValid(Instance) to handle this separately, if desired. + * @see FOnMontageEnded */ +UE5CORO_API Private::FAnimAwaiterBool MontageEnded(UAnimInstance* Instance, + UAnimMontage* Montage); + +/** Waits for the anim notify to happen on the provided anim instance.
+ * The anim instance getting destroyed early counts as the notify having + * happened.
+ * The return value of this function is copyable but only one copy may be + * co_awaited at the same time.
+ * Use IsValid(Instance) after the co_await to detect this, if desired. */ +UE5CORO_API Private::FAnimAwaiterVoid NextNotify(UAnimInstance* Instance, + FName NotifyName); + +/** Waits for any PlayMontageNotify or PlayMontageNotifyWindow to begin on the + * montage's currently-playing instance.
+ * The result of co_await is a TTuple of the name of the montage that began + * along with a pointer to its notify payload.
+ * The return value of this function is copyable but only one copy may be + * co_awaited at the same time.
+ * The payload pointer is only valid until the next co_await, and it might be + * nullptr in case the notify happened before co_awaiting the returned value of + * this function or (rarely) if the anim instance got destroyed. + * Use IsValid(AnimInstance) if you need to handle this separately. + * @see FPlayMontageAnimNotifyDelegate */ +UE5CORO_API auto PlayMontageNotifyBegin(UAnimInstance* Instance, + UAnimMontage* Montage) + -> Private::FAnimAwaiterTuple; + +/** Waits for any PlayMontageNotify or PlayMontageNotifyWindow to end on the + * montage's currently-playing instance.
+ * The result of co_await is a TTuple of the name of the montage that ended + * along with a pointer to its notify payload.
+ * The return value of this function is copyable but only one copy may be + * co_awaited at the same time.
+ * The payload pointer is only valid until the next co_await, and it might be + * nullptr in case the notify happened before co_awaiting the returned value of + * this function or (rarely) if the anim instance got destroyed. + * Use IsValid(AnimInstance) if you need to handle this separately. + * @see FPlayMontageAnimNotifyDelegate */ +UE5CORO_API auto PlayMontageNotifyEnd(UAnimInstance* Instance, + UAnimMontage* Montage) + -> Private::FAnimAwaiterTuple; + +/** Waits for the PlayMontageNotify or PlayMontageNotifyWindow of the given name + * to begin on the montage's currently-playing instance.
+ * The result of co_await is a pointer to the notify payload.
+ * The return value of this function is copyable but only one copy may be + * co_awaited at the same time.
+ * The payload pointer is only valid until the next co_await, and it might be + * nullptr in case the notify happened before co_awaiting the returned value of + * this function or (rarely) if the anim instance got destroyed. + * Use IsValid(AnimInstance) if you need to handle this separately. + * @see FPlayMontageAnimNotifyDelegate */ +UE5CORO_API auto PlayMontageNotifyBegin(UAnimInstance* Instance, + UAnimMontage* Montage, FName NotifyName) + -> Private::FAnimAwaiterPayload; + +/** Waits for the PlayMontageNotify or PlayMontageNotifyWindow of the given name + * to end on the montage's currently-playing instance.
+ * The result of co_await is a pointer to the notify payload.
+ * The return value of this function is copyable but only one copy may be + * co_awaited at the same time.
+ * The payload pointer is only valid until the next co_await, and it might be + * nullptr in case the notify happened before co_awaiting the returned value of + * this function or (rarely) if the anim instance got destroyed. + * Use IsValid(AnimInstance) if you need to handle this separately. + * @see FPlayMontageAnimNotifyDelegate */ +UE5CORO_API auto PlayMontageNotifyEnd(UAnimInstance* Instance, + UAnimMontage* Montage, FName NotifyName) + -> Private::FAnimAwaiterPayload; +} + +namespace UE5Coro::Private +{ +class [[nodiscard]] FAnimAwaiter : public TAwaiter +{ +protected: + TStrongObjectPtr Target; + bool bSuspended = false; + + FAnimAwaiter(UAnimInstance*, UAnimMontage*); + ~FAnimAwaiter(); + + UE5CORO_API FAnimAwaiter(const FAnimAwaiter&); + UE5CORO_API FAnimAwaiter& operator=(const FAnimAwaiter&); + +public: + UE5CORO_API void Suspend(FPromise&); +}; + +template +class [[nodiscard]] TAnimAwaiter : public FAnimAwaiter +{ + friend UUE5CoroAnimCallbackTarget; + + static constexpr enum + { + Void, + Bool, + Payload, + NameAndPayload, + } Type = std::is_same_v ? Void + : std::is_same_v ? Bool + : std::is_pointer_v ? Payload + : NameAndPayload; + +public: + template + TAnimAwaiter(TEnd, UAnimInstance*, UAnimMontage*); + template + TAnimAwaiter(TEnd, UAnimInstance*, UAnimMontage*, FName); + UE5CORO_API ~TAnimAwaiter(); + + UE5CORO_API bool await_ready(); + UE5CORO_API std::conditional_t await_resume(); +}; +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/AsyncAwaiters.h b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/AsyncAwaiters.h new file mode 100644 index 00000000..a7899129 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/AsyncAwaiters.h @@ -0,0 +1,430 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#include +#include +#include "Async/TaskGraphInterfaces.h" +#include "UE5Coro/AsyncCoroutine.h" +#include "UE5Coro/Private.h" + +namespace UE5Coro::Private +{ +class FAsyncAwaiter; +class FAsyncPromise; +class FAsyncTimeAwaiter; +class FAsyncYieldAwaiter; +class FLatentPromise; +class FNewThreadAwaiter; +template class TDelegateAwaiter; +template class TDynamicDelegateAwaiter; +} + +namespace UE5Coro::Async +{ +/** Suspends the coroutine and resumes it on the provided named thread, if it's + * not already on that thread. If it is, nothing happens.
+ * The return value of this function is reusable. Repeated co_awaits will keep + * moving back into the provided thread. */ +UE5CORO_API Private::FAsyncAwaiter MoveToThread(ENamedThreads::Type); + +/** Convenience function to resume on the game thread.
+ * Equivalent to calling Async::MoveToThread(ENamedThreads::GameThread).
+ * As such, its return value is reusable and will keep co_awaiting back into + * the game thread. */ +UE5CORO_API Private::FAsyncAwaiter MoveToGameThread(); + +/** Convenience function to resume on the same kind of named thread that this + * function was called on.
+ * co_await MoveToSimilarThread() is not useful. The return value should be + * stored to "remember" the original thread, then co_awaited later. */ +UE5CORO_API Private::FAsyncAwaiter MoveToSimilarThread(); + +/** Always suspends the coroutine and resumes it on the same kind of named + * thread that it's currently running on, or AnyThread otherwise.
+ * The return value of this function is reusable and always refers to the + * current thread, even if the coroutine has moved threads since this function + * was called. */ +UE5CORO_API Private::FAsyncYieldAwaiter Yield(); + +/** Starts a new thread with additional control over priority, affinity, etc. + * and resumes the coroutine there.
+ * Intended for long-running operations before the next co_await or co_return. + * For parameters see the engine function FRunnableThread::Create().
+ * The return value of this function is reusable. Every co_await will start a + * new thread. */ +UE5CORO_API Private::FNewThreadAwaiter MoveToNewThread( + EThreadPriority Priority = TPri_Normal, + uint64 Affinity = FPlatformAffinity::GetNoAffinityMask(), + EThreadCreateFlags Flags = EThreadCreateFlags::None); + +/** Resumes the coroutine after the specified amount of time has elapsed, based + * on FPlatformTime.
+ * The coroutine will resume on the same kind of named thread as it was running + * on when it was suspended. */ +UE5CORO_API Private::FAsyncTimeAwaiter PlatformSeconds(double Seconds); + +/** Resumes the coroutine after the specified amount of time has elapsed, based + * on FPlatformTime.
+ * The coroutine will resume on an unspecified worker thread. */ +UE5CORO_API Private::FAsyncTimeAwaiter PlatformSecondsAnyThread(double Seconds); + +/** Resumes the coroutine after FPlatformTime::Seconds has reached the specified + * amount.
+ * The coroutine will resume on the same kind of named thread as it was running + * on when it was suspended. */ +UE5CORO_API Private::FAsyncTimeAwaiter UntilPlatformTime(double Time); + +/** Resumes the coroutine after FPlatformTime::Seconds has reached the specified + * amount.
+ * The coroutine will resume on an unspecified worker thread. */ +UE5CORO_API Private::FAsyncTimeAwaiter UntilPlatformTimeAnyThread(double Time); +} + +namespace UE5Coro::Private +{ +// Bits used to identify a kind of thread, without the scheduling flags +constexpr auto ThreadTypeMask = ENamedThreads::ThreadIndexMask | + ENamedThreads::ThreadPriorityMask; + +class [[nodiscard]] UE5CORO_API FAsyncAwaiter : public TAwaiter +{ + ENamedThreads::Type Thread; + +public: + explicit FAsyncAwaiter(ENamedThreads::Type Thread) + : Thread(Thread) { } + + bool await_ready(); + void Suspend(FPromise&); +}; + +class [[nodiscard]] UE5CORO_API FAsyncTimeAwaiter + : public TAwaiter +{ + friend class FTimerThread; + + const double TargetTime; + union FState + { + bool bAnyThread; // Before suspension + ENamedThreads::Type Thread; // After suspension + explicit FState(bool bAnyThread) : bAnyThread(bAnyThread) { } + } U; + std::atomic Promise = nullptr; + +public: + explicit FAsyncTimeAwaiter(double TargetTime, bool bAnyThread) + : TargetTime(TargetTime), U(bAnyThread) { } + FAsyncTimeAwaiter(const FAsyncTimeAwaiter&); + ~FAsyncTimeAwaiter(); + + bool await_ready(); + void Suspend(FPromise&); + + bool operator<(const FAsyncTimeAwaiter& Other) const noexcept + { + return TargetTime < Other.TargetTime; + } + +private: + void Resume(); +}; + +template +class [[nodiscard]] TFutureAwaiter final : public TAwaiter> +{ + TFuture Future; + std::remove_reference_t* Result = nullptr; // Dangerous! + +public: + explicit TFutureAwaiter(TFuture&& Future) : Future(std::move(Future)) { } + UE_NONCOPYABLE(TFutureAwaiter); + + bool await_ready() + { + checkf(!Result, TEXT("Attempting to reuse spent TFutureAwaiter")); + checkf(Future.IsValid(), + TEXT("Awaiting invalid/spent future will never resume")); + return Future.IsReady(); + } + + void Suspend(FPromise& Promise) + { + // Extremely rarely, Then will run synchronously because Future + // finished after IsReady but before Suspend. + // This is OK and will result in the caller coroutine resuming itself. + + Future.Then([this, &Promise](auto InFuture) + { + checkf(!Future.IsValid(), + TEXT("Internal error: future was not consumed")); + + if constexpr (std::is_lvalue_reference_v) + { + // The type of Get() is T* in 5.3 and T*& in 5.4 + static_assert(std::is_pointer_v< + std::remove_reference_t>); + Result = InFuture.Get(); + Promise.Resume(); + } + else + { + // If T is void, Get() returns an int (5.3) or int& (5.4), which + // is harmless to process; await_resume will ignore it. + + // It's normally dangerous to expose a pointer to a local, but + auto Value = InFuture.Get(); // This will be alive while... + Result = &Value; + Promise.Resume(); // ...await_resume moves from it here + } + }); + } + + T await_resume() + { + if (!Result) + { + // Result being nullptr indicates that await_ready returned true, + // Then has not and will not run, and Future is still valid + checkf(Future.IsValid(), TEXT("Internal error: future was consumed")); + Result = reinterpret_cast(-1); // Mark as spent + return Future.Get(); + } + else + { + // Otherwise, we're being called from Then, and Future is spent + checkf(!Future.IsValid(), + TEXT("Internal error: future was not consumed")); + if constexpr (std::is_lvalue_reference_v) + return *Result; + else if constexpr (!std::is_void_v) + return std::move(*Result); // This will move from Then's local + } + } +}; + +template +struct TAwaitTransform> +{ + TFutureAwaiter operator()(TFuture&& Future) + { + return TFutureAwaiter(std::move(Future)); + } + + // co_awaiting a TFuture consumes it, use MoveTemp/std::move + TFutureAwaiter operator()(TFuture&) = delete; +}; + +template +struct TDelegateAwaiterFor; +template +struct TDelegateAwaiterFor +{ + using type = std::conditional_t, + TDynamicDelegateAwaiter, + TDelegateAwaiter>; +}; + +template +struct TAwaitTransform>> +{ + static constexpr auto ExecutePtr(T) + { + if constexpr (TIsSparseDelegate) + { + using Ptr = decltype(std::declval().GetShared().Get()); + return &std::remove_pointer_t::Broadcast; + } + else if constexpr (TIsMulticastDelegate) + return &T::Broadcast; + else + return &T::Execute; + } + using FExecutePtr = decltype(ExecutePtr(std::declval())); + using FAwaiter = typename TDelegateAwaiterFor::type; + + FAwaiter operator()(T& Delegate) { return FAwaiter(Delegate); } + + // The delegate needs to live longer than the awaiter. Use lvalues only. + FAwaiter operator()(T&& Delegate) = delete; +}; + +class [[nodiscard]] UE5CORO_API FNewThreadAwaiter + : public TAwaiter +{ + EThreadPriority Priority; + uint64 Affinity; + EThreadCreateFlags Flags; + +public: + explicit FNewThreadAwaiter(EThreadPriority Priority, uint64 Affinity, + EThreadCreateFlags Flags) + : Priority(Priority), Affinity(Affinity), Flags(Flags) { } + + void Suspend(FPromise&); +}; + +// Stores references as values. Structured bindings will give references. +// Used with DYNAMIC delegates. +// The memory layout has to match TBaseUFunctionDelegateInstance::Execute! +template +class TDecayedPayload : private TPayload...)> +{ + template + using TType = std::tuple_element_t>; + +public: + // Objects of this type are never made, it's only used to reinterpret params + TDecayedPayload() = delete; + + template + TType& get() { return this->Values.template Get(); } +}; + +class [[nodiscard]] UE5CORO_API FDelegateAwaiter + : public TAwaiter +{ +protected: + FPromise* Promise = nullptr; + std::function Cleanup; + void TryResumeOnce(); + UObject* SetupCallbackTarget(std::function); + +public: + ~FDelegateAwaiter(); + void Suspend(FPromise& InPromise); +}; + +template +class [[nodiscard]] TDelegateAwaiter : public FDelegateAwaiter +{ + using ThisClass = TDelegateAwaiter; + using FResult = std::conditional_t, void>; + TTuple* Result = nullptr; + +public: + template + explicit TDelegateAwaiter(T& Delegate) + { + static_assert(!TIsDynamicDelegate); + if constexpr (TIsMulticastDelegate) + { + auto Handle = Delegate.AddRaw(this, &ThisClass::ResumeWith); + Cleanup = [Handle, &Delegate] { Delegate.Remove(Handle); }; + } + else + { + Delegate.BindRaw(this, &ThisClass::ResumeWith); + Cleanup = [&Delegate] { Delegate.Unbind(); }; + } + } + UE_NONCOPYABLE(TDelegateAwaiter); + + template + R ResumeWith(T... Args) + { + TTuple Values(std::forward(Args)...); + Result = &Values; // This exposes a pointer to a local, but... + TryResumeOnce(); // ...it's only read by await_resume, right here + // The coroutine might have completed, destroying this object + return R(); + } + + FResult await_resume() + { + checkf(Result, TEXT("Internal error: resumed without a result")); + if constexpr (sizeof...(A) != 0) + return std::move(*Result); + } +}; + +template +class [[nodiscard]] TDynamicDelegateAwaiter : public FDelegateAwaiter +{ + using FResult = std::conditional_t&, void>; + using FPayload = std::conditional_t, TDecayedPayload, + TDecayedPayload>; + TDecayedPayload* Result; // Missing R, for the coroutine + +public: + template + explicit TDynamicDelegateAwaiter(T& InDelegate) + { + static_assert(TIsDynamicDelegate); + // SetupCallbackTarget sets Cleanup and ties Target's lifetime to this + auto* Target = SetupCallbackTarget([this](void* Params) + { + // This matches the hack in TBaseUFunctionDelegateInstance::Execute + Result = static_cast*>(Params); + TryResumeOnce(); + // The coroutine might have completed, deleting the awaiter + if constexpr (!std::is_void_v) + static_cast(Params)->template get() = R(); + }); + + if constexpr (TIsMulticastDelegate) + { + FScriptDelegate Delegate; + Delegate.BindUFunction(Target, NAME_Core); + InDelegate.Add(Delegate); + } + else + InDelegate.BindUFunction(Target, NAME_Core); + } + UE_NONCOPYABLE(TDynamicDelegateAwaiter); + + FResult await_resume() + { + if constexpr (sizeof...(A) != 0) + { + checkf(Result, TEXT("Internal error: resumed without a result")); + return *Result; + } + } +}; +} + +template +struct std::tuple_size> +{ + static constexpr size_t value = sizeof...(T); +}; + +template +struct std::tuple_element> +{ + using type = std::tuple_element_t>; +}; diff --git a/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/AsyncCoroutine.h b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/AsyncCoroutine.h new file mode 100644 index 00000000..b0dbe8db --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/AsyncCoroutine.h @@ -0,0 +1,489 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +/****************************************************************************** + * This file only contains private implementation details. * + * #include "UE5Coro/Coroutine.h" for the public API. * + ******************************************************************************/ + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#include +#define UE5CORO_PRIVATE_SUPPRESS_COROUTINE_INL +#include "UE5Coro/Coroutine.h" +#include "UE5Coro/Private.h" + +namespace UE5Coro::Private +{ +enum class ELatentExitReason : uint8; +class FAsyncAwaiter; +class FAsyncPromise; +class FLatentAwaiter; +class FLatentPromise; +class FPromise; +class FPromiseExtras; +template class TFutureAwaiter; +template class TTaskAwaiter; +namespace Test { class FTestHelper; } + +extern thread_local bool GDestroyedEarly; + +template +struct TAwaitTransform +{ + // Default passthrough + A& operator()(A& Awaitable) { return Awaitable; } + A&& operator()(A&& Awaitable) { return std::move(Awaitable); } +}; +} + +namespace UE5Coro +{ +#if UE5CORO_CPP20 +/** Things that can be co_awaited in a TCoroutine. */ +template +concept TAwaitable = requires +{ + // FLatentPromise supports more things than FAsyncPromise + Private::TAwaitTransform>()(std::declval()) + .await_suspend(std::declval>()); +}; +#endif +} + +namespace UE5Coro::Private +{ +template +struct [[nodiscard]] TAwaiter +{ + bool await_ready() noexcept { return false; } + + template + auto await_suspend(stdcoro::coroutine_handle

Handle) + -> std::enable_if_t> + { + if constexpr (std::is_base_of_v) + Handle.promise().DetachFromGameThread(); + static_cast(this)->Suspend(Handle.promise()); + } + + void await_resume() noexcept { } +}; + +struct FInitialSuspend +{ + enum EAction + { + Resume, + Destroy, + } Action; + + bool await_ready() noexcept { return false; } + + template + void await_suspend(stdcoro::coroutine_handle

Handle) + { + switch (Action) + { + case Resume: Handle.promise().ResumeFast(); break; + // This is very early and doesn't yet count as cancellation + case Destroy: Handle.destroy(); break; + } + } + + void await_resume() noexcept { } +}; + +struct FFinalSuspend +{ + bool bDestroy; + bool await_ready() noexcept { return false; } + void await_suspend(stdcoro::coroutine_handle<> Handle) noexcept + { + if (bDestroy) + Handle.destroy(); + } + void await_resume() noexcept { } +}; + +/** Fields of FPromise that may be alive after the coroutine is done. */ +class [[nodiscard]] UE5CORO_API FPromiseExtras +{ +public: +#if UE5CORO_DEBUG + int DebugID = -1; + const TCHAR* DebugPromiseType = nullptr; + const TCHAR* DebugName = nullptr; +#endif + + FEventRef Completed{EEventMode::ManualReset}; + // This could be read from another thread + std::atomic bWasSuccessful = false; + + FMutex Lock; + union + { + FPromise* Promise; // nullptr once destroyed + void* ReturnValuePtr; // in the destructor only + }; + + explicit FPromiseExtras(FPromise& Promise) noexcept : Promise(&Promise) { } + UE_NONCOPYABLE(FPromiseExtras); + virtual ~FPromiseExtras() = default; // Virtual for warning suppression only + + bool IsComplete() const; + template + void ContinueWith(F Fn); +}; + +template +struct [[nodiscard]] TPromiseExtras final : FPromiseExtras +{ +#if UE5CORO_DEBUG + std::atomic bMoveUsed = false; +#endif + T ReturnValue{}; + + explicit TPromiseExtras(FPromise& Promise) noexcept + : FPromiseExtras(Promise) { } +}; + +class [[nodiscard]] UE5CORO_API FCancellationTracker +{ + std::atomic bCanceled = false; + std::atomic CancellationHolds = 0; + +public: + void Cancel() { bCanceled = true; } + void Hold() { verify(++CancellationHolds >= 0); } + void Release() { verify(--CancellationHolds >= 0); } + bool ShouldCancel(bool bBypassHolds) const; +}; + +#if UE5CORO_DEBUG +extern std::atomic GLastDebugID; +#endif + +extern thread_local FPromise* GCurrentPromise; + +class [[nodiscard]] UE5CORO_API FPromise +{ + friend void TCoroutine<>::SetDebugName(const TCHAR*); + + FCancellationTracker CancellationTracker; + +protected: + std::shared_ptr Extras; + TArray> OnCompleted; +#if !PLATFORM_EXCEPTIONS_DISABLED + std::atomic bUnhandledException = false; +#endif + + explicit FPromise(std::shared_ptr, const TCHAR* PromiseType); + UE_NONCOPYABLE(FPromise); + virtual ~FPromise(); // Virtual for warning suppression only + virtual bool IsEarlyDestroy() const = 0; + +public: + static FPromise& Current(); + + /** Request deletion now or very soon. */ + virtual void ThreadSafeDestroy(); + void Cancel(); + bool ShouldCancel(bool bBypassHolds = false) const; + void HoldCancellation(); + void ReleaseCancellation(); + virtual void Resume(bool bBypassCancellationHolds = false); + void ResumeFast(); + void AddContinuation(std::function); + + void unhandled_exception(); + + // co_yield is not allowed in async coroutines + template + stdcoro::suspend_never yield_value(T&&) = delete; +}; + +class [[nodiscard]] UE5CORO_API FAsyncPromise : public FPromise +{ + virtual bool IsEarlyDestroy() const override; + +protected: + template + explicit FAsyncPromise(std::shared_ptr InExtras, A&&...) + : FPromise(std::move(InExtras), TEXT("Async")) { } + +public: + FInitialSuspend initial_suspend() noexcept + { + return {FInitialSuspend::Resume}; + } + + stdcoro::suspend_never final_suspend() noexcept { return {}; } + + template + decltype(auto) await_transform(T&& Awaitable) + { + TAwaitTransform> Transform; + return Transform(std::forward(Awaitable)); + } +}; + +class [[nodiscard]] UE5CORO_API FLatentPromise : public FPromise +{ + FWeakObjectPtr Context = nullptr; // Owning object or world, best approximation + void* PendingLatentCoroutine = nullptr; + enum ELatentFlags + { + LF_Detached = 1, + LF_Successful = 2, + }; + std::atomic LatentFlags = 0; // int to get the bitwise operators + ELatentExitReason ExitReason = static_cast(0); + + void CreateLatentAction(); + void CreateLatentAction(FLatentActionInfo&&); + void Init(); + template void Init(const UObject*, T&...); + template void Init(FLatentActionInfo, T&...); + template void Init(T&, A&...); + +protected: + template + explicit FLatentPromise(std::shared_ptr, T&&...); + virtual ~FLatentPromise() override; + virtual bool IsEarlyDestroy() const override; + +public: + virtual void ThreadSafeDestroy() override; + + virtual void Resume(bool bBypassCancellationHolds = false) override; + void CancelFromWithin(); + + void AttachToGameThread(bool bFromAnyThread); + void DetachFromGameThread(); + bool IsOnGameThread() const; + + ELatentExitReason GetExitReason() const { return ExitReason; } + void SetExitReason(ELatentExitReason Reason); + void SetCurrentAwaiter(FLatentAwaiter*); + + FInitialSuspend initial_suspend(); + template + FFinalSuspend final_suspend() noexcept; + + template + decltype(auto) await_transform(T&& Awaitable) + { + TAwaitTransform> Transform; + return Transform(std::forward(Awaitable)); + } +}; + +template +class TCoroutinePromise : public Base +{ +public: + template + explicit TCoroutinePromise(A&&... Args) + : Base(std::make_shared>(*this), + std::forward(Args)...) { } + UE_NONCOPYABLE(TCoroutinePromise); + + ~TCoroutinePromise() + { + auto* ExtrasT = static_cast*>(this->Extras.get()); + ExtrasT->Lock.lock(); // This will be held until the end of ~FPromise + checkf(ExtrasT->Promise, TEXT("Unexpected double promise destruction")); + ExtrasT->ReturnValuePtr = &ExtrasT->ReturnValue; + } + + void return_value(T Value) + { + auto* ExtrasT = static_cast*>(this->Extras.get()); + std::scoped_lock _(ExtrasT->Lock); + check(!ExtrasT->IsComplete()); // Completion is after a value is returned + ExtrasT->ReturnValue = std::move(Value); + } + + TCoroutine get_return_object() noexcept + { + return TCoroutine(this->Extras); + } +}; + +template +class TCoroutinePromise : public Base +{ +public: + template + explicit TCoroutinePromise(A&&... Args) + : Base(std::make_shared(*this), std::forward(Args)...) + { } + UE_NONCOPYABLE(TCoroutinePromise); + + ~TCoroutinePromise() + { + // This will be held until the end of ~FPromise + this->Extras->Lock.lock(); + checkf(this->Extras->Promise, + TEXT("Unexpected double promise destruction")); + this->Extras->ReturnValuePtr = nullptr; + } + + void return_void() noexcept { } + + TCoroutine<> get_return_object() noexcept + { + return TCoroutine<>(this->Extras); + } +}; + +template +void FPromiseExtras::ContinueWith(F Fn) +{ + std::unique_lock _(Lock); + if (IsComplete()) // Already completed? + { + _.unlock(); + if constexpr (std::is_void_v) + Fn(); + else // T is controlled by TCoroutine, safe to cast + Fn(static_cast*>(this)->ReturnValue); + return; + } + + checkf(Promise, + TEXT("Internal error: attaching continuation to a complete promise")); + Promise->AddContinuation([Fn = std::move(Fn)](void* Data) + { + if constexpr (std::is_void_v) + Fn(); + else + Fn(*static_cast(Data)); + }); +} + +template +FLatentPromise::FLatentPromise(std::shared_ptr Extras, + T&&... Args) + : FPromise(std::move(Extras), TEXT("Latent")) +{ + checkf(IsInGameThread(), + TEXT("Latent coroutines may only be started on the game thread")); + + Init(Args...); // Deliberately not forwarding to force lvalue references +} + +template +void FLatentPromise::Init(const UObject* WorldContext, T&... Args) +{ + // Scan the UObjects passed in, looking for the first one with a world + if (Context.IsExplicitlyNull() && IsValid(WorldContext) && + IsValid(WorldContext->GetWorld())) + Context = WorldContext; // null is fine + + Init(Args...); +} + +template +void FLatentPromise::Init(FLatentActionInfo LatentInfo, T&... Args) +{ + // The static_assert on coroutine_traits prevents this + check(!PendingLatentCoroutine); + CreateLatentAction(std::move(LatentInfo)); + + // The latent info's CallbackTarget has the highest priority as a context + if (auto* Target = LatentInfo.CallbackTarget; + IsValid(Target) && IsValid(Target->GetWorld())) + Context = FWeakObjectPtr(LatentInfo.CallbackTarget); + + Init(Args...); +} + +template +void FLatentPromise::Init(T& First, A&... Args) +{ + // Convert UObject& to UObject* for world context + if constexpr (std::is_convertible_v) + Init(static_cast(std::addressof(First)), Args...); + // Do the UObject* cast if overload resolution finds this function somehow + else if constexpr (std::is_convertible_v) + Init(static_cast(First), Args...); + else + Init(Args...); +} +} + +template +struct UE5Coro::Private::stdcoro::coroutine_traits, + Args...> +{ + static constexpr int LatentInfoCount = + (0 + ... + std::is_convertible_v); + static constexpr int LatentForceCount = + (0 + ... + std::is_same_v); + static_assert(LatentInfoCount + LatentForceCount <= 1, + "Multiple latent info/force parameters found in coroutine"); + static constexpr bool bUseLatent = LatentInfoCount || LatentForceCount; + using promise_type = UE5Coro::Private::TCoroutinePromise< + T, std::conditional_t>; +}; + +template +struct UE5Coro::Private::stdcoro::coroutine_traits, + Args...> +{ + using promise_type = typename coroutine_traits, + Args...>::promise_type; +}; + +template +struct UE5Coro::Private::stdcoro::coroutine_traits +{ + using promise_type = typename coroutine_traits, + Args...>::promise_type; +}; + +template +struct UE5Coro::Private::stdcoro::coroutine_traits +{ + using promise_type = typename coroutine_traits, + Args...>::promise_type; +}; + +#include "UE5Coro/Coroutine.inl" diff --git a/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Cancellation.h b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Cancellation.h new file mode 100644 index 00000000..8968d183 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Cancellation.h @@ -0,0 +1,111 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#include "Misc/ScopeExit.h" +#include "UE5Coro/AsyncCoroutine.h" + +namespace UE5Coro +{ +namespace Private +{ +class FPromise; +class FCancellationAwaiter; +} + +/** + * Guards against user-requested cancellation. For advanced use.
+ * This does NOT affect a latent coroutine destroyed by the latent action + * manager.

+ * If any number of these objects is in scope within a coroutine returning + * TCoroutine, it will delay cancellations and not process them in co_awaits.
+ * The first co_await after the last one of these has gone out of scope will + * process the cancellation that was deferred.
+ */ +class [[nodiscard]] UE5CORO_API FCancellationGuard +{ +#if UE5CORO_DEBUG + Private::FPromise* Promise; +#endif + +public: + FCancellationGuard(); + UE_NONCOPYABLE(FCancellationGuard); + ~FCancellationGuard(); + + // These objects only make sense as locals + void* operator new(std::size_t) = delete; + void* operator new[](std::size_t) = delete; +}; + +/** + * Provided for advanced scenarios, prefer ON_SCOPE_EXIT or RAII for + * unconditional cleanup.

+ * This will ONLY call the provided callback if this object is in scope within + * a coroutine that's being cleaned up early: due to manual cancellation, the + * latent action manager deleting its corresponding latent action, etc.
+ *
Example usage:
+ * FOnCoroutineCanceled Guard([this]{cleanup code}); + */ +struct [[nodiscard]] UE5CORO_API FOnCoroutineCanceled + : ScopeExitSupport::TScopeGuard> +{ + explicit FOnCoroutineCanceled(std::function Fn); +}; + +/** co_awaiting the return value of this function does nothing if the calling + * coroutine is not currently canceled. + * If it is canceled, the cancellation will be processed immediately. + * FCancellationGuards are respected. */ +UE5CORO_API Private::FCancellationAwaiter FinishNowIfCanceled(); + +/** Checks if the current coroutine is canceled, without processing the + * cancellation.
+ * Prefer co_await to invoke normal cancellation processing instead.
+ * Only valid to call from within a coroutine returning TCoroutine. + * @return True if the current coroutine is canceled.
+ * FCancellationGuards do not affect the return value of this function. */ +[[nodiscard]] UE5CORO_API bool IsCurrentCoroutineCanceled(); +} + +namespace UE5Coro::Private +{ +class [[nodiscard]] UE5CORO_API FCancellationAwaiter + : public TAwaiter +{ +public: + bool await_ready(); + void Suspend(FPromise&); +}; +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Coroutine.h b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Coroutine.h new file mode 100644 index 00000000..f0185fe5 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Coroutine.h @@ -0,0 +1,258 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#if UE5CORO_CPP20 +#include +#include +#endif +#include +#include "CoroutinePrivate.inl" +#include "Coroutine.generated.h" + +namespace UE5Coro +{ +/** + * Asynchronous coroutine. Return this type from a function and it will be able + * to co_await various awaiters without blocking the calling thread.
+ * These objects do not represent ownership of the coroutine and do not need to + * be stored. Copies will refer to the same coroutine.
+ * TCoroutine objects may be safely object sliced to TCoroutine<>, providing + * a return-type-erased handle to the same coroutine. + * @tparam T Optional return value of the coroutine. @p void if not provided. + */ +template +class TCoroutine; + +/** Common functionality for TCoroutines of all return types. */ +template<> +class UE5CORO_API TCoroutine<> +{ + template + friend class Private::TCoroutinePromise; + friend std::hash>; + +protected: + std::shared_ptr Extras; + + explicit TCoroutine(std::shared_ptr Extras) + : Extras(std::move(Extras)) { } + +public: + /** A coroutine that has already completed with no return value. */ + static const TCoroutine<> CompletedCoroutine; + + /** A coroutine that has already completed with the provided value. */ + template + static auto FromResult(V&& Value) + -> TCoroutine>>; + + /** Request the coroutine to stop executing at the next opportunity.
+ * This function returns immediately, with the coroutine still running.
+ * Has no effect on coroutines that have already completed. */ + void Cancel(); + + /** Blocks until the coroutine completes for any reason, including being + * unsuccessful or canceled. + * This could result in a deadlock if the coroutine wants to use the thread + * that's blocking. + * @return True if the coroutine completed, false on timeout. */ + bool Wait(uint32 WaitTimeMilliseconds = MAX_uint32, + bool bIgnoreThreadIdleStats = false) const; + + /** Returns true if the coroutine has ended for any reason, including normal + * completion, cancellation, or an unhandled exception. */ + [[nodiscard]] bool IsDone() const; + + /** Returns true if the coroutine ran to completion successfully. + * Cancellations after completion don't change this flag. */ + [[nodiscard]] bool WasSuccessful() const; + + /** Calls the provided functor when this coroutine is complete, including + * unsuccessful completions such as being canceled.
+ * If the coroutine is already complete, it will be called immediately, + * otherwise it will be called on the same thread where the coroutine + * completes. */ + template + auto ContinueWith(F Continuation) + -> std::enable_if_t>; + + /** Like ContinueWith, but the provided functor will only be called if the + * object is still alive at the time of coroutine completion.
+ * The first parameter may be UObject*, TSharedPtr, or std::shared_ptr. */ + template + auto ContinueWithWeak(U Ptr, F Continuation) + -> std::enable_if_t::value && std::is_invocable_v>; + + /** Convenience overload that also passes the object as the first argument + * for, e.g., UObject/Slate member function pointers or static methods with + * a world context.
+ * The first parameter may be UObject*, TSharedPtr, or std::shared_ptr.
+ * Example usage: ContinueWithWeak(this, &ThisClass::Method) */ + template + auto ContinueWithWeak(U Ptr, F Continuation) + -> std::enable_if_t::ptr>>; + + /** Sets a debug name for the currently-executing coroutine. + * Only valid to call from within a coroutine returning TCoroutine. */ + static void SetDebugName(const TCHAR* Name); + + /** @return True if the two objects refer to the same coroutine invocation. */ + bool operator==(const TCoroutine<>&) const noexcept; + +#if UE5CORO_CPP20 + std::strong_ordering operator<=>(const TCoroutine<>&) const noexcept; +#else + bool operator!=(const TCoroutine<>&) const noexcept; + bool operator<(const TCoroutine<>&) const noexcept; +#endif +}; + +/** Extra functionality for coroutines with non-void return types. */ +template +class TCoroutine : public TCoroutine<> +{ +protected: + using TCoroutine<>::TCoroutine; + +public: + /** A coroutine that has already completed with the provided value. */ + static TCoroutine FromResult(T Value); + + /** Waits for the coroutine to finish, then gets its result. */ + const T& GetResult() const; + + /** Waits for the coroutine to finish, then gets its result as an rvalue.
+ * Depending on T, this will often invalidate further GetResult and + * ContinueWith calls across all copies that refer to the same coroutine. */ + T&& MoveResult(); + + /** Calls the provided functor with this coroutine's result when it's + * complete, including unsuccessful completions such as being canceled.
+ * If the coroutine is already complete, it will be called immediately, + * otherwise it will be called on the same thread where the coroutine + * completes. */ + template + auto ContinueWith(F Continuation) + -> std::enable_if_t || std::is_invocable_v>; + + /** Like ContinueWith, but the provided functor will only be called if the + * object is still alive at the time of coroutine completion.
+ * The first parameter may be UObject*, TSharedPtr, or std::shared_ptr. */ + template + auto ContinueWithWeak(U Ptr, F Continuation) + -> std::enable_if_t::value && + (std::is_invocable_v || std::is_invocable_v)>; + + /** Convenience overload that also passes the object as the first argument + * for, e.g., UObject/Slate member function pointers or static methods with + * a world context.
+ * The first parameter may be UObject*, TSharedPtr, or std::shared_ptr.
+ * Example usage: ContinueWithWeak(this, &ThisClass::Method) */ + template + auto ContinueWithWeak(U Ptr, F Continuation) -> std::enable_if_t< + std::is_invocable_v::ptr> || + std::is_invocable_v::ptr, T>>; +}; + +static_assert(sizeof(TCoroutine) == sizeof(TCoroutine<>)); +#if UE5CORO_CPP20 && !UE5CORO_PRIVATE_LIBCPP_IS_BROKEN +static_assert(std::totally_ordered>); +static_assert(std::totally_ordered_with, TCoroutine>); +#endif + +UE5CORO_API uint32 GetTypeHash(const TCoroutine<>&) noexcept; // ADL +} + +/** USTRUCT wrapper for TCoroutine<>. */ +USTRUCT(BlueprintInternalUseOnly) +struct UE5CORO_API FAsyncCoroutine +#if CPP + : UE5Coro::TCoroutine<> +#endif +{ + GENERATED_BODY() + + /** This constructor is public to placate the reflection system and BP.
+ * Do not use directly. Interacting with default-constructed + * FAsyncCoroutines is undefined behavior. */ + FAsyncCoroutine() : TCoroutine(nullptr) { } + + /** Implicit conversion from any TCoroutine. */ + template + FAsyncCoroutine(const TCoroutine& Coroutine) : TCoroutine(Coroutine) { } +}; + +static_assert(sizeof(FAsyncCoroutine) == sizeof(UE5Coro::TCoroutine<>)); + + +#pragma region std::hash +template<> +struct UE5CORO_API std::hash> +{ + size_t operator()(const UE5Coro::TCoroutine<>&) const noexcept; +}; +template<> +struct std::hash +{ + size_t operator()(const UE5Coro::TCoroutine<>& Handle) const noexcept + { + return std::hash>()(Handle); + } +}; +template +struct std::hash> +{ + size_t operator()(const UE5Coro::TCoroutine& Handle) const noexcept + { + return std::hash>()(Handle); + } +}; +#pragma endregion + +/** Taking this struct as a parameter in a coroutine will force latent execution + * mode, even if it does not have a FLatentActionInfo parameter.
+ * It is compatible with UFUNCTIONs and hidden on BP call nodes. */ +USTRUCT(BlueprintInternalUseOnly) +struct UE5CORO_API FForceLatentCoroutine +{ + GENERATED_BODY() +}; + +#if CPP +#include "UE5Coro/AsyncCoroutine.h" +#ifndef UE5CORO_PRIVATE_SUPPRESS_COROUTINE_INL +#include "UE5Coro/Coroutine.inl" +#endif +#endif diff --git a/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Coroutine.inl b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Coroutine.inl new file mode 100644 index 00000000..817dbdb0 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Coroutine.inl @@ -0,0 +1,182 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +namespace UE5Coro +{ +// Argument deduction on TCoroutine<> +template +auto TCoroutine<>::FromResult(V&& Value) + -> TCoroutine>> +{ + co_return std::forward(Value); +} + +// TCoroutine matches T exactly +template +TCoroutine TCoroutine::FromResult(T Value) +{ + co_return std::move(Value); +} + +template +const T& TCoroutine::GetResult() const +{ + Wait(); + auto* ExtrasT = static_cast*>(Extras.get()); + return ExtrasT->ReturnValue; +} + +template +T&& TCoroutine::MoveResult() +{ + Wait(); + auto* ExtrasT = static_cast*>(Extras.get()); +#if UE5CORO_DEBUG + [[maybe_unused]] bool bOld = false; + ensureMsgf(ExtrasT->bMoveUsed.compare_exchange_strong(bOld, true), + TEXT("MoveResult called multiple times on the same value")); +#endif + return std::move(ExtrasT->ReturnValue); +} + +template +auto TCoroutine<>::ContinueWith(F Continuation) + -> std::enable_if_t> +{ + Extras->ContinueWith(std::move(Continuation)); +} + +template +auto TCoroutine<>::ContinueWithWeak(U Ptr, F Continuation) + -> std::enable_if_t::value && std::is_invocable_v> +{ + ContinueWith([Weak = typename Private::TWeak::weak(std::move(Ptr)), + Fn = std::move(Continuation)] + { + auto Strong = Private::TWeak::Strengthen(Weak); + if (Private::TWeak::Get(Strong)) + Fn(); + }); +} + +template +auto TCoroutine<>::ContinueWithWeak(U Ptr, F Continuation) + -> std::enable_if_t::ptr>> +{ + ContinueWith([Weak = typename Private::TWeak::weak(std::move(Ptr)), + Fn = std::move(Continuation)] + { + auto Strong = Private::TWeak::Strengthen(Weak); + if (auto* Raw = Private::TWeak::Get(Strong)) + std::invoke(Fn, Raw); + }); +} + +template +template +auto TCoroutine::ContinueWith(F Continuation) + -> std::enable_if_t || std::is_invocable_v> +{ + // Handle functors that can't take (T) with the void specialization. + // This can't be an overload, it would be considered ambiguous. + if constexpr (!std::is_invocable_v) + TCoroutine<>::ContinueWith(std::move(Continuation)); + else + Extras->ContinueWith(std::move(Continuation)); +} + +template +template +auto TCoroutine::ContinueWithWeak(U Ptr, F Continuation) + -> std::enable_if_t::value && + (std::is_invocable_v || std::is_invocable_v)> +{ + if constexpr (!std::is_invocable_v) + TCoroutine<>::ContinueWithWeak(std::move(Ptr), std::move(Continuation)); + else + ContinueWith([Weak = typename Private::TWeak::weak(std::move(Ptr)), + Fn = std::move(Continuation)](const T& Result) + { + auto Strong = Private::TWeak::Strengthen(Weak); + if (Private::TWeak::Get(Strong)) + std::invoke(Fn, Result); + }); +} + +template +template +auto TCoroutine::ContinueWithWeak(U Ptr, F Continuation) + -> std::enable_if_t::ptr> || + std::is_invocable_v::ptr, T>> +{ + if constexpr (!std::is_invocable_v::ptr, T>) + TCoroutine<>::ContinueWithWeak(std::move(Ptr), std::move(Continuation)); + else + ContinueWith([Weak = typename Private::TWeak::weak(std::move(Ptr)), + Fn = std::move(Continuation)](const T& Result) + { + auto Strong = Private::TWeak::Strengthen(Weak); + if (auto* Raw = Private::TWeak::Get(Strong)) + std::invoke(Fn, Raw, Result); + }); +} +} + +// Declare these here, so that co_awaiting TCoroutines always picks them up. +// They're implemented in another header. +namespace UE5Coro::Private +{ +template class TAsyncCoroutineAwaiter; +template class TLatentCoroutineAwaiter; + +template +struct Private::TAwaitTransform> +{ + TAsyncCoroutineAwaiter operator()(TCoroutine); +}; + +template +struct Private::TAwaitTransform> +{ + TLatentCoroutineAwaiter operator()(TCoroutine); +}; + +template +struct Private::TAwaitTransform +{ + auto operator()(FAsyncCoroutine Coro) + { + return TAwaitTransform>()(std::move(Coro)); + } +}; +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/CoroutineAwaiters.h b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/CoroutineAwaiters.h new file mode 100644 index 00000000..facfd0fe --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/CoroutineAwaiters.h @@ -0,0 +1,117 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#include "Async/TaskGraphInterfaces.h" +#include "UE5Coro/Coroutine.h" +#include "UE5Coro/AsyncAwaiters.h" +#include "UE5Coro/LatentAwaiters.h" + +namespace UE5Coro::Private +{ +template +class TAsyncCoroutineAwaiter : public TAwaiter> +{ + TCoroutine Antecedent; + +public: + explicit TAsyncCoroutineAwaiter(TCoroutine Antecedent) + : Antecedent(std::move(Antecedent)) { } + + void Suspend(FPromise& Promise) + { + Antecedent.ContinueWith([&Promise] { Promise.Resume(); }); + } + + T await_resume() + { + checkf(Antecedent.IsDone(), TEXT("Internal error: resuming too early")); + if constexpr (!std::is_void_v) + return Antecedent.GetResult(); + } +}; + +template +bool ShouldResumeLatentCoroutine(void* State, bool bCleanup) +{ + auto* This = static_cast*>(State); + if (UNLIKELY(bCleanup)) + { + delete This; + return false; + } + return This->IsDone(); +} + +template +class TLatentCoroutineAwaiter : public FLatentAwaiter +{ +public: + explicit TLatentCoroutineAwaiter(TCoroutine Antecedent) + : FLatentAwaiter(new TCoroutine(std::move(Antecedent)), + &ShouldResumeLatentCoroutine) { } + + // Prevent surprises with `co_await SomeCoroutine();` by making a copy. + // This cannot be moved as there could be another TCoroutine still owning it + T await_resume() + { + auto* Coro = static_cast*>(State); + checkf(Coro->IsDone(), TEXT("Internal error: resuming too early")); + return Coro->GetResult(); + } +}; + +template<> +class TLatentCoroutineAwaiter : public FLatentAwaiter +{ +public: + explicit TLatentCoroutineAwaiter(TCoroutine<> Antecedent) + : FLatentAwaiter(new TCoroutine(std::move(Antecedent)), + &ShouldResumeLatentCoroutine) { } +}; + +template +auto TAwaitTransform>::operator()(TCoroutine Coro) + -> TAsyncCoroutineAwaiter +{ + return TAsyncCoroutineAwaiter(std::move(Coro)); +} + +template +auto TAwaitTransform>::operator()(TCoroutine Coro) + -> TLatentCoroutineAwaiter +{ + return TLatentCoroutineAwaiter(std::move(Coro)); +} +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/CoroutinePrivate.inl b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/CoroutinePrivate.inl new file mode 100644 index 00000000..99805697 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/CoroutinePrivate.inl @@ -0,0 +1,79 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "UObject/StrongObjectPtr.h" + +namespace UE5Coro::Private +{ +class FPromiseExtras; +template class TCoroutinePromise; + +// Transforms T to its weak pointer version +template +struct TWeak : std::false_type { }; + +template +struct TWeak : std::bool_constant> +{ + using strong = TStrongObjectPtr; + using weak = TWeakObjectPtr; + using ptr = std::enable_if_t; + static strong Strengthen(const weak& Weak) + { + FGCScopeGuard _; + // There's no API to convert a weak ptr to a strong one... + return strong(Weak.Get()); // relying on C++17 mandatory RVO + } + static ptr Get(const strong& Strong) { return Strong.Get(); } +}; + +template // ContinueWith isn't guaranteed GT only +struct TWeak> : std::true_type +{ + using strong = TSharedPtr; + using weak = TWeakPtr; + using ptr = T*; + static strong Strengthen(const weak& Weak) { return Weak.Pin(); } + static ptr Get(const strong& Strong) { return Strong.Get(); } +}; + +template +struct TWeak> : std::true_type +{ + using strong = std::shared_ptr; + using weak = std::weak_ptr; + using ptr = T*; + static strong Strengthen(const weak& Weak) { return Weak.lock(); } + static ptr Get(const strong& Strong) { return Strong.get(); } +}; +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Definitions.h b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Definitions.h new file mode 100644 index 00000000..50b9d84d --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Definitions.h @@ -0,0 +1,56 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#if ENGINE_MAJOR_VERSION > 4 +#error This release does not support Unreal Engine 5. +#endif + +#if __has_include() && defined(__cpp_impl_coroutine) +#include +namespace UE5Coro::Private +{ + namespace stdcoro = ::std; +} +#else +#error UE5Coro requires C++20 or the Coroutines TS for C++17. +#endif + +#ifndef UE5CORO_DEBUG +#define UE5CORO_DEBUG (UE_BUILD_DEBUG || UE_BUILD_DEVELOPMENT) +#endif + +#if defined(_LIBCPP_VERSION) && _LIBCPP_VERSION < 16000 +#define UE5CORO_PRIVATE_LIBCPP_IS_BROKEN 1 +#else +#define UE5CORO_PRIVATE_LIBCPP_IS_BROKEN 0 +#endif diff --git a/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Generator.h b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Generator.h new file mode 100644 index 00000000..30b0de9e --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Generator.h @@ -0,0 +1,203 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" + +namespace UE5Coro +{ +template class TGeneratorIterator; +namespace Private { template class TGeneratorPromise; } + +/** + * Generator coroutine. Make a function return Generator instead of T and + * it will be able to co_yield multiple values throughout its execution. + * Callers can either manually fetch values or use the provided iterator + * wrappers to treat the returned values as a virtual container. + */ +template +struct [[nodiscard]] TGenerator +{ + using promise_type = Private::TGeneratorPromise; + using iterator = TGeneratorIterator; + friend promise_type; + +private: + Private::stdcoro::coroutine_handle Handle; + + explicit TGenerator( + Private::stdcoro::coroutine_handle Handle) noexcept + : Handle(Handle) { } + +public: + TGenerator(TGenerator&& Other) noexcept { std::swap(Handle, Other.Handle); } + + ~TGenerator() + { + if (Handle) + Handle.destroy(); + } + + TGenerator(const TGenerator&) = delete; + TGenerator& operator=(const TGenerator&) = delete; + TGenerator& operator=(TGenerator&&) = delete; + + /** Returns true if Current() is valid. */ + explicit operator bool() const noexcept { return Handle && !Handle.done(); } + + /** Resumes the generator. Returns true if Current() is valid. */ + bool Resume() + { + if (LIKELY(*this)) + Handle.resume(); + return operator bool(); + } + + /** Retrieves the value that was last co_yielded. */ + T& Current() const + { + checkf(Handle && Handle.promise().Current, + TEXT("Attempting to read from invalid generator")); + return *static_cast(Handle.promise().Current); + } + + iterator CreateIterator() noexcept { return iterator(*this); } + iterator begin() noexcept { return iterator(*this); } + iterator end() const noexcept { return iterator(nullptr); } +}; + +/** Provides an iterator-like interface over TGenerator: operator++ advances + * the generator, operator* and operator-> read the current value, etc. */ +template +class TGeneratorIterator +{ + TGenerator* Generator; // nullptr == end() + +public: + /** Constructs an iterator wrapper over a generator coroutine. */ + explicit TGeneratorIterator(TGenerator& Generator) noexcept + : Generator(Generator ? &Generator : nullptr) { } + + /** The end() iterator for every generator coroutine. */ + explicit TGeneratorIterator(std::nullptr_t) noexcept + : Generator(nullptr) { } + + /** Returns true if the iterator is not equal to end(). + * Provided for compatibility with code expecting UE-style iterators. */ + explicit operator bool() const noexcept { return Generator != nullptr; } + + /** Compares this iterator with another. Provided for STL compatibility. */ + bool operator==(const TGeneratorIterator& Other) const noexcept + { + return Generator == Other.Generator; + } + + /** Compares this iterator with another. Provided for STL compatibility. */ + bool operator!=(const TGeneratorIterator& Other) const noexcept + { + return Generator != Other.Generator; + } + + /** Advances the generator. */ + TGeneratorIterator& operator++() + { + checkf(Generator, TEXT("Attempted to move iterator past end()")); + if (UNLIKELY(!Generator->Resume())) // Did the coroutine finish? + Generator = nullptr; // Become end() if it did + return *this; + } + + /** Advances the generator. Returns void to break code that expects a + * meaningful value before incrementing, because the generator's previous + * state cannot be copied and is always lost. */ + void operator++(int) { operator++(); } + + /** Returns the generator's Current() value. */ + T& operator*() const + { + checkf(Generator, TEXT("Attempted to dereference invalid iterator")); + return Generator->Current(); + } + + /** Returns a pointer to the generator's Current() value. */ + T* operator->() const { return std::addressof(operator*()); } +}; +} + +namespace UE5Coro::Private +{ +class [[nodiscard]] UE5CORO_API FGeneratorPromise +{ +protected: + /** Points to the current co_yield expression if valid. */ + void* Current = nullptr; + +public: + FGeneratorPromise() = default; + UE_NONCOPYABLE(FGeneratorPromise); + + stdcoro::suspend_never initial_suspend() noexcept { return {}; } + stdcoro::suspend_always final_suspend() noexcept { return {}; } + void return_void() noexcept { Current = nullptr; } + void unhandled_exception(); + + // co_await is not allowed in generators + template + stdcoro::suspend_never await_transform(T&&) = delete; +}; + +template +class [[nodiscard]] TGeneratorPromise : public FGeneratorPromise +{ + friend TGenerator; + using handle_type = stdcoro::coroutine_handle; + +public: + TGenerator get_return_object() noexcept + { + return TGenerator(handle_type::from_promise(*this)); + } + + auto yield_value(std::remove_reference_t& Value) noexcept + { + Current = std::addressof(Value); + return stdcoro::suspend_always(); + } + + auto yield_value(std::remove_reference_t&& Value) noexcept + { + Current = std::addressof(Value); + return stdcoro::suspend_always(); + } +}; +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/HttpAwaiters.h b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/HttpAwaiters.h new file mode 100644 index 00000000..d98a914a --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/HttpAwaiters.h @@ -0,0 +1,79 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#include +#include "Interfaces/IHttpRequest.h" +#include "UE5Coro/AsyncCoroutine.h" + +namespace UE5Coro::Private +{ +class FHttpAwaiter; +} + +namespace UE5Coro::Http +{ +/** Processes the request, resumes the coroutine after it's done.
+ * The result of the co_await expression will be a TTuple of + * FHttpResponsePtr and bool bConnectedSuccessfully. */ +UE5CORO_API Private::FHttpAwaiter ProcessAsync(FHttpRequestRef); +} + +namespace UE5Coro::Private +{ +class [[nodiscard]] UE5CORO_API FHttpAwaiter : public TAwaiter +{ + struct [[nodiscard]] UE5CORO_API FState + { + const FHttpRequestRef Request; + FMutex Lock; + FPromise* Promise = nullptr; + bool bSuspended = false; + // end Lock + std::optional> Result; + + explicit FState(FHttpRequestRef&&); + void RequestComplete(FHttpRequestPtr, FHttpResponsePtr, bool); + void Resume(); + }; + TSharedPtr State; + +public: + explicit FHttpAwaiter(FHttpRequestRef&& Request); + + bool await_ready(); + void Suspend(FPromise&); + TTuple await_resume(); +}; +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/LatentAwaiters.h b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/LatentAwaiters.h new file mode 100644 index 00000000..1cdf3d7f --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/LatentAwaiters.h @@ -0,0 +1,531 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#if UE5CORO_CPP20 +#include +#endif +#include +#include "Engine/StreamableManager.h" +#include "UE5Coro/AsyncCoroutine.h" +#include "UE5Coro/Private.h" + +namespace UE5Coro::Private +{ +class FAsyncPromise; +class FLatentAwaiter; +class FLatentCancellation; +class FLatentChainAwaiter; +class FLatentPromise; +class FPackageLoadAwaiter; +template class TAsyncLoadAwaiter; +template class TAsyncQueryAwaiter; +template class TAsyncQueryAwaiterRV; + +UE5CORO_API std::tuple UntilDelegateCore(); +} + +namespace UE5Coro::Latent +{ +/** Stops the latent coroutine immediately WITHOUT firing the latent exec pin.
+ * The coroutine WILL NOT be resumed. + * This does not count as the coroutine being aborted for FOnActionAborted. + * @see TCoroutine<>::Cancel to cancel a coroutine from outside. */ +Private::FLatentCancellation Cancel() noexcept; + +#pragma region Tick + +/** Resumes the coroutine in the next tick.
+ * @see Latent::Until for an alternative to while-NextTick loops. */ +UE5CORO_API Private::FLatentAwaiter NextTick(); + +/** Resumes the coroutine the given number of ticks later. */ +UE5CORO_API Private::FLatentAwaiter Ticks(int64); + +/** Polls the provided function, resumes the coroutine when it returns true. */ +UE5CORO_API Private::FLatentAwaiter Until(std::function Function); + +/** Resumes the coroutine after the delegate executes.
+ * Delegate parameters are ignored, a return value is not provided.
+ * Delegates are also co_awaitable without this wrapper. + * See the documentation for details on the differences in behavior. */ +template +auto UntilDelegate(T& Delegate) + -> std::enable_if_t, Private::FLatentAwaiter>; + +#pragma endregion + +#pragma region Time + +/** Resumes the coroutine the specified amount of seconds later.
+ * This is affected by both pause and time dilation. */ +UE5CORO_API Private::FLatentAwaiter Seconds(float); + +/** Resumes the coroutine the specified amount of seconds later.
+ * This is affected by time dilation only, NOT pause. */ +UE5CORO_API Private::FLatentAwaiter UnpausedSeconds(float); + +/** Resumes the coroutine the specified amount of seconds later.
+ * This is not affected by pause or time dilation. */ +UE5CORO_API Private::FLatentAwaiter RealSeconds(float); + +/** Resumes the coroutine the specified amount of seconds later.
+ * This is affected by pause only, NOT time dilation. */ +UE5CORO_API Private::FLatentAwaiter AudioSeconds(float); + +/** Resumes the coroutine when world time reaches the provided value.
+ * This is affected by both pause and time dilation. */ +UE5CORO_API Private::FLatentAwaiter UntilTime(float); + +/** Resumes the coroutine when unpaused world time reaches the provided value.
+ * This is affected by time dilation only, NOT pause. */ +UE5CORO_API Private::FLatentAwaiter UntilUnpausedTime(float); + +/** Resumes the coroutine when real world time reaches the provided value.
+ * This is not affected by pause or time dilation. */ +UE5CORO_API Private::FLatentAwaiter UntilRealTime(float); + +/** Resumes the coroutine when audio world time reaches the provided value.
+ * This is affected by pause only, NOT time dilation. */ +UE5CORO_API Private::FLatentAwaiter UntilAudioTime(float); + +#pragma endregion + +#pragma region Chain + +#if UE5CORO_CPP20 && !UE5CORO_PRIVATE_LIBCPP_IS_BROKEN +/** Resumes the coroutine once the chained static latent action has finished, + * with automatic parameter matching.
+ * The result of the co_await expression is true if the chained latent action + * finished normally, false if it didn't.
+ * Example usage:
+ * co_await Latent::Chain(&UKismetSystemLibrary::Delay, 1.0f); */ +template +Private::FLatentChainAwaiter Chain(auto (*Function)(FnParams...), auto&&... Args); + +/** Resumes the coroutine once the chained member latent action has finished, + * with automatic parameter matching. + * The result of the co_await expression is true if the chained latent action + * finished normally, false if it didn't.
+ * Example usage:
+ * co_await Latent::Chain(&UMediaPlayer::OpenSourceLatent, MediaPlayer, + * MediaSource, Options, bSuccess); */ +template Class, typename... FnParams> +Private::FLatentChainAwaiter Chain(auto (Class::*Function)(FnParams...), + Class* Object, auto&&... Args); +#endif + +/** Resumes the coroutine once the chained latent action has finished, + * with manual parameter matching.
+ * The result of the co_await expression is true if the chained latent action + * finished normally, false if it didn't.
+ * Use std::placeholders::_1 and _2 for the world context and LatentInfo.
+ * Example usage:
+ * co_await Latent::ChainEx(&UKismetSystemLibrary::Delay, _1, 1.0f, _2); */ +template +Private::FLatentChainAwaiter ChainEx(F&& Function, A&&... Args); + +#pragma endregion + +#pragma region Async loading + +/** Asynchronously starts loading the object, resumes once it's loaded.
+ * The result of the co_await expression is the loaded T*. */ +template +auto AsyncLoadObject(TSoftObjectPtr, + TAsyncLoadPriority = FStreamableManager::DefaultAsyncLoadPriority) + -> std::enable_if_t, + Private::TAsyncLoadAwaiter>; + +/** Asynchronously starts loading the objects, resumes once they're loaded.
+ * The result of the co_await expression is TArray. */ +template +auto AsyncLoadObjects(const TArray>&, + TAsyncLoadPriority = FStreamableManager::DefaultAsyncLoadPriority) + -> std::enable_if_t, + Private::TAsyncLoadAwaiter, 0>>; + +/** Asynchronously starts loading the objects at the given paths, + * resumes once they're loaded. The loaded objects are not resolved. */ +UE5CORO_API auto AsyncLoadObjects(TArray, + TAsyncLoadPriority = FStreamableManager::DefaultAsyncLoadPriority) + -> Private::FLatentAwaiter; + +/** Asynchronously starts loading the primary asset with any bundles specified, + * resumes once they're loaded.
+ * The asset will stay in memory until explicitly unloaded. */ +UE5CORO_API auto AsyncLoadPrimaryAsset(const FPrimaryAssetId& AssetToLoad, + const TArray& LoadBundles = {}, + TAsyncLoadPriority Priority = FStreamableManager::DefaultAsyncLoadPriority) + -> Private::FLatentAwaiter; + +/** Asynchronously starts loading the primary asset of the given type with any + * bundles specified, resumes once they're loaded.
+ * The asset will stay in memory until explicitly unloaded.
+ * The result of the co_await expression is the loaded T* or nullptr. */ +template +auto AsyncLoadPrimaryAsset(FPrimaryAssetId AssetToLoad, + const TArray& LoadBundles = {}, + TAsyncLoadPriority Priority = FStreamableManager::DefaultAsyncLoadPriority) + -> std::enable_if_t, + Private::TAsyncLoadAwaiter>; + +/** Asynchronously starts loading the primary assets with any bundles specified, + * resumes once they're loaded.
+ * The assets will stay in memory until explicitly unloaded. */ +UE5CORO_API auto AsyncLoadPrimaryAssets(TArray AssetsToLoad, + const TArray& LoadBundles = {}, + TAsyncLoadPriority Priority = FStreamableManager::DefaultAsyncLoadPriority) + -> Private::FLatentAwaiter; + +/** Asynchronously starts loading the primary assets of the given type with any + * bundles specified, resumes once they're loaded.
+ * The assets will stay in memory until explicitly unloaded.
+ * The result of the co_await expression is the loaded and filtered TArray. */ +template +auto AsyncLoadPrimaryAssets(TArray AssetsToLoad, + const TArray& LoadBundles = {}, + TAsyncLoadPriority Priority = FStreamableManager::DefaultAsyncLoadPriority) + -> std::enable_if_t, + Private::TAsyncLoadAwaiter, 1>>; + +/** Asynchronously starts loading the class, resumes once it's loaded.
+ * The result of the co_await expression is the loaded UClass*. */ +UE5CORO_API auto AsyncLoadClass(TSoftClassPtr, + TAsyncLoadPriority = FStreamableManager::DefaultAsyncLoadPriority) + -> Private::TAsyncLoadAwaiter; + +/** Asynchronously starts loading the classes, resumes once they're loaded.
+ * The result of the co_await expression is TArray. */ +UE5CORO_API auto AsyncLoadClasses(const TArray>&, + TAsyncLoadPriority = FStreamableManager::DefaultAsyncLoadPriority) + -> Private::TAsyncLoadAwaiter, 0>; + +/** Asynchronously starts loading the package, resumes once it's loaded.
+ * The result of the co_await expression is the UPackage*.
+ * For parameters see the engine function ::LoadPackageAsync(). */ +UE5CORO_API auto AsyncLoadPackage(const FString& Path, + const FGuid* Guid = nullptr, const TCHAR* PackageToLoadFrom = nullptr, + EPackageFlags PackageFlags = PKG_None, int32 PIEInstanceID = INDEX_NONE, + TAsyncLoadPriority PackagePriority = 0, + const FLinkerInstancingContext* InstancingContext = nullptr) + -> Private::FPackageLoadAwaiter; + +#pragma endregion + +#pragma region Async collision queries + +// Async UWorld queries. For parameters, see their originals in World.h. +// It's slightly more efficient to co_await rvalues of these instead of lvalues. + +UE5CORO_API Private::TAsyncQueryAwaiter AsyncLineTraceByChannel( + const UObject* WorldContextObject, EAsyncTraceType InTraceType, + const FVector& Start, const FVector& End, ECollisionChannel TraceChannel, + const FCollisionQueryParams& Params = FCollisionQueryParams::DefaultQueryParam, + const FCollisionResponseParams& ResponseParam = + FCollisionResponseParams::DefaultResponseParam); + +UE5CORO_API Private::TAsyncQueryAwaiter AsyncLineTraceByObjectType( + const UObject* WorldContextObject, EAsyncTraceType InTraceType, + const FVector& Start, const FVector& End, + const FCollisionObjectQueryParams& ObjectQueryParams, + const FCollisionQueryParams& Params = FCollisionQueryParams::DefaultQueryParam); + +UE5CORO_API Private::TAsyncQueryAwaiter AsyncLineTraceByProfile( + const UObject* WorldContextObject, EAsyncTraceType InTraceType, + const FVector& Start, const FVector& End, FName ProfileName, + const FCollisionQueryParams& Params = FCollisionQueryParams::DefaultQueryParam); + +UE5CORO_API Private::TAsyncQueryAwaiter AsyncSweepByChannel( + const UObject* WorldContextObject, EAsyncTraceType InTraceType, + const FVector& Start, const FVector& End, const FQuat& Rot, + ECollisionChannel TraceChannel, const FCollisionShape& CollisionShape, + const FCollisionQueryParams& Params = FCollisionQueryParams::DefaultQueryParam, + const FCollisionResponseParams& ResponseParam = + FCollisionResponseParams::DefaultResponseParam); + +UE5CORO_API Private::TAsyncQueryAwaiter AsyncSweepByObjectType( + const UObject* WorldContextObject, EAsyncTraceType InTraceType, + const FVector& Start, const FVector& End, const FQuat& Rot, + const FCollisionObjectQueryParams& ObjectQueryParams, + const FCollisionShape& CollisionShape, + const FCollisionQueryParams& Params = FCollisionQueryParams::DefaultQueryParam); + +UE5CORO_API Private::TAsyncQueryAwaiter AsyncSweepByProfile( + const UObject* WorldContextObject, EAsyncTraceType InTraceType, + const FVector& Start, const FVector& End, const FQuat& Rot, + FName ProfileName, const FCollisionShape& CollisionShape, + const FCollisionQueryParams& Params = FCollisionQueryParams::DefaultQueryParam); + +UE5CORO_API Private::TAsyncQueryAwaiter AsyncOverlapByChannel( + const UObject* WorldContextObject, const FVector& Pos, const FQuat& Rot, + ECollisionChannel TraceChannel, const FCollisionShape& CollisionShape, + const FCollisionQueryParams& Params = FCollisionQueryParams::DefaultQueryParam, + const FCollisionResponseParams& ResponseParam = + FCollisionResponseParams::DefaultResponseParam); + +UE5CORO_API Private::TAsyncQueryAwaiter AsyncOverlapByObjectType( + const UObject* WorldContextObject, const FVector& Pos, const FQuat& Rot, + const FCollisionObjectQueryParams& ObjectQueryParams, + const FCollisionShape& CollisionShape, + const FCollisionQueryParams& Params = FCollisionQueryParams::DefaultQueryParam); + +#pragma endregion +} + +namespace UE5Coro::Private +{ +class [[nodiscard]] UE5CORO_API FLatentCancellation final // not TAwaiter +{ +public: + bool await_ready() noexcept { return false; } + + template + auto await_suspend(stdcoro::coroutine_handle

Handle) + // co_awaiting this in async mode is meaningless, use co_return instead. + -> std::enable_if_t> + { + Handle.promise().CancelFromWithin(); + } + + void await_resume() noexcept { } +}; + +class [[nodiscard]] UE5CORO_API FLatentAwaiter // not TAwaiter +{ + void Suspend(FAsyncPromise&); + void Suspend(FLatentPromise&); + +protected: + void* State; + bool (*Resume)(void* State, bool bCleanup); + +public: + explicit FLatentAwaiter(void* State, bool (*Resume)(void*, bool)) noexcept + : State(State), Resume(Resume) { } + FLatentAwaiter(const FLatentAwaiter&) = delete; + FLatentAwaiter(FLatentAwaiter&&) noexcept; + ~FLatentAwaiter(); + + bool ShouldResume(); + + bool await_ready() { return ShouldResume(); } + + template + auto await_suspend(stdcoro::coroutine_handle

Handle) + -> std::enable_if_t> + { + Suspend(Handle.promise()); + } + + void await_resume() noexcept { } +}; + +namespace AsyncLoad +{ +template // Switches between non-exported types +UE5CORO_API TArray InternalResume(void*); +} + +template +class [[nodiscard]] TAsyncLoadAwaiter : public FLatentAwaiter +{ +public: + explicit TAsyncLoadAwaiter(FLatentAwaiter&& Other) noexcept + : FLatentAwaiter(std::move(Other)) { } + TAsyncLoadAwaiter(TAsyncLoadAwaiter&&) noexcept = default; + + T await_resume() + { + TArray Assets = AsyncLoad::InternalResume(State); + if constexpr (TIsTArray::Value) + { + static_assert(std::is_pointer_v); + using V = std::remove_pointer_t; + static_assert(std::is_base_of_v); + static_assert(!std::is_const_v); + [[maybe_unused]] int OldNum = Assets.Num(); + Assets.RemoveAll([](UObject* Ptr) { return !Ptr->IsA(); }); + if constexpr (HiddenType == 0) // These come from typed soft ptrs + check(Assets.Num() == OldNum); // Strict check + return std::move(*std::launder(reinterpret_cast(&Assets))); + } + else + { + static_assert(std::is_pointer_v); + using V = std::remove_pointer_t; + static_assert(std::is_base_of_v); + checkf(Assets.Num() <= 1, + TEXT("Unexpected multiple assets for single load")); + return Assets.IsValidIndex(0) ? Cast(Assets[0]) : nullptr; + } + } +}; + +static_assert(sizeof(FLatentAwaiter) == + sizeof(TAsyncLoadAwaiter)); +static_assert(sizeof(FLatentAwaiter) == + sizeof(TAsyncLoadAwaiter, 0>)); + +class [[nodiscard]] UE5CORO_API FPackageLoadAwaiter + : public TAwaiter +{ + struct FState + { + FPromise* Promise = nullptr; + TStrongObjectPtr Result; // This might be carried across co_awaits + void Loaded(const FName&, UPackage*, EAsyncLoadingResult::Type); + }; + TSharedPtr State; + +public: + explicit FPackageLoadAwaiter( + const FString& Path, const FGuid* Guid, const TCHAR* Package, + EPackageFlags PackageFlags, int32 PIEInstanceID, + TAsyncLoadPriority PackagePriority, + const FLinkerInstancingContext* InstancingContext); + + bool await_ready(); + void Suspend(FPromise&); + UPackage* await_resume(); +}; + +template +class [[nodiscard]] UE5CORO_API TAsyncQueryAwaiter + : public TAwaiter> +{ + class TImpl; + TSharedPtr Impl; + +public: + template + explicit TAsyncQueryAwaiter(UWorld*, FTraceHandle (UWorld::*)(P...), A...); + ~TAsyncQueryAwaiter(); + + // Workaround for not being able to rvalue overload await_resume + TAsyncQueryAwaiter& operator co_await() &; + TAsyncQueryAwaiterRV& operator co_await() &&; + + bool await_ready(); + void Suspend(FPromise&); + const TArray& await_resume(); +}; + +template +class [[nodiscard]] UE5CORO_API TAsyncQueryAwaiterRV + : public TAsyncQueryAwaiter +{ +public: + TAsyncQueryAwaiterRV() = delete; // Objects of this type are never created + TArray await_resume(); +}; +} + +inline UE5Coro::Private::FLatentCancellation UE5Coro::Latent::Cancel() noexcept +{ + return {}; +} + +template +auto UE5Coro::Latent::UntilDelegate(T& Delegate) + -> std::enable_if_t, Private::FLatentAwaiter> +{ + using namespace UE5Coro::Private; + auto [Awaiter, Target] = UntilDelegateCore(); + + if constexpr (TIsMulticastDelegate) + { + if constexpr (TIsDynamicDelegate) + { + FScriptDelegate D; + D.BindUFunction(Target, NAME_Core); + Delegate.Add(D); + } + else + Delegate.AddUFunction(Target, NAME_Core); + } + else + Delegate.BindUFunction(Target, NAME_Core); + + return std::move(Awaiter); +} + +template +auto UE5Coro::Latent::AsyncLoadObject(TSoftObjectPtr Ptr, + TAsyncLoadPriority Priority) + -> std::enable_if_t, + Private::TAsyncLoadAwaiter> +{ + return Private::TAsyncLoadAwaiter( + AsyncLoadObjects(TArray{Ptr.ToSoftObjectPath()}, Priority)); +} + +template +auto UE5Coro::Latent::AsyncLoadObjects(const TArray>& Ptrs, + TAsyncLoadPriority Priority) + -> std::enable_if_t, + Private::TAsyncLoadAwaiter, 0>> +{ + TArray Paths; + Paths.Reserve(Ptrs.Num()); + for (const auto& Ptr : Ptrs) + Paths.Add(Ptr.ToSoftObjectPath()); + + return Private::TAsyncLoadAwaiter, 0>( + AsyncLoadObjects(std::move(Paths), Priority)); +} + +template +auto UE5Coro::Latent::AsyncLoadPrimaryAsset(FPrimaryAssetId AssetToLoad, + const TArray& LoadBundles, + TAsyncLoadPriority Priority) + -> std::enable_if_t, + Private::TAsyncLoadAwaiter> +{ + return Private::TAsyncLoadAwaiter( + AsyncLoadPrimaryAsset(std::move(AssetToLoad), LoadBundles, Priority)); +} + +template +auto UE5Coro::Latent::AsyncLoadPrimaryAssets(TArray AssetsToLoad, + const TArray& LoadBundles, + TAsyncLoadPriority Priority) + -> std::enable_if_t, + Private::TAsyncLoadAwaiter, 1>> +{ + return Private::TAsyncLoadAwaiter, 1>( + AsyncLoadPrimaryAssets(std::move(AssetsToLoad), LoadBundles, Priority)); +} + +#include "LatentChain.inl" diff --git a/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/LatentCallbacks.h b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/LatentCallbacks.h new file mode 100644 index 00000000..055e92e7 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/LatentCallbacks.h @@ -0,0 +1,88 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#include +#include "Misc/ScopeExit.h" + +namespace UE5Coro::Latent +{ +/** + * Provided for advanced scenarios, prefer ON_SCOPE_EXIT or RAII for + * unconditional cleanup, or FOnCoroutineCanceled for a generic handler.

+ * This is a combination of FOnActionAborted and FOnObjectDestroyed and will + * ONLY run the provided callback in either of those two situations.
+ * It has no effect within an async mode coroutine. Note that because + * NotifyObjectDestroyed is included, `this` might not be valid.
+ *
Example usage:
+ * Latent::FOnAbnormalExit Guard([]{cleanup code}); + */ +struct [[nodiscard]] UE5CORO_API FOnAbnormalExit + : ScopeExitSupport::TScopeGuard> +{ + explicit FOnAbnormalExit(std::function Fn); +}; + +/** + * Provided for advanced scenarios, prefer ON_SCOPE_EXIT or RAII for + * unconditional cleanup.

+ * This will ONLY call the provided callback if this object is in scope within + * a latent mode coroutine that's aborted by the latent action manager. + * It has no effect within an async mode coroutine.
+ *
Example usage:
+ * Latent::FOnActionAborted Guard([this]{cleanup code}); + * @see FPendingLatentAction::NotifyActionAborted() + */ +struct [[nodiscard]] UE5CORO_API FOnActionAborted + : ScopeExitSupport::TScopeGuard> +{ + explicit FOnActionAborted(std::function Fn); +}; + +/** + * Provided for advanced scenarios, prefer ON_SCOPE_EXIT or RAII for + * unconditional cleanup.

+ * This will ONLY call the provided callback if this object is in scope within + * a latent mode coroutine whose object has been garbage collected. + * It has no effect within an async mode coroutine.
+ *
Example usage:
+ * Latent::FOnObjectDestroyed Guard([]{cleanup code}); + * @see FPendingLatentAction::NotifyObjectDestroyed() + */ +struct [[nodiscard]] UE5CORO_API FOnObjectDestroyed + : ScopeExitSupport::TScopeGuard> +{ + explicit FOnObjectDestroyed(std::function Fn); +}; +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/LatentChain.inl b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/LatentChain.inl new file mode 100644 index 00000000..a29ea4e7 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/LatentChain.inl @@ -0,0 +1,199 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#if UE5CORO_CPP20 +#include +#endif +#include +#include "Engine/LatentActionManager.h" +#include "Engine/World.h" +#include "UE5Coro/UE5CoroSubsystem.h" + +namespace UE5Coro::Private +{ +UE5CORO_API std::tuple MakeLatentInfo(); + +class [[nodiscard]] UE5CORO_API FLatentChainAwaiter : public FLatentAwaiter +{ +public: + explicit FLatentChainAwaiter(FTwoLives* Done) noexcept; + FLatentChainAwaiter(FLatentChainAwaiter&&) noexcept = default; + bool await_resume(); +}; + +static_assert(sizeof(FLatentAwaiter) == sizeof(FLatentChainAwaiter)); + +#if UE5CORO_CPP20 && !UE5CORO_PRIVATE_LIBCPP_IS_BROKEN +template +concept TWorldContext = std::same_as, UObject*> || + std::same_as, const UObject*> || + std::same_as, UWorld*> || + std::same_as, const UWorld*>; + +template +concept TLatentInfo = std::same_as, FLatentActionInfo>; + +template +using TForwardRef = + std::conditional_t, + std::reference_wrapper>, T&&>; + +// Terminator +template +struct FLatentChain +{ + static void Call(auto&& Fn, FLatentActionInfo, auto&&... Args) + { + static_assert(!bInfo, "Chained function is not latent"); + static_assert(sizeof...(Args) == 0, + "Too many parameters provided for chained call"); + // This one last forward is needed to forward &&s all the way through + std::forward(Fn)(); + } +}; + +// World context +template +struct FLatentChain +{ + static void Call(auto&& Fn, FLatentActionInfo LatentInfo, auto&&... Args) + { + checkf(GWorld, TEXT("Could not chain latent action: no world found")); + FLatentChain::Call( + std::bind_front(std::move(Fn), &*GWorld), + LatentInfo, + TForwardRef(Args)...); + } +}; + +// LatentInfo +template +struct FLatentChain +{ + static void Call(auto&& Fn, FLatentActionInfo LatentInfo, auto&&... Args) + { + FLatentChain::Call( + std::bind_front(std::move(Fn), LatentInfo), + LatentInfo, + TForwardRef(Args)...); + } +}; + +// Everything else +template +struct FLatentChain +{ + static void Call(auto&&, FLatentActionInfo) + { + static_assert(false && bWorld, // This needs to depend on a template param + "Not enough parameters provided for chained call"); + } + + static void Call(auto&& Fn, FLatentActionInfo LatentInfo, + Type&& Arg1, auto&&... Args) + { + FLatentChain::Call( + std::bind_front(std::move(Fn), TForwardRef(Arg1)), + LatentInfo, + TForwardRef(Args)...); + } +}; +#endif +} + +namespace UE5Coro::Latent +{ +#if UE5CORO_CPP20 && !UE5CORO_PRIVATE_LIBCPP_IS_BROKEN + +#if defined(_MSC_VER) && _MSC_VER < 1930 +#define UE5CORO_PRIVATE_LATENT_CHAIN_IS_OK 0 +#define UE5CORO_PRIVATE_LATENT_CHAIN_BUG_MSG \ +[[deprecated("Old versions of MSVC are known to have codegen issues with Chain. " \ +"Consider updating to something less broken, or using ChainEx as a workaround.")]] +#else +#define UE5CORO_PRIVATE_LATENT_CHAIN_IS_OK 1 +#define UE5CORO_PRIVATE_LATENT_CHAIN_BUG_MSG +#endif + +template +UE5CORO_PRIVATE_LATENT_CHAIN_BUG_MSG +Private::FLatentChainAwaiter Chain(auto (*Function)(FnParams...), auto&&... Args) +{ + checkf(IsInGameThread(), + TEXT("Latent awaiters may only be used on the game thread")); + auto [LatentInfo, Done] = Private::MakeLatentInfo(); + Private::FLatentChain::Call( + Function, + LatentInfo, + std::forward(Args)...); + return Private::FLatentChainAwaiter(Done); +} + +template Class, typename... FnParams> +UE5CORO_PRIVATE_LATENT_CHAIN_BUG_MSG +Private::FLatentChainAwaiter Chain(auto (Class::*Function)(FnParams...), + Class* Object, auto&&... Args) +{ + checkf(IsInGameThread(), + TEXT("Latent awaiters may only be used on the game thread")); + auto [LatentInfo, Done] = Private::MakeLatentInfo(); + Private::FLatentChain::Call( + std::bind_front(Function, Object), + LatentInfo, + std::forward(Args)...); + return Private::FLatentChainAwaiter(Done); +} +#else +// C++17: not available +#define UE5CORO_PRIVATE_LATENT_CHAIN_IS_OK 0 +#endif + +template +Private::FLatentChainAwaiter ChainEx(F&& Function, A&&... Args) +{ + checkf(IsInGameThread(), + TEXT("Latent awaiters may only be used on the game thread")); + if constexpr ((... || (std::is_placeholder_v> == 1))) + checkf(GWorld, + TEXT("Could not chain latent action: no world found for _1")); + static_assert((... || (std::is_placeholder_v> == 2)), + "The _2 parameter for LatentInfo is mandatory"); + + auto [LatentInfo, Done] = Private::MakeLatentInfo(); + std::bind(std::forward(Function), + std::forward
(Args)...)(&*GWorld, LatentInfo); + return Private::FLatentChainAwaiter(Done); +} +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/LatentTimeline.h b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/LatentTimeline.h new file mode 100644 index 00000000..63c24ea4 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/LatentTimeline.h @@ -0,0 +1,67 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#include +#include "UE5Coro/Coroutine.h" + +namespace UE5Coro::Latent +{ +/** Repeatedly calls the provided function with linearly-interpolated values. */ +UE5CORO_API TCoroutine<> Timeline(const UObject* WorldContextObject, + float From, float To, float Length, + std::function Fn, + bool bRunWhenPaused = false); + +/** Repeatedly calls the provided function with linearly-interpolated values.
+ * This is affected by time dilation only, NOT pause. */ +UE5CORO_API TCoroutine<> UnpausedTimeline(const UObject* WorldContextObject, + float From, float To, float Length, + std::function Fn, + bool bRunWhenPaused = true); + +/** Repeatedly calls the provided function with linearly-interpolated values.
+ * This is not affected by pause or time dilation. */ +UE5CORO_API TCoroutine<> RealTimeline(const UObject* WorldContextObject, + float From, float To, float Length, + std::function Fn, + bool bRunWhenPaused = true); + +/** Repeatedly calls the provided function with linearly-interpolated values.
+ * This is affected by pause only, NOT time dilation. */ +UE5CORO_API TCoroutine<> AudioTimeline(const UObject* WorldContextObject, + float From, float To, float Length, + std::function Fn, + bool bRunWhenPaused = false); +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Private.h b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Private.h new file mode 100644 index 00000000..f2e781cd --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Private.h @@ -0,0 +1,76 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#include +#include +#include "Delegates/DelegateBase.h" + +/****************************************************************************** + * This file only contains private implementation details. * + ******************************************************************************/ + +namespace UE5Coro::Private +{ +// On Windows, both std::mutex and std::shared_mutex are SRWLOCKs, but mutex +// has extra padding for ABI compatibility. Prefer shared_mutex for now. +#ifdef _MSVC_STL_VERSION +using FMutex = std::conditional_t; +#else +using FMutex = std::mutex; +#endif + +template +constexpr bool TIsSparseDelegate = std::is_base_of_v; + +template +constexpr bool TIsDynamicDelegate = + std::is_base_of_v || + std::is_base_of_v || + // Sparse delegates are always dynamic multicast + TIsSparseDelegate; + +template +constexpr bool TIsMulticastDelegate = + std::is_base_of_v || + std::is_base_of_v, T> || + // Sparse delegates are always dynamic multicast + TIsSparseDelegate; + +template +constexpr bool TIsDelegate = + std::is_base_of_v || + TIsDynamicDelegate || TIsMulticastDelegate; +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Threading.h b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Threading.h new file mode 100644 index 00000000..6dd9ecf1 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/Threading.h @@ -0,0 +1,125 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#include "UE5Coro/AsyncCoroutine.h" +#include "UE5Coro/Private.h" + +namespace UE5Coro +{ +namespace Private +{ +struct FAwaitingPromise; +} +/** + * Awaitable event. co_awaiting this object suspends the coroutine if the event + * is not triggered, and resumes it at the next call to Trigger(). + * AutoReset events will only resume one awaiter, ManualReset all of them. + */ +class UE5CORO_API FAwaitableEvent final + : public Private::TAwaiter +{ + const EEventMode Mode; + + Private::FMutex Lock; + bool bActive; + Private::FAwaitingPromise* Awaiters = nullptr; + +public: + /** Initializes this event to be in the given mode and state. */ + explicit FAwaitableEvent(EEventMode Mode = EEventMode::AutoReset, + bool bInitialState = false); + UE_NONCOPYABLE(FAwaitableEvent); +#if UE5CORO_DEBUG + ~FAwaitableEvent(); +#endif + + /** Resumes one or more coroutines awaiting this event, depending on Mode. */ + void Trigger(); + /** Clears this event, making subsequent co_awaits suspend. */ + void Reset(); + /** @return true if this object was made as ManualReset. */ + [[nodiscard]] bool IsManualReset() const; + + bool await_ready() noexcept; + void Suspend(Private::FPromise&); + void await_resume() noexcept { } + +private: + void ResumeOne(); + void TryResumeAll(); +}; + +/** + * Awaitable semaphore. co_awaiting this object will attempt to lock/acquire one + * count and suspend the coroutine if this was not possible, resuming it when + * the semaphore is next Unlock()ed (released). + */ +class UE5CORO_API FAwaitableSemaphore final + : public Private::TAwaiter +{ + const int Capacity; + Private::FMutex Lock; + int Count; + Private::FAwaitingPromise* Awaiters = nullptr; + +public: + /** Initializes the semaphore to the given capacity and initial count.
+ * Defaults to being an unlocked mutex. */ + explicit FAwaitableSemaphore(int Capacity = 1, int InitialCount = 1); + UE_NONCOPYABLE(FAwaitableSemaphore); +#if UE5CORO_DEBUG + ~FAwaitableSemaphore(); +#endif + + /** Unlocks (releases) the semaphore the specified amount of times. */ + void Unlock(int Count = 1); + + bool await_ready(); + void Suspend(Private::FPromise&); + void await_resume() noexcept { } + +private: + void TryResumeAll(); +}; + +namespace Private +{ +struct FAwaitingPromise +{ + FPromise* Promise; + FAwaitingPromise* Next; +}; +} +} diff --git a/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/UE5CoroSubsystem.h b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/UE5CoroSubsystem.h new file mode 100644 index 00000000..56ca7b97 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/Public/UE5Coro/UE5CoroSubsystem.h @@ -0,0 +1,87 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#include "Engine/LatentActionManager.h" +#include "Subsystems/WorldSubsystem.h" +#include "UE5CoroSubsystem.generated.h" + +namespace UE5Coro::Private +{ +class [[nodiscard]] UE5CORO_API FTwoLives +{ + std::atomic RefCount = 2; + +public: + int UserData = 0; + + bool Release(); // Dangerous! Only call externally exactly once! + + // Generic implementation for FLatentAwaiter + static bool ShouldResume(void* State, bool bCleanup); +}; +} + +/** + * Subsystem supporting some async coroutine functionality.
+ * You never need to interact with it directly. + */ +UCLASS() +class UE5CORO_API UUE5CoroSubsystem final : public UTickableWorldSubsystem +{ + GENERATED_BODY() + + UPROPERTY() + TMap ChainCallbackTargets; + int32 NextLinkage = 0; + FDelegateHandle LatentActionsChangedHandle; + +public: + /** Creates a unique LatentInfo that does not lead anywhere. */ + FLatentActionInfo MakeLatentInfo(); + + /** Creates a LatentInfo suitable for the Latent::Chain* functions. */ + FLatentActionInfo MakeLatentInfo(UE5Coro::Private::FTwoLives* State); + +#pragma region UTickableWorldSubsystem overrides + virtual void Deinitialize() override; + virtual bool IsTickableWhenPaused() const override { return true; } + virtual bool IsTickableInEditor() const override { return true; } + virtual void Tick(float DeltaTime) override; + virtual TStatId GetStatId() const override; +#pragma endregion + +private: + void LatentActionsChanged(UObject* Object, ELatentActionChangeType Change); +}; diff --git a/Plugins/UE5Coro/Source/UE5Coro/UE5Coro.Build.cs b/Plugins/UE5Coro/Source/UE5Coro/UE5Coro.Build.cs new file mode 100644 index 00000000..affc0442 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5Coro/UE5Coro.Build.cs @@ -0,0 +1,63 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using UnrealBuildTool; + +public class UE5Coro : UE5CoroModuleRules +{ + public UE5Coro(ReadOnlyTargetRules Target) + : base(Target) + { + PublicDependencyModuleNames.AddRange(new[] + { + "HTTP", + }); + } +} + +public abstract class UE5CoroModuleRules : ModuleRules +{ + protected UE5CoroModuleRules(ReadOnlyTargetRules Target) + : base(Target) + { + CppStandard = CppStandardVersion.Latest; + PublicDefinitions.Add("UE5CORO_CPP20=1"); + + bUseUnity = false; + + PublicDependencyModuleNames.AddRange(new[] + { + "Core", + "CoreUObject", + "Engine", + }); + } +} diff --git a/Plugins/UE5Coro/Source/UE5CoroK2/Private/K2Node_UE5CoroCallCoroutine.cpp b/Plugins/UE5Coro/Source/UE5CoroK2/Private/K2Node_UE5CoroCallCoroutine.cpp new file mode 100644 index 00000000..c8eee369 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroK2/Private/K2Node_UE5CoroCallCoroutine.cpp @@ -0,0 +1,103 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "K2Node_UE5CoroCallCoroutine.h" +#include "BlueprintActionDatabaseRegistrar.h" +#include "BlueprintNodeSpawner.h" +#include "EdGraphSchema_K2.h" +#include "UE5Coro/AsyncCoroutine.h" +#include "UObject/Class.h" +#include "UObject/Field.h" +#include "UObject/UnrealType.h" +#include "UObject/UObjectIterator.h" + +#define LOCTEXT_NAMESPACE "UE5Coro" + +void UK2Node_UE5CoroCallCoroutine::CustomizeNode(UEdGraphNode* NewNode, bool, + UFunction* Function) +{ + auto* This = CastChecked(NewNode); + This->SetFromFunction(Function); +} + +void UK2Node_UE5CoroCallCoroutine::GetMenuActions( + FBlueprintActionDatabaseRegistrar& BlueprintActionDatabaseRegistrar) const +{ + auto* Struct = FAsyncCoroutine::StaticStruct(); + // Sign up for every BPCallable UFUNCTION that returns a FAsyncCoroutine + for (auto* Fn : TObjectRange()) + if (auto* Return = CastField(Fn->GetReturnProperty()); + UNLIKELY(Return && Return->Struct == Struct)) + { + if (!Fn->HasAllFunctionFlags(FUNC_BlueprintCallable)) + continue; + + // Patch the UFUNCTION to hide the regular function call + Fn->SetMetaData(FBlueprintMetadata::MD_BlueprintInternalUseOnly, + TEXT("true")); + + // Sign up to create a coroutine call K2Node instead + auto* BNS = UBlueprintNodeSpawner::Create(GetClass()); + BNS->CustomizeNodeDelegate.BindWeakLambda( + Fn, &ThisClass::CustomizeNode, Fn); + + auto& Menu = BNS->DefaultMenuSignature; + Menu.MenuName = Fn->GetDisplayNameText(); + Menu.Category = GetDefaultCategoryForFunction(Fn, FText::GetEmpty()); + if (Menu.Category.IsEmpty()) + Menu.Category = LOCTEXT("CallCoroutine", "Call Coroutine"); + Menu.Tooltip = FText::FromString(GetDefaultTooltipForFunction(Fn)); + Menu.Keywords = GetKeywordsForFunction(Fn); + Menu.Icon = GetIconAndTint(Menu.IconTint); + Menu.DocLink = GetDocumentationLink(); + Menu.DocExcerptTag = GetDocumentationExcerptName(); + + BlueprintActionDatabaseRegistrar.AddBlueprintAction(Struct, BNS); + } +} + +void UK2Node_UE5CoroCallCoroutine::PostParameterPinCreated(UEdGraphPin* Pin) +{ + Super::PostParameterPinCreated(Pin); + + UObject* Type = Pin->PinType.PinSubCategoryObject.Get(); + + // Is this an output FAsyncCoroutine pin? + if (Pin->Direction == EGPD_Output && Type == FAsyncCoroutine::StaticStruct()) + Pin->SafeSetHidden(true); + + // Is this an input FForceLatentCoroutine pin? + if (Pin->Direction == EGPD_Input && + Type == FForceLatentCoroutine::StaticStruct()) + Pin->SafeSetHidden(true); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/UE5Coro/Source/UE5CoroK2/Private/K2Node_UE5CoroCallCoroutine.h b/Plugins/UE5Coro/Source/UE5CoroK2/Private/K2Node_UE5CoroCallCoroutine.h new file mode 100644 index 00000000..26b1591d --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroK2/Private/K2Node_UE5CoroCallCoroutine.h @@ -0,0 +1,50 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "K2Node_CallFunction.h" +#include "K2Node_UE5CoroCallCoroutine.generated.h" + +/** A minor cosmetic alteration of the regular "Call Function" node, hiding the + * useless return value of coroutines. */ +UCLASS() +class UE5COROK2_API UK2Node_UE5CoroCallCoroutine : public UK2Node_CallFunction +{ + GENERATED_BODY() + + static void CustomizeNode(UEdGraphNode*, bool, UFunction*); + +public: + virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar&) const override; + virtual void PostParameterPinCreated(UEdGraphPin*) override; +}; diff --git a/Plugins/UE5Coro/Source/UE5CoroK2/Private/UE5CoroK2.cpp b/Plugins/UE5Coro/Source/UE5CoroK2/Private/UE5CoroK2.cpp new file mode 100644 index 00000000..898cc079 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroK2/Private/UE5CoroK2.cpp @@ -0,0 +1,38 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "Modules/ModuleManager.h" + +class FUE5CoroK2Module : public IModuleInterface +{ +}; + +IMPLEMENT_MODULE(FUE5CoroK2Module, UE5CoroK2); diff --git a/Plugins/UE5Coro/Source/UE5CoroK2/UE5CoroK2.Build.cs b/Plugins/UE5Coro/Source/UE5CoroK2/UE5CoroK2.Build.cs new file mode 100644 index 00000000..aaa999df --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroK2/UE5CoroK2.Build.cs @@ -0,0 +1,45 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using UnrealBuildTool; + +public class UE5CoroK2 : UE5CoroModuleRules +{ + public UE5CoroK2(ReadOnlyTargetRules Target) + : base(Target) + { + PublicDependencyModuleNames.AddRange(new[] + { + "BlueprintGraph", + "UE5Coro", + }); + } +} diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/Private/AggregateAwaiterTest.cpp b/Plugins/UE5Coro/Source/UE5CoroTests/Private/AggregateAwaiterTest.cpp new file mode 100644 index 00000000..6cd876da --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/Private/AggregateAwaiterTest.cpp @@ -0,0 +1,300 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include +#include "TestWorld.h" +#include "Misc/AutomationTest.h" +#include "UE5Coro/AggregateAwaiters.h" +#include "UE5Coro/CoroutineAwaiters.h" +#include "UE5Coro/Threading.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private::Test; + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FAggregateAsyncTest, "UE5Coro.Aggregate.Async", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FAggregateLatentTest, "UE5Coro.Aggregate.Latent", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +namespace +{ +template +void DoTest(FAutomationTestBase& Test) +{ + FTestWorld World; + + { + int State = 0; + World.Run(CORO + { + ++State; + auto A = World.Run(CORO + { + ++State; + co_await Latent::NextTick(); + ++State; + }); + auto B = World.Run(CORO + { + ++State; + co_await Latent::Ticks(2); + ++State; + }); + ++State; + co_await WhenAll(A, B); + ++State; + }); + World.EndTick(); + Test.TestEqual("Initial state", State, 4); // All 3 coroutines suspended + World.Tick(); + Test.TestEqual("First tick", State, 5); // A resumed + World.Tick(); + Test.TestEqual("Second tick", State, 7); // B and outer resumed + World.Tick(); + } + + { + int State = 0; + std::optional First; + World.Run(CORO + { + ++State; + auto A = World.Run(CORO + { + ++State; + co_await Latent::NextTick(); + ++State; + }); + auto B = World.Run(CORO + { + ++State; + co_await Latent::Ticks(2); + ++State; + }); + ++State; + First = co_await WhenAny(A, B); + ++State; + }); + World.EndTick(); + Test.TestEqual("Initial state", State, 4); // All 3 coroutines suspended + Test.TestFalse("Not resumed yet", First.has_value()); + World.Tick(); + Test.TestEqual("First tick", State, 6); // A and outer resumed + Test.TestEqual("Resumer index", *First, 0); + World.Tick(); + Test.TestEqual("Second tick", State, 7); // B resumed + World.Tick(); + } + + { + std::optional First; + World.Run(CORO + { + auto A = World.Run(CORO { co_await Latent::Ticks(3); }); + auto B = World.Run(CORO { co_await Latent::Ticks(4); }); + auto C = World.Run(CORO { co_await Latent::Ticks(1); }); + auto D = World.Run(CORO { co_await Latent::Ticks(2); }); + First = co_await WhenAny(A, B, C, D); + }); + World.EndTick(); + Test.TestFalse("Not resumed yet", First.has_value()); + World.Tick(); + Test.TestEqual("Resumer index", *First, 2); + World.Tick(); + } + + { + std::optional First; + World.Run(CORO + { + auto A = Latent::Ticks(1); + auto B = Latent::Ticks(2); + co_await Latent::Ticks(3); + First = co_await WhenAny(std::move(A), std::move(B)); + }); + World.EndTick(); + Test.TestFalse("Not resumed yet", First.has_value()); + World.Tick(); + Test.TestFalse("Not resumed yet", First.has_value()); + World.Tick(); + Test.TestFalse("Not resumed yet", First.has_value()); + World.Tick(); + Test.TestEqual("Resumer index", *First, 0); + World.Tick(); + } + + { + std::optional First; + World.Run(CORO + { + auto A = Latent::Ticks(1); + auto B = Latent::Ticks(2); + First = co_await WhenAny(std::move(A), std::move(B)); + }); + World.EndTick(); + Test.TestFalse("Not resumed yet", First.has_value()); + World.Tick(); + Test.TestEqual("Resumer index", *First, 0); + World.Tick(); + } + + { + int State = 0; + World.Run(CORO + { + auto A = Latent::Ticks(1); + auto B = Latent::Ticks(2); + auto C = Latent::Ticks(3); + auto D = Latent::Ticks(4); + auto E = WhenAll(std::move(A), std::move(C)); + auto F = WhenAny(std::move(B), std::move(D)); + auto E2 = E; + auto F2 = F; + State = 1; + co_await WhenAll(std::move(E2), std::move(F2)); + State = 2; + }); + World.EndTick(); + Test.TestEqual(TEXT("Initial state"), State, 1); + World.Tick(); + Test.TestEqual(TEXT("Hasn't resumed yet"), State, 1); + World.Tick(); + Test.TestEqual(TEXT("Hasn't resumed yet"), State, 1); + World.Tick(); + Test.TestEqual(TEXT("Resumed"), State, 2); + World.Tick(); + } + + { + int State = 0; + auto Coro = World.Run(CORO_R(int) + { + auto A = World.Run(CORO + { + ON_SCOPE_EXIT { State = 2; }; + co_await Latent::Ticks(5); + for (;;) + co_await Latent::NextTick(); + }); + + auto B = World.Run(CORO + { + ON_SCOPE_EXIT { State = 1; }; + co_await Latent::NextTick(); + }); + + co_return co_await Race(A, B); + }); + World.EndTick(); + World.Tick(); // NextTick + Test.TestEqual(TEXT("State"), State, 1); + World.Tick(); // A needs to poll to process the cancellation from Race + IF_NOT_CORO_LATENT + { + // Only latent->latent awaits poll, async needs all 5 ticks + World.Tick(); + World.Tick(); + Test.TestEqual(TEXT("State"), State, 1); + World.Tick(); + } + Test.TestEqual(TEXT("State"), State, 2); + Test.TestEqual(TEXT("Return value"), Coro.GetResult(), 1); + } + +#if UE5CORO_CPP20 + { + int State = 0; + FAwaitableEvent Event(EEventMode::ManualReset); + World.Run(CORO + { + TArray> Coros; + for (int i = 0; i < 10; ++i) + Coros.Add(World.Run(CORO + { + ++State; + co_await Event; + ++State; + })); + Test.TestEqual(TEXT("Initial state inside"), State, 10); + co_await WhenAll(Coros); + Test.TestEqual(TEXT("Final state inside"), State, 20); + ++State; + }); + Test.TestEqual(TEXT("Initial state outside"), State, 10); + Event.Trigger(); + Test.TestEqual(TEXT("Final state outside"), State, 21); + } + + { + int State = 0; + FAwaitableEvent Event(EEventMode::AutoReset); + World.Run(CORO + { + TArray> Coros; + for (int i = 0; i < 10; ++i) + Coros.Add(World.Run(CORO + { + ++State; + co_await Event; + ++State; + })); + Test.TestEqual(TEXT("Initial state inside"), State, 10); + co_await WhenAny(Coros); + Test.TestEqual(TEXT("Final state inside"), State, 11); + ++State; + }); + Test.TestEqual(TEXT("Initial state outside"), State, 10); + for (int i = 0; i < 10; ++i) + { + Event.Trigger(); + Test.TestEqual(TEXT("State outside"), State, i + 12); + } + } +#endif +} +} + +bool FAggregateAsyncTest::RunTest(const FString& Parameters) +{ + DoTest<>(*this); + return true; +} + +bool FAggregateLatentTest::RunTest(const FString& Parameters) +{ + DoTest(*this); + return true; +} diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/Private/AnimationAwaiterTest.cpp b/Plugins/UE5Coro/Source/UE5CoroTests/Private/AnimationAwaiterTest.cpp new file mode 100644 index 00000000..9a0b1935 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/Private/AnimationAwaiterTest.cpp @@ -0,0 +1,54 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5Coro/AnimationAwaiters.h" + +using namespace UE5Coro; + +TCoroutine<> UE5CoroAnimationAwaiterCompileTest() +{ + // Not actual tests (that would require assets), but compiling this function + // tests co_await result types and that everything is properly exported. + using namespace UE5Coro::Anim; + using FAnimTuple = TTuple; + // Using {} for the extra strictness + [[maybe_unused]] bool val1{co_await MontageBlendingOut(nullptr, nullptr)}; + [[maybe_unused]] bool val2{co_await MontageEnded(nullptr, nullptr)}; + co_await NextNotify(nullptr, NAME_None); + [[maybe_unused]] FAnimTuple val3{ + co_await PlayMontageNotifyBegin(nullptr, nullptr)}; + [[maybe_unused]] const FBranchingPointNotifyPayload* val4{ + co_await PlayMontageNotifyBegin(nullptr, nullptr, NAME_None)}; + [[maybe_unused]] FAnimTuple val5{ + co_await PlayMontageNotifyEnd(nullptr, nullptr)}; + [[maybe_unused]] const FBranchingPointNotifyPayload* val6{ + co_await PlayMontageNotifyEnd(nullptr, nullptr, NAME_None)}; +} diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/Private/AsyncAwaiterTest.cpp b/Plugins/UE5Coro/Source/UE5CoroTests/Private/AsyncAwaiterTest.cpp new file mode 100644 index 00000000..0966c5aa --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/Private/AsyncAwaiterTest.cpp @@ -0,0 +1,229 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "TestWorld.h" +#include "Misc/AutomationTest.h" +#include "UE5Coro/AggregateAwaiters.h" +#include "UE5Coro/AsyncAwaiters.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private; +using namespace UE5Coro::Private::Test; + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FAsyncAwaiterTest, "UE5Coro.Async.TrueAsync", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FAsyncInLatentTest, "UE5Coro.Async.Latent", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FAsyncStressTest, "UE5Coro.Async.Stress", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::MediumPriority | + EAutomationTestFlags::ProductFilter) + +namespace +{ +template +void DoTest(FAutomationTestBase& Test) +{ + FTestWorld World; + + IF_CORO_LATENT + { + FEventRef TestToCoro(EEventMode::AutoReset); + FEventRef CoroToTest(EEventMode::AutoReset); + bool bStarted = false; + bool bDone = false; + World.Run(CORO + { + bStarted = true; + co_await UE5Coro::Async::MoveToThread(ENamedThreads::AnyThread); + TestToCoro->Wait(); + bDone = true; + co_await UE5Coro::Async::MoveToGameThread(); + CoroToTest->Trigger(); + }); + Test.TestTrue(TEXT("Started"), bStarted); + Test.TestFalse(TEXT("Not done yet 1"), bDone); + TestToCoro->Trigger(); + + // This test is running on the game thread so MoveToGameThread() needs + // a little help + FTestHelper::PumpGameThread(World, [&] { return CoroToTest->Wait(0); }); + Test.TestTrue(TEXT("Done"), bDone); + } + + { + FEventRef TestToCoro(EEventMode::AutoReset); + FEventRef CoroToTest(EEventMode::AutoReset); + int State = 0; + World.Run(CORO + { + State = 1; + CoroToTest->Trigger(); + co_await Async::MoveToThread(ENamedThreads::AnyThread); + TestToCoro->Wait(); + State = 2; + CoroToTest->Trigger(); + }); + Test.TestEqual(TEXT("Initial state"), State, 1); + Test.TestTrue(TEXT("Wait 1"), CoroToTest->Wait()); + Test.TestEqual(TEXT("First event, original thread"), State, 1); + TestToCoro->Trigger(); + Test.TestTrue(TEXT("Wait 2"), CoroToTest->Wait()); + Test.TestEqual(TEXT("Second event, new thread"), State, 2); + } + + { + FEventRef TestToCoro(EEventMode::AutoReset); + FEventRef CoroToTest(EEventMode::AutoReset); + int State = 0; + World.Run(CORO + { + State = 1; + CoroToTest->Trigger(); + co_await UE5Coro::Async::MoveToNewThread(); + TestToCoro->Wait(); + State = 2; + CoroToTest->Trigger(); + }); + Test.TestEqual(TEXT("Initial state"), State, 1); + Test.TestTrue(TEXT("Wait 1"), CoroToTest->Wait()); + Test.TestEqual(TEXT("First event, original thread"), State, 1); + TestToCoro->Trigger(); + Test.TestTrue(TEXT("Wait 2"), CoroToTest->Wait()); + Test.TestEqual(TEXT("Second event, new thread"), State, 2); + } + + { + FEventRef CoroToTest; + World.Run(CORO + { + co_await WhenAll( + UE5Coro::Async::MoveToNewThread(), + UE5Coro::Async::MoveToThread(ENamedThreads::AnyThread)); + CoroToTest->Trigger(); + }); + Test.TestTrue(TEXT("Triggered"), CoroToTest->Wait()); + } + + { + FEventRef CoroToTest; + std::atomic bMovedOut = false; + std::atomic bMovedIn = false; + World.Run(CORO + { + auto Return = Async::MoveToSimilarThread(); + co_await Async::MoveToNewThread(); + bMovedOut = !IsInGameThread(); + co_await Return; + bMovedIn = IsInGameThread(); + CoroToTest->Trigger(); + }); + FTestHelper::PumpGameThread(World, [&] { return CoroToTest->Wait(0); }); + Test.TestTrue(TEXT("Moved out"), bMovedOut); + Test.TestTrue(TEXT("Moved back in"), bMovedIn); + } + + { + std::atomic State = 0; + World.Run(CORO + { + co_await UE5Coro::Async::PlatformSecondsAnyThread(0.05); + ++State; + co_await UE5Coro::Async::PlatformSecondsAnyThread(0.05); + ++State; + }); + Test.TestEqual(TEXT("Initial state"), State, 0); + auto Start = FPlatformTime::Seconds(); + FPlatformProcess::Sleep(0.07); + Test.TestTrue(TEXT("Sleep is reliable 1"), + FPlatformTime::Seconds() >= Start + 0.05); + Test.TestTrue(TEXT("State has increased"), State >= 1); + FPlatformProcess::Sleep(0.05); + Test.TestTrue(TEXT("Sleep is reliable 2"), + FPlatformTime::Seconds() >= Start + 0.1); + Test.TestEqual(TEXT("Final state"), State, 2); + } +} +} + +bool FAsyncAwaiterTest::RunTest(const FString& Parameters) +{ + DoTest<>(*this); + return true; +} + +bool FAsyncInLatentTest::RunTest(const FString& Parameters) +{ + DoTest(*this); + return true; +} + +bool FAsyncStressTest::RunTest(const FString& Parameters) +{ + FTestWorld World; + + std::atomic Count = 0; + World.Run([&]() -> TCoroutine<> + { + TArray Awaiters; + auto GetValue = [](int i) + { + // Map hashes to -0.001..0.001 + auto Div = 1000 * + static_cast(std::numeric_limits::max()); + return (static_cast(std::hash()(i)) / Div * 2 - 1); + }; + for (int i = 0; i < 1000; ++i) + { + Awaiters.Add(Async::PlatformSecondsAnyThread(GetValue(i))); + if (i > 500) + co_await Async::PlatformSecondsAnyThread(GetValue(-i)); + ++Count; + } + for (auto& Awaiter : Awaiters) + { + co_await Awaiter; + ++Count; + } + }); + while (Count < 2000) + ; + FPlatformProcess::Sleep(0.01); + TestEqual(TEXT("Final count"), Count, 2000); + + return true; +} diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/Private/AsyncLoadTest.cpp b/Plugins/UE5Coro/Source/UE5CoroTests/Private/AsyncLoadTest.cpp new file mode 100644 index 00000000..d780d8c8 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/Private/AsyncLoadTest.cpp @@ -0,0 +1,183 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "TestWorld.h" +#include "Misc/AutomationTest.h" +#include "UE5Coro/AggregateAwaiters.h" +#include "UE5Coro/LatentAwaiters.h" +#include "UE5CoroTestObject.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private::Test; + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FAsyncLoadTestLatent, "UE5Coro.AsyncLoad.Latent", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FAsyncLoadTestAsync, "UE5Coro.AsyncLoad.Async", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +namespace +{ +template +void DoTest(FAutomationTestBase& Test) +{ + FTestWorld World; + + { + TStrongObjectPtr Object(World.operator->()); + UWorld* Result; + FEventRef CoroToTest; + World.Run(CORO + { + co_await Latent::NextTick(); + TSoftObjectPtr Soft = Object.Get(); + Result = co_await Latent::AsyncLoadObject(Soft); + CoroToTest->Trigger(); + }); + FTestHelper::PumpGameThread(World, [&] { return CoroToTest->Wait(0); }); + Test.TestEqual(TEXT("Loaded"), Result, Object.Get()); + } + + { + TStrongObjectPtr Object1(World.operator->()); + TStrongObjectPtr Object2(NewObject()); + TArray Result; + FEventRef CoroToTest; + World.Run(CORO + { + co_await Latent::NextTick(); + TSoftObjectPtr Soft1 = Object1.Get(); + TSoftObjectPtr Soft2 = Object2.Get(); + Result = co_await Latent::AsyncLoadObjects(TArray{Soft1, Soft2}); + CoroToTest->Trigger(); + }); + FTestHelper::PumpGameThread(World, [&] { return CoroToTest->Wait(0); }); + Test.TestEqual(TEXT("Num"), Result.Num(), 2); + Test.TestEqual(TEXT("Loaded 1"), Result[0], Object1.Get()); + Test.TestEqual(TEXT("Loaded 2"), Result[1], Object2.Get()); + } + + { + TStrongObjectPtr Object1(World.operator->()); + TStrongObjectPtr Object2(NewObject()); + FEventRef CoroToTest; + World.Run(CORO + { + co_await Latent::NextTick(); + FSoftObjectPath Soft1 = Object1.Get(); + FSoftObjectPath Soft2 = Object2.Get(); + co_await Latent::AsyncLoadObjects(TArray{Soft1, Soft2}); + CoroToTest->Trigger(); + }); + FTestHelper::PumpGameThread(World, [&] { return CoroToTest->Wait(0); }); + // Nothing to test for this overload, other than it triggering the event + } + + { + UClass* Result; + FEventRef CoroToTest; + World.Run(CORO + { + co_await Latent::NextTick(); + TSoftClassPtr Soft = UObject::StaticClass(); + Result = co_await Latent::AsyncLoadClass(Soft); + CoroToTest->Trigger(); + }); + FTestHelper::PumpGameThread(World, [&] { return CoroToTest->Wait(0); }); + Test.TestEqual(TEXT("Loaded"), Result, UObject::StaticClass()); + } + + { + TArray Result; + FEventRef CoroToTest; + World.Run(CORO + { + co_await Latent::NextTick(); + TSoftClassPtr Soft1 = UObject::StaticClass(); + TSoftClassPtr Soft2 = AActor::StaticClass(); + Result = co_await Latent::AsyncLoadClasses(TArray{Soft1, Soft2}); + CoroToTest->Trigger(); + }); + FTestHelper::PumpGameThread(World, [&] { return CoroToTest->Wait(0); }); + Test.TestEqual(TEXT("Num"), Result.Num(), 2); + Test.TestEqual(TEXT("Loaded 1"), Result[0], UObject::StaticClass()); + Test.TestEqual(TEXT("Loaded 2"), Result[1], AActor::StaticClass()); + } + + FString PackagePath = TEXT("/Engine/BasicShapes/Cube"); + + { + UPackage* Package = nullptr; + FEventRef CoroToTest; + World.Run(CORO + { + co_await Latent::NextTick(); + Package = co_await Latent::AsyncLoadPackage(PackagePath); + CoroToTest->Trigger(); + }); + FTestHelper::PumpGameThread(World, [&] { return CoroToTest->Wait(0); }); + Test.TestEqual(TEXT("Package"), Package->GetName(), PackagePath); + } + + { + TStrongObjectPtr Object1(World.operator->()); + FEventRef CoroToTest; + World.Run(CORO + { + co_await Latent::NextTick(); + TSoftObjectPtr Soft1 = Object1.Get(); + TSoftClassPtr Soft2 = UObject::StaticClass(); + auto Load1 = Latent::AsyncLoadObject(Soft1); + auto Load2 = Latent::AsyncLoadClass(Soft2); + auto Load3 = Latent::AsyncLoadPackage(PackagePath); + co_await WhenAll(std::move(Load1), std::move(Load2), Load3); + CoroToTest->Trigger(); + }); + FTestHelper::PumpGameThread(World, [&] { return CoroToTest->Wait(0); }); + } +} +} + +bool FAsyncLoadTestLatent::RunTest(const FString& Parameters) +{ + DoTest(*this); + return true; +} + +bool FAsyncLoadTestAsync::RunTest(const FString& Parameters) +{ + DoTest<>(*this); + return true; +} diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/Private/AsyncQueryTest.cpp b/Plugins/UE5Coro/Source/UE5CoroTests/Private/AsyncQueryTest.cpp new file mode 100644 index 00000000..d408f4dd --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/Private/AsyncQueryTest.cpp @@ -0,0 +1,146 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "TestWorld.h" +#include "Misc/AutomationTest.h" +#include "UE5Coro/LatentAwaiters.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private; +using namespace UE5Coro::Private::Test; + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FAsyncQueryTestAsync, "UE5Coro.AsyncQuery.Async", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FAsyncQueryTestLatent, "UE5Coro.AsyncQuery.Latent", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +#if defined(_MSC_VER) && _MSVC_LANG < 2020'02L +// MSVC workaround - DoTest is not a coroutine but it won't compile without this +template<> +struct stdcoro::coroutine_traits +{ + using promise_type = UE5Coro::Private::TCoroutinePromise; +}; +#endif + +namespace +{ +template +void DoTest(FAutomationTestBase& Test) +{ + FTestWorld World; + + { + int State = -1; + World.Run(CORO + { + auto Result = co_await Latent::AsyncLineTraceByChannel( + World.operator->(), EAsyncTraceType::Single, + FVector::ZeroVector, FVector::UpVector, ECC_WorldStatic); + State = Result.Num(); + }); + World.Tick(); // This queries the async trace on the game thread + Test.TestEqual(TEXT("No results yet"), State, -1); + World.Tick(); // This completes it + Test.TestEqual(TEXT("Results"), State, 0); + } + + { + int State = -1; + World.Run(CORO + { + auto Result = co_await Latent::AsyncOverlapByChannel( + World.operator->(), FVector::ZeroVector, FQuat::Identity, + ECC_WorldStatic, FCollisionShape::MakeBox(FVector::OneVector)); + State = Result.Num(); + }); + World.Tick(); // This queries the async trace on the game thread + Test.TestEqual(TEXT("No results yet"), State, -1); + World.Tick(); // This completes it + Test.TestEqual(TEXT("Results"), State, 0); + } + + { + int State = -1; + World.Run(CORO + { + auto Awaiter = Latent::AsyncLineTraceByChannel( + World.operator->(), EAsyncTraceType::Single, + FVector::ZeroVector, FVector::UpVector, ECC_WorldStatic); + co_await Latent::Ticks(2); // Make sure the async query is complete + Test.TestTrue(TEXT("Ready"), Awaiter.await_ready()); + State = (co_await Awaiter).Num(); + }); + World.Tick(); // This queries the async trace on the game thread + Test.TestEqual(TEXT("No results yet"), State, -1); + World.Tick(); // This completes it + Test.TestEqual(TEXT("No results yet"), State, -1); + World.Tick(); // This will end Ticks(2) + Test.TestEqual(TEXT("Results"), State, 0); + } + + { + int State = -1; + World.Run(CORO + { + auto Awaiter = Latent::AsyncOverlapByChannel( + World.operator->(), FVector::ZeroVector, FQuat::Identity, + ECC_WorldStatic, FCollisionShape::MakeBox(FVector::OneVector)); + co_await Latent::Ticks(2); // Make sure the async query is complete + Test.TestTrue(TEXT("Ready"), Awaiter.await_ready()); + State = (co_await std::move(Awaiter)).Num(); + }); + World.Tick(); // This queries the async trace on the game thread + Test.TestEqual(TEXT("No results yet"), State, -1); + World.Tick(); // This completes it + Test.TestEqual(TEXT("No results yet"), State, -1); + World.Tick(); // This will end Ticks(2) + Test.TestEqual(TEXT("Results"), State, 0); + } +} +} + +bool FAsyncQueryTestAsync::RunTest(const FString& Parameters) +{ + DoTest<>(*this); + return true; +} + +bool FAsyncQueryTestLatent::RunTest(const FString& Parameters) +{ + DoTest(*this); + return true; +} diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/Private/AwaitableEventTest.cpp b/Plugins/UE5Coro/Source/UE5CoroTests/Private/AwaitableEventTest.cpp new file mode 100644 index 00000000..4b35fd1f --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/Private/AwaitableEventTest.cpp @@ -0,0 +1,168 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "TestWorld.h" +#include "Misc/AutomationTest.h" +#include "UE5Coro/Threading.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private::Test; + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FEventAsyncTest, "UE5Coro.Threading.Event.Async", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::CriticalPriority | + EAutomationTestFlags::ProductFilter) + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FEventLatentTest, "UE5Coro.Threading.Event.Latent", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::CriticalPriority | + EAutomationTestFlags::ProductFilter) + +namespace +{ +template +void DoTest(FAutomationTestBase& Test) +{ + FTestWorld World; + + { + bool bResult = false; + FAwaitableEvent Event(Mode, false); + World.Run(CORO + { + co_await Event; + bResult = true; + }); + Test.TestFalse(TEXT("Not triggered yet"), bResult); + Event.Trigger(); + Test.TestTrue(TEXT("Triggered"), bResult); + } + + { + int State = 0; + FAwaitableEvent Event(Mode, false); + World.Run(CORO + { + State += 1; + co_await Event; + State += 10; + }); + World.Run(CORO + { + State += 2; + co_await Event; + State += 20; + }); + Test.TestEqual(TEXT("Start"), State, 3); + Event.Trigger(); + if constexpr (Mode == EEventMode::AutoReset) + { + Test.TestTrue(TEXT("Done"), State == 13 || State == 23); + Event.Trigger(); + } + Test.TestEqual(TEXT("Done"), State, 33); + } + + { + int State = 0; + FAwaitableEvent Event(Mode, true); + World.Run(CORO + { + co_await Event; + State += 1; + co_await Event; + State += 10; + }); + World.Run(CORO + { + co_await Event; + State += 2; + co_await Event; + State += 20; + }); + if constexpr (Mode == EEventMode::ManualReset) + Test.TestEqual(TEXT("Start"), State, 33); + else + { + Test.TestTrue(TEXT("Start"), State == 1 || State == 2); + Event.Trigger(); + int OldState = State; + Test.TestTrue(TEXT("Trigger 1"), State < 32); + Event.Trigger(); + Test.TestTrue(TEXT("Trigger 2A"), State > OldState); + Test.TestTrue(TEXT("Trigger 2B"), State < 32); + Event.Trigger(); + Test.TestEqual(TEXT("Trigger 3"), State, 33); + } + } + + { + int State = 0; + FAwaitableEvent Event(Mode, true); + World.Run(CORO + { + Event.Reset(); + ++State; + co_await Event; + ++State; + co_await Event; + Event.Reset(); + ++State; + co_await Event; + ++State; + }); + Test.TestEqual(TEXT("Start"), State, 1); + Event.Trigger(); + if constexpr (Mode == EEventMode::AutoReset) + { + Test.TestEqual(TEXT("Trigger 1"), State, 2); + Event.Trigger(); + } + Test.TestEqual(TEXT("Trigger 2"), State, 3); + Event.Trigger(); + Test.TestEqual(TEXT("Trigger 3"), State, 4); + } +} +} + +bool FEventAsyncTest::RunTest(const FString& Parameters) +{ + DoTest(*this); + DoTest(*this); + return true; +} + +bool FEventLatentTest::RunTest(const FString& Parameters) +{ + DoTest(*this); + DoTest(*this); + return true; +} diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/Private/AwaitableSemaphoreTest.cpp b/Plugins/UE5Coro/Source/UE5CoroTests/Private/AwaitableSemaphoreTest.cpp new file mode 100644 index 00000000..2f0d50a4 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/Private/AwaitableSemaphoreTest.cpp @@ -0,0 +1,124 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "TestWorld.h" +#include "Misc/AutomationTest.h" +#include "UE5Coro/Threading.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private::Test; + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FSemaAsyncTest, "UE5Coro.Threading.Semaphore.Async", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::CriticalPriority | + EAutomationTestFlags::ProductFilter) + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FSemaLatentTest, "UE5Coro.Threading.Semaphore.Latent", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::CriticalPriority | + EAutomationTestFlags::ProductFilter) + +namespace +{ +template +void DoTest(FAutomationTestBase& Test) +{ + FTestWorld World; + + { + int State = 0; + FAwaitableSemaphore Semaphore(10, 0); + World.Run(CORO + { + ++State; + co_await Semaphore; + ++State; + co_await Semaphore; + ++State; + co_await Semaphore; + ++State; + co_await Semaphore; + ++State; + }); + + Test.TestEqual(TEXT("Initial state"), State, 1); + Semaphore.Unlock(); + Test.TestEqual(TEXT("Unlock 1"), State, 2); + Semaphore.Unlock(2); + Test.TestEqual(TEXT("Unlock 2"), State, 4); + Semaphore.Unlock(); + } + + { + int State = 0; + int Count = 0; + FAwaitableSemaphore Semaphore(100, 50); + World.Run(CORO + { + for (int i = 1; i < 20; ++i) + { + for (int j = 0; j < i; ++j) + { + co_await Semaphore; + ++Count; + } + State = i; + } + }); + Test.TestEqual(TEXT("Initial state"), State, 9); + Test.TestEqual(TEXT("Initial count"), Count, 50); + Semaphore.Unlock(4); + + Test.TestEqual(TEXT("State 2"), State, 9); + Test.TestEqual(TEXT("Count 2"), Count, 54); + Semaphore.Unlock(); + Test.TestEqual(TEXT("State 3"), State, 10); + Test.TestEqual(TEXT("Count 3"), Count, 55); + Semaphore.Unlock(100); + Test.TestEqual(TEXT("State 4"), State, 17); + Test.TestEqual(TEXT("Count 4"), Count, 155); + Semaphore.Unlock(100); + Test.TestEqual(TEXT("State 5"), State, 19); + } +} +} + +bool FSemaAsyncTest::RunTest(const FString& Parameters) +{ + DoTest<>(*this); + return true; +} + +bool FSemaLatentTest::RunTest(const FString& Parameters) +{ + DoTest(*this); + return true; +} diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/Private/CancellationTest.cpp b/Plugins/UE5Coro/Source/UE5CoroTests/Private/CancellationTest.cpp new file mode 100644 index 00000000..fa57b0c1 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/Private/CancellationTest.cpp @@ -0,0 +1,311 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "TestWorld.h" +#include "Misc/AutomationTest.h" +#include "UE5Coro/AsyncAwaiters.h" +#include "UE5Coro/Cancellation.h" +#include "UE5Coro/LatentAwaiters.h" +#include "UE5Coro/LatentCallbacks.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private; +using namespace UE5Coro::Private::Test; + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FCancelTestAsync, "UE5Coro.Cancel.Async", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FCancelTestLatent, "UE5Coro.Cancel.Latent", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +namespace +{ +template +void DoTest(FAutomationTestBase& Test) +{ + FTestWorld World; + + IF_CORO_LATENT + { + bool bCanceled = false; + bool bAborted = false; + bool bDestroyed = false; + auto Coro = World.Run(CORO_R(int) + { + FOnCoroutineCanceled _1([&] + { + bCanceled = true; + Test.TestTrue(TEXT("Read cancellation from within"), + IsCurrentCoroutineCanceled()); + }); + Latent::FOnActionAborted _2([&] { bAborted = true; }); + Latent::FOnObjectDestroyed _3([&] { bDestroyed = true; }); + co_await Latent::Cancel(); + co_return 1; + }); + Test.TestTrue(TEXT("Done"), Coro.IsDone()); + Test.TestTrue(TEXT("Canceled"), bCanceled); + Test.TestFalse(TEXT("Not aborted"), bAborted); + Test.TestFalse(TEXT("Not destroyed"), bDestroyed); + Test.TestFalse(TEXT("Not successful"), Coro.WasSuccessful()); + Test.TestEqual(TEXT("No return value"), Coro.GetResult(), 0); + } + + IF_CORO_LATENT + { + std::atomic bCanceled = false; + std::atomic bAborted = false; + std::atomic bDestroyed = false; + auto Coro = World.Run(CORO_R(int) + { + FOnCoroutineCanceled _1([&] + { + bCanceled = true; + Test.TestTrue(TEXT("Back on the game thread"), IsInGameThread()); + }); + Latent::FOnActionAborted _2([&] { bAborted = true; }); + Latent::FOnObjectDestroyed _3([&] { bDestroyed = true; }); + Test.TestFalse(TEXT("Not canceled yet"), + IsCurrentCoroutineCanceled()); + co_await Async::MoveToNewThread(); + co_await Latent::Cancel(); + co_return 1; + }); + FTestHelper::PumpGameThread(World, [&] { return Coro.IsDone(); }); + Test.TestTrue(TEXT("Canceled"), bCanceled); + Test.TestFalse(TEXT("Not aborted"), bAborted); + Test.TestFalse(TEXT("Not destroyed"), bDestroyed); + Test.TestFalse(TEXT("Not successful"), Coro.WasSuccessful()); + Test.TestEqual(TEXT("No return value"), Coro.GetResult(), 0); + } + + { + bool bCanceled = false; + bool bDestroyed = false; + auto Coro = World.Run(CORO + { + FOnCoroutineCanceled _([&] + { + bCanceled = true; + Test.TestFalse(TEXT("Not canceled"), + IsCurrentCoroutineCanceled()); + }); + ON_SCOPE_EXIT + { + bDestroyed = true; + Test.TestFalse(TEXT("Not canceled"), + IsCurrentCoroutineCanceled()); + }; + Test.TestFalse(TEXT("Not canceled yet"), + IsCurrentCoroutineCanceled()); + co_return; + }); + Test.TestTrue(TEXT("Destroyed"), bDestroyed); + Test.TestFalse(TEXT("Not canceled"), bCanceled); + Test.TestTrue(TEXT("Successful"), Coro.WasSuccessful()); + } + + { + bool bCanceled = false; + bool bDestroyed = false; + std::optional> Coro; + { + FTestWorld World2; + Coro = World2.Run(CORO + { + FOnCoroutineCanceled _([&] + { + bCanceled = true; + Test.TestTrue(TEXT("Read cancellation from within"), + IsCurrentCoroutineCanceled()); + }); + ON_SCOPE_EXIT + { + bDestroyed = true; + Test.TestTrue(TEXT("Read cancellation from within"), + IsCurrentCoroutineCanceled()); + }; + Test.TestFalse(TEXT("Not canceled yet"), + IsCurrentCoroutineCanceled()); + co_await Latent::NextTick(); + }); + Test.TestFalse(TEXT("Still running"), Coro->WasSuccessful()); + } // Indirectly cancel by destroying the world during a latent co_await + Test.TestTrue(TEXT("Destroyed"), bDestroyed); + Test.TestTrue(TEXT("Canceled"), bCanceled); + Test.TestFalse(TEXT("Not successful"), Coro->WasSuccessful()); + } + + { + bool bCanceled = false; + bool bDestroyed = false; + auto Coro = World.Run(CORO + { + FOnCoroutineCanceled _([&] + { + bCanceled = true; + Test.TestTrue(TEXT("Read cancellation from within"), + IsCurrentCoroutineCanceled()); + }); + ON_SCOPE_EXIT + { + bDestroyed = true; + Test.TestTrue(TEXT("Read cancellation from within"), + IsCurrentCoroutineCanceled()); + }; + Test.TestFalse(TEXT("Not canceled yet"), + IsCurrentCoroutineCanceled()); + co_await Latent::Ticks(5); + }); + World.EndTick(); + Test.TestFalse(TEXT("Active"), bCanceled); + Test.TestFalse(TEXT("Active"), bDestroyed); + Test.TestFalse(TEXT("Not done yet"), Coro.IsDone()); + Test.TestFalse(TEXT("Still running"), Coro.WasSuccessful()); + Coro.Cancel(); + for (int i = 0; i < 5; ++i) // Async needs to attempt to resume + { + IF_NOT_CORO_LATENT // Latent->latent sees the cancellation right away + Test.TestFalse(TEXT("Not canceled yet"), bDestroyed); + World.Tick(); + } + Test.TestTrue(TEXT("Canceled"), bCanceled); + Test.TestTrue(TEXT("Canceled"), bDestroyed); + Test.TestFalse(TEXT("Not successful"), Coro.WasSuccessful()); + } + + { + std::atomic bDone = false; + auto Coro = World.Run(CORO + { + ON_SCOPE_EXIT + { + bDone = true; + IF_CORO_LATENT + Test.TestTrue(TEXT("Back on the game thread"), + IsInGameThread()); + }; + Test.TestFalse(TEXT("Not canceled yet"), + IsCurrentCoroutineCanceled()); + co_await Async::MoveToThread(ENamedThreads::AnyThread); + for (;;) + co_await Async::PlatformSecondsAnyThread(0.01); + }); + for (int i = 0; i < 10; ++i) + { + World.Tick(); + Test.TestFalse(TEXT("Still running"), bDone); + } + Coro.Cancel(); + // Also acts as a busy wait for the async test + FTestHelper::PumpGameThread(World, [&] { return bDone.load(); }); + Test.TestTrue(TEXT("Canceled"), bDone); + Test.TestFalse(TEXT("Not successful"), Coro.WasSuccessful()); + } + + { + bool bDone = false; + bool bContinue = false; + auto Coro = World.Run(CORO + { + ON_SCOPE_EXIT { bDone = true; }; + // First, run with cancellations blocked + { + FCancellationGuard _; + while (!bContinue) + co_await Latent::NextTick(); + Test.TestTrue(TEXT("Incoming guarded cancellation"), + IsCurrentCoroutineCanceled()); + } + // Then, allow cancellations + Test.TestTrue(TEXT("Incoming unguarded cancellation"), + IsCurrentCoroutineCanceled()); + for (;;) + co_await Latent::NextTick(); + }); + Coro.Cancel(); + for (int i = 0; i < 10; ++i) + { + World.Tick(); + Test.TestFalse(TEXT("Still running"), bDone); + } + bContinue = true; + World.Tick(); + // Async->Latent await needs an extra tick to figure this out + IF_NOT_CORO_LATENT + World.Tick(); + Test.TestTrue(TEXT("Canceled"), bDone); + Test.TestFalse(TEXT("Not successful"), Coro.WasSuccessful()); + } + + { + bool bDone = false; + auto Coro = World.Run(CORO + { + Test.TestFalse(TEXT("Not canceled yet"), + IsCurrentCoroutineCanceled()); + co_await Async::MoveToNewThread(); + ON_SCOPE_EXIT + { + bDone = true; + IF_CORO_LATENT + Test.TestTrue(TEXT("Back on the game thread"), + IsInGameThread()); + }; + for (;;) + co_await FinishNowIfCanceled(); + }); + for (int i = 0; i < 10; ++i) + { + World.Tick(); + Test.TestFalse(TEXT("Still running"), bDone); + } + Coro.Cancel(); + FTestHelper::PumpGameThread(World, [&] { return bDone; }); + } +} +} + +bool FCancelTestAsync::RunTest(const FString& Parameters) +{ + DoTest<>(*this); + return true; +} + +bool FCancelTestLatent::RunTest(const FString& Parameters) +{ + DoTest(*this); + return true; +} diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/Private/CoroutineHandleTest.cpp b/Plugins/UE5Coro/Source/UE5CoroTests/Private/CoroutineHandleTest.cpp new file mode 100644 index 00000000..763e85a8 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/Private/CoroutineHandleTest.cpp @@ -0,0 +1,313 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include +#include +#include "TestWorld.h" +#include "Misc/AutomationTest.h" +#include "UE5CoroTestObject.h" +#include "UE5Coro/Coroutine.h" +#include "UE5Coro/AsyncAwaiters.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private; +using namespace UE5Coro::Private::Test; + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FHandleTestAsync, "UE5Coro.Handle.Async", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FHandleTestLatent, "UE5Coro.Handle.Latent", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +namespace +{ +template +using TThreadSafeSharedPtr = TSharedPtr; + +template typename S, typename... T> +void DoTestSharedPtr(FTestWorld& World, FAutomationTestBase& Test) +{ + { + S Ptr(new int(0)); + auto Coro = World.Run(CORO { co_await Latent::NextTick(); }); + Coro.ContinueWithWeak(Ptr, [](int* Value) { *Value = 1; }); + World.EndTick(); + Test.TestEqual(TEXT("Not completed yet"), *Ptr, 0); + World.Tick(); + Test.TestEqual(TEXT("Completed"), *Ptr, 1); + } + + { + S Ptr(new int(0)); + bool bContinued = false; + auto Coro = World.Run(CORO { co_await Latent::NextTick(); }); + Coro.ContinueWithWeak(Ptr, [&] { bContinued = true; }); + World.EndTick(); + Test.TestFalse(TEXT("Not completed yet"), bContinued); + Ptr = nullptr; + World.Tick(); + Test.TestFalse(TEXT("No continuation"), bContinued); + } + + struct FBoolSetter + { + bool* Ptr; + void Set() { *Ptr = true; } + }; + + { + bool bContinued = false; + S Ptr(new FBoolSetter{&bContinued}); + auto Coro = World.Run(CORO { co_await Latent::NextTick(); }); + Coro.ContinueWithWeak(Ptr, &FBoolSetter::Set); + World.EndTick(); + Test.TestFalse(TEXT("Not completed yet"), bContinued); + World.Tick(); + Test.TestTrue(TEXT("Completed"), bContinued); + } + + { + bool bContinued = false; + S Ptr(new FBoolSetter{&bContinued}); + auto Coro = World.Run(CORO { co_await Latent::NextTick(); }); + Coro.ContinueWithWeak(Ptr, &FBoolSetter::Set); + World.EndTick(); + Test.TestFalse(TEXT("Not completed yet"), bContinued); + Ptr = nullptr; + World.Tick(); + Test.TestFalse(TEXT("No continuation"), bContinued); + } + + struct FIntSetter + { + int* Ptr; + void Set(int Value) const { *Ptr = Value; } + }; + + { + int State = 0; + S Ptr(new FIntSetter{&State}); + auto Coro = World.Run(CORO_R(int) + { + co_await Latent::NextTick(); + co_return 1; + }); + Coro.ContinueWithWeak(Ptr, &FIntSetter::Set); + World.EndTick(); + Test.TestEqual(TEXT("Not completed yet"), State, 0); + World.Tick(); + Test.TestEqual(TEXT("Completed"), State, 1); + } + + { + int State = 0; + S Ptr(new FIntSetter{&State}); + auto Coro = World.Run(CORO_R(int) + { + co_await Latent::NextTick(); + co_return 1; + }); + Coro.ContinueWithWeak(Ptr, &FIntSetter::Set); + World.EndTick(); + Test.TestEqual(TEXT("Not completed yet"), State, 0); + Ptr = nullptr; + World.Tick(); + Test.TestEqual(TEXT("No continuation"), State, 0); + } +} + +template +void DoTest(FAutomationTestBase& Test) +{ + FTestWorld World; + + { + FEventRef StartTest; + auto Coro = World.Run(CORO + { + co_await Async::MoveToNewThread(); + StartTest->Wait(); + FPlatformProcess::Sleep(0.1f); + }); + + // Waiting itself has to run on another thread. + // In the latent case, not doing this would deadlock the game thread. + std::atomic bDone = false; + World.Run(CORO + { + co_await Async::MoveToNewThread(); + StartTest->Trigger(); + Test.TestFalse(TEXT("Timeout"), Coro.Wait(1)); + Test.TestTrue(TEXT("Waited enough"), Coro.Wait()); + bDone = true; + }); + FTestHelper::PumpGameThread(World, [&] { return bDone.load(); }); + Test.TestTrue(TEXT("Reports done"), Coro.IsDone()); + Test.TestTrue(TEXT("Successful"), Coro.WasSuccessful()); + } + + { + int Value = 0; + auto Coro = World.Run(CORO_R(int) { co_return 1; }); + Coro.ContinueWith([&](int InValue) { Value = InValue; }); + Test.TestEqual(TEXT("Value"), Value, 1); + Test.TestTrue(TEXT("Successful"), Coro.WasSuccessful()); + } + + { + FEventRef TestToCoro, CoroToTest; + TStrongObjectPtr Object(NewObject()); + auto Coro = World.Run(CORO + { + co_await Async::MoveToNewThread(); + TestToCoro->Wait(); + // Unconditionally move to the GT, ContinueWithWeak is on a UObject + co_await Async::MoveToGameThread(); + }); + Coro.ContinueWithWeak(Object.Get(), [&] { CoroToTest->Trigger(); }); + Test.TestFalse(TEXT("Not triggered yet"), CoroToTest->Wait(0)); + TestToCoro->Trigger(); + FTestHelper::PumpGameThread(World, [&] { return Coro.IsDone(); }); + CoroToTest->Wait(); + Test.TestTrue(TEXT("Done"), true); + Test.TestTrue(TEXT("Successful"), Coro.WasSuccessful()); + } + + { + FEventRef TestToCoro, CoroToTest; + TStrongObjectPtr Object(NewObject()); + Object->Callback = [&] { CoroToTest->Trigger(); }; + auto Coro = World.Run(CORO + { + co_await Async::MoveToNewThread(); + TestToCoro->Wait(); + // Unconditionally move to the GT, ContinueWithWeak is on a UObject + co_await Async::MoveToGameThread(); + }); + Coro.ContinueWithWeak(Object.Get(), &UUE5CoroTestObject::Core); + Test.TestFalse(TEXT("Not triggered yet"), CoroToTest->Wait(0)); + TestToCoro->Trigger(); + FTestHelper::PumpGameThread(World, [&] { return Coro.IsDone(); }); + CoroToTest->Wait(); + Test.TestTrue(TEXT("Done"), true); + Test.TestTrue(TEXT("Successful"), Coro.WasSuccessful()); + } + + { + FEventRef TestToCoro; + std::atomic bContinued = false; + TWeakObjectPtr Object(NewObject()); + auto Coro = World.Run(CORO + { + co_await Async::MoveToNewThread(); + TestToCoro->Wait(); + // Unconditionally move to the GT, ContinueWithWeak is on a UObject + co_await Async::MoveToGameThread(); + }); + Coro.ContinueWithWeak(Object.Get(), [&] { bContinued = true; }); + Object->MarkPendingKill(); + CollectGarbage(RF_NoFlags); + Test.TestFalse(TEXT("Object destroyed"), Object.IsValid()); + Test.TestFalse(TEXT("Coroutine still running"), Coro.IsDone()); + Test.TestFalse(TEXT("Not successful yet"), Coro.WasSuccessful()); + TestToCoro->Trigger(); + FTestHelper::PumpGameThread(World, [&] { return Coro.IsDone(); }); + // There's a data race with IsDone() when async, wait a little + for (int i = 0; i < 10; ++i) + World.Tick(); + Test.TestFalse(TEXT("Continuation not called"), bContinued); + Test.TestTrue(TEXT("Successful"), Coro.WasSuccessful()); + } + + { + auto Coro = TCoroutine<>::CompletedCoroutine; + Test.TestTrue(TEXT("Completed"), Coro.IsDone()); + Test.TestTrue(TEXT("Successful"), Coro.WasSuccessful()); + + auto Ptr = std::make_unique(1); // move-only + auto Coro1 = TCoroutine<>::FromResult(std::move(Ptr)); + auto Coro2 = TCoroutine<>::FromResult(2); + auto Coro3 = TCoroutine::FromResult(3); + Test.TestTrue(TEXT("Completed 1"), Coro1.IsDone()); + Test.TestTrue(TEXT("Successful 1"), Coro.WasSuccessful()); + Test.TestTrue(TEXT("Completed 2"), Coro2.IsDone()); + Test.TestTrue(TEXT("Successful 2"), Coro.WasSuccessful()); + Test.TestTrue(TEXT("Completed 3"), Coro3.IsDone()); + Test.TestTrue(TEXT("Successful 3"), Coro.WasSuccessful()); + Test.TestNull(TEXT("Moved from"), Ptr.get()); + Test.TestEqual(TEXT("Moved to"), *Coro1.GetResult(), 1); + Test.TestEqual(TEXT("Coro2"), Coro2.GetResult(), 2); + Test.TestEqual(TEXT("Coro3"), Coro3.MoveResult(), 3); + } + + { + TMap Map1; + TSortedMap, int> Map2; + std::unordered_map, int> Map3; + std::map, int> Map4; + for (int i = 0; i < 5; ++i) + { + Map1.Add(World.Run(CORO_R(int) { co_return i; }), i); + Map2.Add(World.Run(CORO_R(int) { co_return i; }), i); + Map3[World.Run(CORO_R(int) { co_return i; })] = i; + Map4[World.Run(CORO_R(int) { co_return i; })] = i; + } + auto TestMap = [&](auto& Map) + { + for (auto& [Key, Value] : Map) + Test.TestEqual(TEXT("Value"), Map[Key], Value); + }; + TestMap(Map1); + TestMap(Map2); + TestMap(Map3); + TestMap(Map4); + } + + DoTestSharedPtr(World, Test); + DoTestSharedPtr(World, Test); +} +} + +bool FHandleTestAsync::RunTest(const FString& Parameters) +{ + DoTest<>(*this); + return true; +} + +bool FHandleTestLatent::RunTest(const FString& Parameters) +{ + DoTest(*this); + return true; +} diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/Private/DelegateAwaiterTest.cpp b/Plugins/UE5Coro/Source/UE5CoroTests/Private/DelegateAwaiterTest.cpp new file mode 100644 index 00000000..edac79b7 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/Private/DelegateAwaiterTest.cpp @@ -0,0 +1,339 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "Misc/AutomationTest.h" +#include "TestDelegates.h" +#include "TestWorld.h" +#include "UE5CoroTestObject.h" +#include "UE5Coro/AsyncAwaiters.h" +#include "UE5Coro/LatentAwaiters.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private; +using namespace UE5Coro::Private::Test; + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDelegateTestCore, "UE5Coro.Delegate.Core", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::CriticalPriority | + EAutomationTestFlags::SmokeFilter) + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDelegateTestAsync, "UE5Coro.Delegate.Async", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDelegateTestLatent, "UE5Coro.Delegate.Latent", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +namespace +{ +template +struct TSelect; + +template<> +struct TSelect +{ + using FVoid = TDelegate; + using FParams = TDelegate; + using FRetVal = TDelegate; + using FAll = TDelegate; +}; + +template<> +struct TSelect +{ + using FVoid = TMulticastDelegate; + using FParams = TMulticastDelegate; + using FRetVal = TMulticastDelegate; + using FAll = TMulticastDelegate; +}; + +template<> +struct TSelect +{ + using FVoid = FUE5CoroTestDynamicVoidDelegate; + using FParams = FUE5CoroTestDynamicParamsDelegate; + using FRetVal = FUE5CoroTestDynamicRetvalDelegate; + using FAll = FUE5CoroTestDynamicAllDelegate; +}; + +template<> +struct TSelect +{ + using FVoid = FUE5CoroTestDynamicMulticastVoidDelegate; + using FParams = FUE5CoroTestDynamicMulticastParamsDelegate; +}; + +struct alignas(4096) FHighlyAlignedByte +{ + uint8 Value; +}; + +template +void DoTest(FAutomationTestBase& Test) +{ + FTestWorld World; + + auto Invoke = [](auto& Delegate, auto&&... Params) + { + if constexpr (bMulticast) + return Delegate.Broadcast(std::forward(Params)...); + else + return Delegate.Execute(std::forward(Params)...); + }; + + { + bool bDone = false; + typename TSelect::FVoid Delegate; + World.Run(CORO + { + if constexpr (bLatentWrapper) + co_await Latent::UntilDelegate(Delegate); + else + co_await Delegate; + bDone = true; + }); + World.EndTick(); + Test.TestFalse(TEXT("Not done yet"), bDone); + Invoke(Delegate); + if constexpr (bLatentWrapper) + World.Tick(); + Test.TestTrue(TEXT("Done"), bDone); + } + + { + bool bDone = false; + typename TSelect::FParams Delegate; + World.Run(CORO + { + if constexpr (bLatentWrapper) + co_await Latent::UntilDelegate(Delegate); + else + { + auto&& [A, B] = co_await Delegate; + static_assert(!std::is_reference_v); + static_assert(std::is_lvalue_reference_v); + Test.TestEqual(TEXT("Param 1"), A, 1); + Test.TestEqual(TEXT("Param 2"), B, 2); + B = 3; + } + bDone = true; + }); + World.EndTick(); + Test.TestFalse(TEXT("Not done yet"), bDone); + int Two = 2; + Invoke(Delegate, 1, Two); + if constexpr (bLatentWrapper) + World.Tick(); + else + Test.TestEqual(TEXT("Reference writes back"), Two, 3); + Test.TestTrue(TEXT("Done"), bDone); + } + + if constexpr (!bMulticast) + { + { + bool bDone = false; + typename TSelect::FRetVal Delegate; + World.Run(CORO + { + if constexpr (bLatentWrapper) + co_await Latent::UntilDelegate(Delegate); + else + co_await Delegate; + bDone = true; + FUE5CoroTestConstructionChecker::bConstructed = false; + }); + World.EndTick(); + Test.TestFalse(TEXT("Not done yet"), bDone); + Invoke(Delegate); + if constexpr (bLatentWrapper) + World.Tick(); + else + Test.TestTrue(TEXT("Return value"), + FUE5CoroTestConstructionChecker::bConstructed); + Test.TestTrue(TEXT("Done"), bDone); + } + + { + bool bDone = false; + typename TSelect::FAll Delegate; + World.Run(CORO + { + if constexpr (bLatentWrapper) + co_await Latent::UntilDelegate(Delegate); + else + { + auto&& [A, B] = co_await Delegate; + static_assert(!std::is_reference_v); + static_assert(std::is_lvalue_reference_v); + Test.TestEqual(TEXT("Param 1"), A, 1); + Test.TestEqual(TEXT("Param 2"), B, 2); + B = 3; + } + bDone = true; + FUE5CoroTestConstructionChecker::bConstructed = false; + }); + World.EndTick(); + Test.TestFalse(TEXT("Not done yet"), bDone); + int Two = 2; + Invoke(Delegate, 1, Two); + Test.TestTrue(TEXT("Return value"), + FUE5CoroTestConstructionChecker::bConstructed); + if constexpr (bLatentWrapper) + World.Tick(); + else + Test.TestEqual(TEXT("Reference writes back"), Two, 3); + Test.TestTrue(TEXT("Done"), bDone); + } + } + + // Sparse delegate tests + if constexpr (bDynamic && bMulticast) + { + { + bool bDone = false; + auto* Object = NewObject(); + World.Run(CORO + { + if constexpr (bLatentWrapper) + co_await Latent::UntilDelegate(Object->SparseDelegate); + else + co_await Object->SparseDelegate; + bDone = true; + }); + World.EndTick(); + Test.TestFalse(TEXT("Not done yet"), bDone); + Invoke(Object->SparseDelegate); + Object->SparseDelegate.Broadcast(); + if constexpr (bLatentWrapper) + World.Tick(); + Test.TestTrue(TEXT("Done"), bDone); + } + + { + bool bDone = false; + auto* Object = NewObject(); + World.Run(CORO + { + if constexpr (bLatentWrapper) + co_await Latent::UntilDelegate(Object->SparseParamsDelegate); + else + { + auto&& [A, B] = co_await Object->SparseParamsDelegate; + static_assert(!std::is_reference_v); + static_assert(std::is_lvalue_reference_v); + Test.TestEqual(TEXT("Param 1"), A, 1); + Test.TestEqual(TEXT("Param 2"), B, 2); + } + bDone = true; + }); + World.EndTick(); + Test.TestFalse(TEXT("Not done yet"), bDone); + int Two = 2; + Object->SparseParamsDelegate.Broadcast(1, Two); + if constexpr (bLatentWrapper) + World.Tick(); + Test.TestTrue(TEXT("Done"), bDone); + } + } +} + +template +void DoTests(FAutomationTestBase& Test) +{ + if constexpr (N < 8) + { + DoTest<(N & 4) != 0, (N & 2) != 0, (N & 1) != 0, T...>(Test); + DoTests(Test); + } +} +} + +bool FDelegateTestCore::RunTest(const FString& Parameters) +{ + // The implementation relies heavily on .gen.cpp structs' in-memory layouts. + // See what changed in TBaseUFunctionDelegateInstance::Execute if this fails. + + // This is what needs to be matched: same offsets, same alignment + struct FReference + { + TCHAR X; + FHighlyAlignedByte Y; + double Z; + }; + using FTestType = TDecayedPayload; + alignas(FTestType) uint8 Storage[sizeof(FTestType)]; + auto& TestData = *std::launder(reinterpret_cast(&Storage)); + static_assert(alignof(FTestType) == alignof(FReference)); + static_assert(sizeof(FTestType) == sizeof(FReference)); + + auto PayloadOffset = [&TestData](auto& Field) -> size_t + { + return reinterpret_cast(&Field) - + reinterpret_cast(&TestData); + }; + bool bOk = true; + bOk &= TestEqual(TEXT("X"), PayloadOffset(TestData.get<0>()), + STRUCT_OFFSET(FReference, X)); + bOk &= TestEqual(TEXT("Y"), PayloadOffset(TestData.get<1>()), + STRUCT_OFFSET(FReference, Y)); + bOk &= TestEqual(TEXT("Z"), PayloadOffset(TestData.get<2>()), + STRUCT_OFFSET(FReference, Z)); + + FMemory::Memzero(TestData); + TestData.get<0>() = TEXT('♩'); + TestData.get<1>().Value = static_cast(TEXT('♪')); + TestData.get<2>() = TEXT('♫') + 0.5; + + auto* AsRef = std::launder(reinterpret_cast(&TestData)); + bOk &= TestEqual(TEXT("X′"), AsRef->X, TEXT('♩')); + bOk &= TestEqual(TEXT("Y′"), AsRef->Y.Value, static_cast(TEXT('♪'))); + bOk &= TestEqual(TEXT("Z′"), AsRef->Z, TEXT('♫') + 0.5); + + // Please open an issue with your platform/build type if this fails! + checkf(bOk, TEXT("Internal error: Unexpected delegate memory layout")); + return bOk; +} + +bool FDelegateTestAsync::RunTest(const FString& Parameters) +{ + DoTests<0>(*this); + return true; +} + +bool FDelegateTestLatent::RunTest(const FString& Parameters) +{ + DoTests<0, FLatentActionInfo>(*this); + return true; +} diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/Private/ExceptionTest.cpp b/Plugins/UE5Coro/Source/UE5CoroTests/Private/ExceptionTest.cpp new file mode 100644 index 00000000..664b0bc6 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/Private/ExceptionTest.cpp @@ -0,0 +1,151 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include +#include "TestWorld.h" +#include "Misc/AutomationTest.h" +#include "UE5CoroTestObject.h" +#include "UE5Coro/AsyncAwaiters.h" +#include "UE5Coro/Generator.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private; +using namespace UE5Coro::Private::Test; + +// Enable exceptions for this module and UE5Coro itself to test this +#if !PLATFORM_EXCEPTIONS_DISABLED + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FExceptionTest, "UE5Coro.Exceptions", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +namespace +{ +class FTestException final : public std::exception +{ + const char* What; + +public: + explicit FTestException(const char* What) : What(What) { } + virtual const char* what() const noexcept override { return What; } +}; +} + +bool FExceptionTest::RunTest(const FString& Parameters) +{ + FTestWorld World; // This is not used but it needs to exist in this scope + + try + { + auto Fn = []() -> TGenerator + { + co_yield 1; + throw new FTestException("test"); + }; + auto Gen = Fn(); + TestEqual(TEXT("Generator init value"), Gen.Current(), 1); + Gen.Resume(); + TestTrue(TEXT("Generator unreachable code"), false); + } + catch (const FTestException* Ex) + { + TestEqual(TEXT("Generator exception"), Ex->what(), "test"); + delete Ex; + } + catch (...) + { + TestTrue(TEXT("Generator unexpected exception"), false); + } + + std::optional> Coro; + try + { + auto Fn = [&]() -> TCoroutine<> + { + co_await stdcoro::suspend_never(); + Coro = static_cast&>( + FPromise::Current()).get_return_object(); + throw new FTestException("async"); + }; + Coro = std::nullopt; + Fn(); + TestTrue(TEXT("Async unreachable code"), false); + } + catch (const FTestException* Ex) + { + TestEqual(TEXT("Async exception"), Ex->what(), "async"); + delete Ex; + } + catch (...) + { + TestTrue(TEXT("Async unexpected exception"), false); + } + TestTrue(TEXT("Handle captured"), Coro.has_value()); + TestTrue(TEXT("Done"), Coro->IsDone()); + TestFalse(TEXT("Not successful"), Coro->WasSuccessful()); + + try + { + auto Fn = [&](FLatentActionInfo) -> TCoroutine<> + { + co_await stdcoro::suspend_never(); + Coro = static_cast&>( + FPromise::Current()).get_return_object(); + throw new FTestException("latent"); + }; + FLatentActionInfo Info(0, 0, nullptr, NewObject()); + Coro = std::nullopt; + Fn(Info); + TestTrue(TEXT("Latent unreachable code"), false); + } + catch (const FTestException* Ex) + { + TestEqual(TEXT("Latent exception"), Ex->what(), "latent"); + delete Ex; + } + catch (...) + { + TestTrue(TEXT("Latent unexpected exception"), false); + } + TestTrue(TEXT("Handle captured"), Coro.has_value()); + TestTrue(TEXT("Done"), Coro->IsDone()); + TestFalse(TEXT("Not successful"), Coro->WasSuccessful()); + + // Check if FLatentPromise detached correctly + World.Tick(); + World.Tick(); + World.Tick(); + + return true; +} + +#endif diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/Private/FutureTest.cpp b/Plugins/UE5Coro/Source/UE5CoroTests/Private/FutureTest.cpp new file mode 100644 index 00000000..e7d067d7 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/Private/FutureTest.cpp @@ -0,0 +1,151 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "Misc/AutomationTest.h" +#include "TestWorld.h" +#include "UE5Coro/AggregateAwaiters.h" +#include "UE5Coro/AsyncAwaiters.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private; +using namespace UE5Coro::Private::Test; + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FFutureAsync, "UE5Coro.Future.Async", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FFutureLatent, "UE5Coro.Future.Latent", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +#ifdef _MSC_VER +// MSVC workaround - DoTest is not a coroutine but it won't compile without this +template<> +struct stdcoro::coroutine_traits +{ + using promise_type = UE5Coro::Private::TCoroutinePromise; +}; +#endif + +namespace +{ +template +void DoTest(FAutomationTestBase& Test) +{ + FTestWorld World; + + { + TPromise Promise; + Promise.SetValue(1); + auto Coro = World.Run(CORO_R(int) + { + co_return co_await Promise.GetFuture(); + }); + Test.TestTrue(TEXT("Already done"), Coro.IsDone()); + Test.TestTrue(TEXT("Successful"), Coro.WasSuccessful()); + Test.TestEqual(TEXT("Value"), Coro.GetResult(), 1); + } + + { + int State = 0; + TPromise Promise; + World.Run(CORO + { + State = 1; + co_await Promise.GetFuture(); + State = 2; + }); + Test.TestEqual(TEXT("Before"), State, 1); + Promise.SetValue(); + Test.TestEqual(TEXT("After"), State, 2); + } + + { + int State = 0; + TPromise Promise; + World.Run(CORO + { + State = 1; + decltype(auto) Value = co_await Promise.GetFuture(); + static_assert(std::is_same_v); + State = Value; + }); + Test.TestEqual(TEXT("Before"), State, 1); + Promise.SetValue(2); + Test.TestEqual(TEXT("After"), State, 2); + } + + { + int State = 0; + TPromise Promise; + World.Run(CORO + { + State = 1; + decltype(auto) Value = co_await Promise.GetFuture(); + static_assert(std::is_same_v); + State = Value; + }); + Test.TestEqual(TEXT("Before"), State, 1); + int Two = 2; + Promise.SetValue(Two); + Test.TestEqual(TEXT("After"), State, 2); + } + + { + int State = 0; + TPromise Promise1; + TPromise Promise2; + World.Run(CORO + { + State = co_await WhenAny(Promise1.GetFuture(), Promise2.GetFuture()); + }); + Test.TestEqual(TEXT("Before"), State, 0); + int One = 1; + Promise2.SetValue(One); + Test.TestEqual(TEXT("After"), State, 1); + Promise1.SetValue(One); + } +} +} // namespace + +bool FFutureAsync::RunTest(const FString& Parameters) +{ + DoTest<>(*this); + return true; +} + +bool FFutureLatent::RunTest(const FString& Parameters) +{ + DoTest(*this); + return true; +} diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/Private/GeneratorTest.cpp b/Plugins/UE5Coro/Source/UE5CoroTests/Private/GeneratorTest.cpp new file mode 100644 index 00000000..99f53a03 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/Private/GeneratorTest.cpp @@ -0,0 +1,93 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "Misc/AutomationTest.h" +#include "UE5Coro/Generator.h" + +using namespace UE5Coro; + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FGeneratorTest, "UE5Coro.Generator", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::CriticalPriority | + EAutomationTestFlags::ProductFilter) + +TGenerator CountUp(int Max) +{ + for (int i = 0; i <= Max; ++i) + co_yield i; +} + +bool FGeneratorTest::RunTest(const FString& Parameters) +{ + { + auto Generator = []() -> TGenerator + { + co_yield 1.0; + }(); + TestEqual("Temporary type conversion", Generator.Current(), 1); + } + + { + TGenerator Generator = CountUp(2); + for (int i = 0; i <= 2; ++i) + { + TestEqual("Current", Generator.Current(), i); + TestEqual("Resume", Generator.Resume(), i != 2); + } + } + + { + TArray> Values; + + TGenerator Count2 = CountUp(2); + for (int i : Count2) + Values.Add(i); + // Not really classic iterator semantics but we can't rewind + TestEqual("begin()==end() at end", Count2.begin(), Count2.end()); + TestEqual("Values.Num()", Values.Num(), 3); + for (int i = 0; i <= 2; ++i) + TestEqual("Values[i]", Values[i], i); + } + + { + TGenerator Count2 = CountUp(2); + // This makes an iterator and discards it + TestNotEqual("begin()!=end() at start", Count2.begin(), Count2.end()); + auto i = Count2.CreateIterator(); + int j = 0; + for (; i; ++i) + TestEqual("*i==j", *i, j++); + TestEqual("iterator length", j, 3); + TestTrue("!i at end", !i); + } + + return true; +} diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/Private/HttpAwaiterTest.cpp b/Plugins/UE5Coro/Source/UE5CoroTests/Private/HttpAwaiterTest.cpp new file mode 100644 index 00000000..1d463254 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/Private/HttpAwaiterTest.cpp @@ -0,0 +1,127 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "HttpModule.h" +#include "TestWorld.h" +#include "Misc/AutomationTest.h" +#include "UE5Coro/AsyncAwaiters.h" +#include "UE5Coro/HttpAwaiters.h" +#include "UE5Coro/LatentAwaiters.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private::Test; + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FHttpAsyncTest, "UE5Coro.Http.Async", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FHttpLatentTest, "UE5Coro.Http.Latent", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +namespace +{ +template +void DoTest(FAutomationTestBase& Test) +{ + FTestWorld World; + + // Expecting the pre-5.3 behavior from UE4 + constexpr bool bExpectResponse = true; + + std::atomic bDone = false; + World.Run(CORO + { + auto Request = FHttpModule::Get().CreateRequest(); + // We're not testing HTTP, just the awaiter + Request->SetURL(TEXT(".invalid")); + Request->SetTimeout(0.01); + auto Awaiter = Http::ProcessAsync(Request); + auto AwaiterCopy = Awaiter; + auto [Response, bSuccess] = co_await AwaiterCopy; + Test.TestFalse(TEXT("Success"), bSuccess); + Test.TestEqual(TEXT("Response"), !!Response, bExpectResponse); + bSuccess = true; + Tie(Response, bSuccess) = co_await Awaiter; + Test.TestFalse(TEXT("Success"), bSuccess); + Test.TestEqual(TEXT("Response"), !!Response, bExpectResponse); + bDone = true; + }); + FTestHelper::PumpGameThread(World, [&] { return bDone.load(); }); + + bDone = false; + World.Run(CORO + { + co_await Async::MoveToThread( + ENamedThreads::AnyBackgroundThreadNormalTask); + FPlatformMisc::MemoryBarrier(); + Test.TestFalse(TEXT("Not in game thread"), IsInGameThread()); + auto Request = FHttpModule::Get().CreateRequest(); + // We're not testing HTTP, just the awaiter + Request->SetURL(TEXT(".invalid")); + Request->SetTimeout(0.01); + auto [Response, bSuccess] = co_await Http::ProcessAsync(Request); + // In UE4, this does not move back to the original thread + Test.TestTrue(TEXT("In game thread"), IsInGameThread()); + Test.TestFalse(TEXT("Success"), bSuccess); + Test.TestEqual(TEXT("Response"), !!Response, bExpectResponse); + FPlatformMisc::MemoryBarrier(); + bDone = true; + }); + // Test is being used by the coroutine on another thread here + FTestHelper::PumpGameThread(World, [&] { return bDone.load(); }); + + World.Run(CORO + { + auto Request = FHttpModule::Get().CreateRequest(); + // We're not testing HTTP, just the awaiter + Request->SetURL(TEXT(".invalid")); + Request->SetTimeout(0.01); + [[maybe_unused]] auto Unused = Http::ProcessAsync(Request); + co_await Latent::NextTick(); // Run() requires some co_await + }); + World.Tick(); // Nothing to test here besides not crashing +} +} + +bool FHttpAsyncTest::RunTest(const FString& Parameters) +{ + DoTest<>(*this); + return true; +} + +bool FHttpLatentTest::RunTest(const FString& Parameters) +{ + DoTest(*this); + return true; +} diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/Private/LatentAwaiterTest.cpp b/Plugins/UE5Coro/Source/UE5CoroTests/Private/LatentAwaiterTest.cpp new file mode 100644 index 00000000..109a2dd4 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/Private/LatentAwaiterTest.cpp @@ -0,0 +1,189 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "TestWorld.h" +#include "Kismet/GameplayStatics.h" +#include "Misc/AutomationTest.h" +#include "Misc/ScopeExit.h" +#include "UE5Coro/LatentAwaiters.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private::Test; + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FLatentAwaiterTest, "UE5Coro.Latent.TrueLatent", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FLatentInAsyncTest, "UE5Coro.Latent.Async", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +namespace +{ +template +void DoTest(FAutomationTestBase& Test) +{ + FTestWorld World; + + IF_CORO_LATENT + { + bool bStarted = false; + bool bDone = false; + auto Fn = CORO + { + ON_SCOPE_EXIT + { + bDone = true; + }; + bStarted = true; + co_return; + }; + World.Run(Fn); + + Test.TestTrue(TEXT("Null latent coroutine started"), bStarted); + Test.TestTrue(TEXT("Null latent coroutine finished"), bDone); + } + + { + int State = 0; + World.Run(CORO + { + State = 1; + co_await Latent::NextTick(); + State = 2; + }); + World.EndTick(); + Test.TestEqual(TEXT("NextTick 1"), State, 1); + World.Tick(); + Test.TestEqual(TEXT("NextTick 2"), State, 2); + } + + { + int State = 0; + World.Run(CORO + { + State = 1; + co_await Latent::Ticks(2); + State = 2; + }); + World.EndTick(); + Test.TestEqual(TEXT("Ticks 1-1"), State, 1); + World.Tick(); + Test.TestEqual(TEXT("Ticks 1-2"), State, 1); + World.Tick(); + Test.TestEqual(TEXT("Ticks 2"), State, 2); + } + + { + // Having this here works around a compiler issue (MSVC 19.33.31629) + // causing the lambdas to "mis-capture" State and RealState + volatile int msvc_workaround = 0; + + int State = 0; + int RealState = 0; + World.Run(CORO + { + State = 1; + co_await Latent::Seconds(1); + State = 2; + }); + World.Run(CORO + { + RealState = 1; + co_await Latent::RealSeconds(1); + RealState = 2; + }); + World.EndTick(); + UGameplayStatics::SetGlobalTimeDilation(GWorld, 0.1); + Test.TestEqual(TEXT("Seconds 1-1"), State, 1); + Test.TestEqual(TEXT("RealSeconds 1-1"), RealState, 1); + World.Tick(0.95); + Test.TestEqual(TEXT("Seconds 1-2"), State, 1); + Test.TestEqual(TEXT("RealSeconds 1-2"), RealState, 1); + World.Tick(0.1); // Crossing 1.0s here + GWorld->bDebugPauseExecution = false; + Test.TestEqual(TEXT("Seconds 1-3"), State, 1); + Test.TestEqual(TEXT("RealSeconds 2"), RealState, 2); + UGameplayStatics::SetGlobalTimeDilation(GWorld, 1); + // The dilated coroutine only had around 0.1 seconds, let it complete + World.Tick(1); + Test.TestEqual(TEXT("Seconds 2"), State, 2); + } + + { + int State = 0; + int RealState = 0; + World.Run(CORO + { + State = 1; + auto Time = World->GetTimeSeconds(); + co_await Latent::UntilTime(Time + 1); + State = 2; + }); + World.Run(CORO + { + RealState = 1; + auto Time = World->GetRealTimeSeconds(); + co_await Latent::UntilRealTime(Time + 1); + RealState = 2; + }); + World.EndTick(); + UGameplayStatics::SetGlobalTimeDilation(GWorld, 0.1); + Test.TestEqual(TEXT("UntilTime 1-1"), State, 1); + Test.TestEqual(TEXT("UntilRealTime 1-1"), RealState, 1); + World.Tick(0.95); + Test.TestEqual(TEXT("UntilTime 1-2"), State, 1); + Test.TestEqual(TEXT("UntilRealTime 1-2"), RealState, 1); + World.Tick(0.1); // Crossing 1.0s here + GWorld->bDebugPauseExecution = false; + Test.TestEqual(TEXT("UntilTime 1-3"), State, 1); + Test.TestEqual(TEXT("UntilRealTime 2"), RealState, 2); + UGameplayStatics::SetGlobalTimeDilation(GWorld, 1); + // The dilated coroutine only had around 0.1 seconds, let it complete + World.Tick(1); + Test.TestEqual(TEXT("UntilTime 2"), State, 2); + } +} +} + +bool FLatentAwaiterTest::RunTest(const FString& Parameters) +{ + DoTest(*this); + return true; +} + +bool FLatentInAsyncTest::RunTest(const FString& Parameters) +{ + DoTest<>(*this); + return true; +} diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/Private/LatentCallbackTest.cpp b/Plugins/UE5Coro/Source/UE5CoroTests/Private/LatentCallbackTest.cpp new file mode 100644 index 00000000..04691bbf --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/Private/LatentCallbackTest.cpp @@ -0,0 +1,129 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "TestWorld.h" +#include "UE5CoroTestObject.h" +#include "Misc/AutomationTest.h" +#include "UE5Coro/Cancellation.h" +#include "UE5Coro/LatentAwaiters.h" +#include "UE5Coro/LatentCallbacks.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private::Test; + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FLatentCallbackTest, "UE5Coro.Latent.Callbacks", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +FAsyncCoroutine UUE5CoroTestObject::ObjectDestroyedTest( + int& State, bool& bAbnormal, bool& bCanceled, FLatentActionInfo) +{ + State = 1; + Latent::FOnActionAborted A([&] { State = 2; }); + Latent::FOnObjectDestroyed B([&] { State = 3; }); + Latent::FOnAbnormalExit C([&] { bAbnormal = true; }); + FOnCoroutineCanceled D([&] { bCanceled = true; }); + co_await Latent::Ticks(10); + State = 10; +} + +bool FLatentCallbackTest::RunTest(const FString& Parameters) +{ + int State = 0; + bool bCanceled = false; + { + FTestWorld World; + World.Run([&](FLatentActionInfo) -> TCoroutine<> + { + FOnCoroutineCanceled _([&] { bCanceled = true; }); + ON_SCOPE_EXIT { State = 2; }; + State = 1; + co_await Latent::NextTick(); + }); + TestEqual(TEXT("Initial state"), State, 1); + } + TestEqual(TEXT("On scope exit"), State, 2); + TestTrue(TEXT("Canceled"), bCanceled); + + { + FTestWorld World; + bool bAbnormal = false; + bCanceled = false; + auto* Object = NewObject(); + Object->ObjectDestroyedTest(State, bAbnormal, bCanceled, + {0, 0, TEXT("Core"), Object}); + World.EndTick(); + TestEqual(TEXT("Initial state"), State, 1); + for (int i = 0; i < 10; ++i) + { + TestEqual(TEXT("No early resume"), State, 1); + World.Tick(); + } + TestEqual(TEXT("Resumed state"), State, 10); + World.Tick(); + TestFalse(TEXT("Normal exit"), bAbnormal); + TestFalse(TEXT("Not canceled"), bCanceled); + } + + { + FTestWorld World; + bool bAbnormal = false; + bCanceled = false; + auto* Object = NewObject(); + Object->ObjectDestroyedTest(State, bAbnormal, bCanceled, + {0, 0, TEXT("Core"), Object}); + TestEqual(TEXT("Initial state"), State, 1); + auto& LAM = World->GetLatentActionManager(); + LAM.RemoveActionsForObject(Object); + World.Tick(); + TestEqual(TEXT("On action aborted"), State, 2); + TestTrue(TEXT("Abnormal exit"), bAbnormal); + TestTrue(TEXT("Implicitly canceled"), bCanceled); + } + + { + FTestWorld World; + bool bAbnormal = false; + bCanceled = false; + auto* Object = NewObject(); + Object->ObjectDestroyedTest(State, bAbnormal, bCanceled, + {0, 0, TEXT("Core"), Object}); + TestEqual(TEXT("Initial state"), State, 1); + Object->MarkPendingKill(); + CollectGarbage(RF_NoFlags); + World.Tick(); + TestEqual(TEXT("On object destroyed"), State, 3); + TestTrue(TEXT("Abnormal exit"), bAbnormal); + TestTrue(TEXT("Implicitly canceled"), bCanceled); + } + return true; +} diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/Private/LatentChainCancellationTest.cpp b/Plugins/UE5Coro/Source/UE5CoroTests/Private/LatentChainCancellationTest.cpp new file mode 100644 index 00000000..8b9a218b --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/Private/LatentChainCancellationTest.cpp @@ -0,0 +1,123 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "TestWorld.h" +#include "Kismet/KismetSystemLibrary.h" +#include "Misc/AutomationTest.h" +#include "UE5Coro/LatentAwaiters.h" + +using namespace std::placeholders; +using namespace UE5Coro; +using namespace UE5Coro::Private; +using namespace UE5Coro::Private::Test; + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FAsyncChainCancelTest, "UE5Coro.Chain.Cancel.Async", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FLatentChainCancelTest, "UE5Coro.Chain.Cancel.Latent", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +namespace UE5Coro::Private +{ +extern UE5CORO_API UClass* ChainCallbackTarget_StaticClass(); +} + +namespace +{ +template +void DoTest(FAutomationTestBase& Test) +{ + FTestWorld World; + int State = 0; + + // The order between Chain and the chained latent actions' Ticks is not + // fixed, so allow one extra tick if needed + auto DoubleTick = [&](int ExpectedState, float DeltaSeconds) + { + World.Tick(DeltaSeconds); + if (State != ExpectedState) + World.Tick(0); + Test.TestEqual(TEXT("Latent state"), State, ExpectedState); + }; + + auto ExpectFail = [&](bool bValue) + { + Test.TestFalse(TEXT("Chain aborted"), bValue); + }; + + { + TSet Targets; + for (auto* Target : TObjectRange()) + if (Target->IsA(ChainCallbackTarget_StaticClass())) + Targets.Add(Target); + + World.Run(CORO + { + State = 1; +#if UE5CORO_PRIVATE_LATENT_CHAIN_IS_OK + ExpectFail(co_await Latent::Chain(&UKismetSystemLibrary::Delay, 1)); +#else + ExpectFail(co_await Latent::ChainEx(&UKismetSystemLibrary::Delay, + _1, 1, _2)); +#endif + State = 2; + }); + Test.TestEqual(TEXT("Started"), State, 1); + UObject* NewTarget = nullptr; + for (auto* Target : TObjectRange()) + if (Target->IsA(ChainCallbackTarget_StaticClass()) && + !Targets.Contains(Target)) + { + NewTarget = Target; + break; + } + Test.TestNotNull(TEXT("Callback target found"), NewTarget); + World->GetLatentActionManager().RemoveActionsForObject(NewTarget); + DoubleTick(2, 0); // Removals are only processed on the next tick + } +} +} + +bool FAsyncChainCancelTest::RunTest(const FString& Parameters) +{ + DoTest<>(*this); + return true; +} + +bool FLatentChainCancelTest::RunTest(const FString& Parameters) +{ + DoTest(*this); + return true; +} diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/Private/LatentChainTest.cpp b/Plugins/UE5Coro/Source/UE5CoroTests/Private/LatentChainTest.cpp new file mode 100644 index 00000000..3be1b064 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/Private/LatentChainTest.cpp @@ -0,0 +1,183 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "TestWorld.h" +#include "UE5CoroTestObject.h" +#include "Kismet/KismetSystemLibrary.h" +#include "Misc/AutomationTest.h" +#include "UE5Coro/AggregateAwaiters.h" +#include "UE5Coro/LatentAwaiters.h" + +using namespace std::placeholders; +using namespace UE5Coro; +using namespace UE5Coro::Private::Test; + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FAsyncChainTest, "UE5Coro.Chain.Async", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FLatentChainTest, "UE5Coro.Chain.Latent", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +namespace +{ +template +void DoTest(FAutomationTestBase& Test) +{ + FTestWorld World; + int State = 0; + + // The order between Chain and the chained latent actions' Ticks is not + // fixed, so allow one extra tick if needed + auto DoubleTick = [&](int ExpectedState, float DeltaSeconds) + { + World.Tick(DeltaSeconds); + if (State != ExpectedState) + World.Tick(0); + Test.TestEqual(TEXT("Latent state"), State, ExpectedState); + }; + + auto ExpectSuccess = [&](bool bValue) + { + Test.TestTrue(TEXT("Chain not aborted"), bValue); + }; + + { + State = 0; + World.Run(CORO + { + State = 1; +#if UE5CORO_PRIVATE_LATENT_CHAIN_IS_OK + ExpectSuccess(co_await Latent::Chain( + &UKismetSystemLibrary::Delay, 0)); +#else + ExpectSuccess(co_await Latent::ChainEx( + &UKismetSystemLibrary::Delay, _1, 0, _2)); +#endif + State = 2; + ExpectSuccess(co_await Latent::ChainEx( + &UKismetSystemLibrary::Delay, _1, 0, _2)); + State = 3; + }); + Test.TestEqual(TEXT("Initial state"), State, 1); + DoubleTick(2, 0); + DoubleTick(3, 0); + } + + { + State = 0; + World.Run(CORO + { + State = 1; + ExpectSuccess(co_await Latent::ChainEx( + &UKismetSystemLibrary::Delay, _1, 1, _2)); + State = 2; +#if UE5CORO_PRIVATE_LATENT_CHAIN_IS_OK + ExpectSuccess(co_await Latent::Chain(&UKismetSystemLibrary::Delay, + 1)); +#else + ExpectSuccess(co_await Latent::ChainEx(&UKismetSystemLibrary::Delay, + _1, 1, _2)); +#endif + State = 3; + }); + Test.TestEqual(TEXT("Initial state"), State, 1); + World.Tick(0.5); + Test.TestEqual(TEXT("Half state"), State, 1); + DoubleTick(2, 1); + DoubleTick(3, 1.01); + } + + { + State = 0; + World.Run(CORO + { + State = 1; + auto* Obj = NewObject(); + TStrongObjectPtr KeepAlive(Obj); +#if UE5CORO_PRIVATE_LATENT_CHAIN_IS_OK + ExpectSuccess(co_await Latent::Chain(&UUE5CoroTestObject::Latent, + Obj)); +#else + ExpectSuccess(co_await Latent::ChainEx(&UUE5CoroTestObject::Latent, + Obj, _2)); +#endif + State = 2; + ExpectSuccess(co_await Latent::ChainEx(&UUE5CoroTestObject::Latent, + Obj, _2)); + State = 3; + }); + Test.TestEqual(TEXT("Initial state"), State, 1); + DoubleTick(2, 0); + DoubleTick(3, 0); + } + + { + State = -1; + World.Run(CORO + { +#if UE5CORO_PRIVATE_LATENT_CHAIN_IS_OK + // The next line passes 0 instead of 1 on older versions of MSVC + auto Chain1 = Latent::Chain(&UKismetSystemLibrary::Delay, 1); + auto Chain2 = + Latent::Chain(&UKismetSystemLibrary::Delay, 0); +#else + auto Chain1 = + Latent::ChainEx(&UKismetSystemLibrary::Delay, _1, 1, _2); + auto Chain2 = Latent::ChainEx( + &UKismetSystemLibrary::Delay, _1, 0, _2); +#endif + State = 0; + State = co_await WhenAny(std::move(Chain1), std::move(Chain2)); + }); + World.EndTick(); + Test.TestEqual(TEXT("Initial state"), State, 0); + DoubleTick(1, 0); + World.Tick(2); + World.Tick(2); + } +} +} + +bool FAsyncChainTest::RunTest(const FString& Parameters) +{ + DoTest<>(*this); + return true; +} + +bool FLatentChainTest::RunTest(const FString& Parameters) +{ + DoTest(*this); + return true; +} diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/Private/ReturnValueTest.cpp b/Plugins/UE5Coro/Source/UE5CoroTests/Private/ReturnValueTest.cpp new file mode 100644 index 00000000..a6cff0b3 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/Private/ReturnValueTest.cpp @@ -0,0 +1,167 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "TestWorld.h" +#include "Misc/AutomationTest.h" +#include "Misc/ScopeExit.h" +#include "UE5Coro/CoroutineAwaiters.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private; +using namespace UE5Coro::Private::Test; + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FLatentReturnTest, "UE5Coro.Return.Latent", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FAsyncReturnTest, "UE5Coro.Return.Async", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +#if defined(_MSC_VER) && _MSC_VER >= 1930 +// MSVC workaround - DoTest is not a coroutine but it won't compile without this +template<> +struct stdcoro::coroutine_traits +{ + using promise_type = + UE5Coro::Private::TCoroutinePromise; +}; +#endif + +namespace +{ +template +void DoTest(FAutomationTestBase& Test) +{ + FTestWorld World; + + { + auto A = World.Run(CORO_R(int) { co_return 1; }); + auto B = World.Run(CORO_R(int) { co_return 1.0; }); + Test.TestEqual(TEXT("Return value passthrough"), A.MoveResult(), 1); + Test.TestEqual(TEXT("Implicit conversion"), B.MoveResult(), 1); + } + + { + bool bSuccess = false; + World.Run(CORO + { + // Always async inner + bSuccess = co_await World.Run([&]() -> TCoroutine + { + co_return true; + }); + }); + Test.TestTrue(TEXT("co_await result"), bSuccess); + } + + { + bool bSuccess = false; + // Always async outer + World.Run([&]() -> TCoroutine<> + { + bSuccess = co_await World.Run(CORO_R(bool) + { + co_return true; + }); + }); + Test.TestTrue(TEXT("co_await result"), bSuccess); + } + + { + bool bSuccess = false; + bool bInnerReturned = false; + World.Run(CORO + { + bSuccess = co_await World.Run(CORO_R(bool) + { + ON_SCOPE_EXIT { bInnerReturned = true; }; + co_return true; + }); + }); + Test.TestTrue(TEXT("Inner returned"), bInnerReturned); + Test.TestTrue(TEXT("co_await result"), bSuccess); + } + + { + bool bInnerComplete = false; + auto Coro = World.Run(CORO_R(TArray) + { + auto InnerCoro = World.Run(CORO_R(TArray) + { + ON_SCOPE_EXIT { bInnerComplete = true; }; + co_await Latent::NextTick(); + co_return {1, 2, 3}; + }); + + for (int i = 0; i < 2; ++i) // Test double await + { + decltype(auto) Array = co_await InnerCoro; // lvalue + static_assert(std::is_same_v>); + Test.TestEqual(TEXT("Array Num"), Array.Num(), 3); + Test.TestEqual(TEXT("Array[0]"), Array[0], 1); + Test.TestEqual(TEXT("Array[1]"), Array[1], 2); + Test.TestEqual(TEXT("Array[2]"), Array[2], 3); + } + + co_return {4}; + }); + World.EndTick(); + Test.TestFalse(TEXT("Inner not complete yet"), bInnerComplete); + World.Tick(); // NextTick + IF_CORO_LATENT + { + Test.TestFalse(TEXT("Outer not complete yet"), Coro.IsDone()); + Test.TestTrue(TEXT("Inner complete"), bInnerComplete); + World.Tick(); // Outer completion + } + Test.TestTrue(TEXT("Outer complete"), Coro.IsDone()); + Test.TestTrue(TEXT("Outer successful"), Coro.WasSuccessful()); + auto Array = Coro.MoveResult(); + Test.TestEqual(TEXT("Outer array Num"), Array.Num(), 1); + Test.TestEqual(TEXT("Outer array[0]"), Array[0], 4); + } +} +} + +bool FAsyncReturnTest::RunTest(const FString& Parameters) +{ + DoTest<>(*this); + return true; +} + +bool FLatentReturnTest::RunTest(const FString& Parameters) +{ + DoTest(*this); + return true; +} diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/Private/TestDelegates.h b/Plugins/UE5Coro/Source/UE5CoroTests/Private/TestDelegates.h new file mode 100644 index 00000000..6b710ac4 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/Private/TestDelegates.h @@ -0,0 +1,55 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "TestDelegates.generated.h" + +USTRUCT() +struct FUE5CoroTestConstructionChecker +{ + GENERATED_BODY() + static inline bool bConstructed = false; + FUE5CoroTestConstructionChecker() { bConstructed = true; } +}; + +DECLARE_DYNAMIC_DELEGATE(FUE5CoroTestDynamicVoidDelegate); +DECLARE_DYNAMIC_DELEGATE_TwoParams(FUE5CoroTestDynamicParamsDelegate, + int, A, int&, B); +DECLARE_DYNAMIC_DELEGATE_RetVal(FUE5CoroTestConstructionChecker, + FUE5CoroTestDynamicRetvalDelegate); +DECLARE_DYNAMIC_DELEGATE_RetVal_TwoParams(FUE5CoroTestConstructionChecker, + FUE5CoroTestDynamicAllDelegate, + int, A, int&, B); +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FUE5CoroTestDynamicMulticastVoidDelegate); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams( + FUE5CoroTestDynamicMulticastParamsDelegate, int, A, int&, B); diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/Private/TestWorld.cpp b/Plugins/UE5Coro/Source/UE5CoroTests/Private/TestWorld.cpp new file mode 100644 index 00000000..a3a22f8e --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/Private/TestWorld.cpp @@ -0,0 +1,88 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "TestWorld.h" +#include "HAL/ThreadManager.h" + +using namespace UE5Coro::Private::Test; + +FTestWorld::FTestWorld() + : World(UWorld::CreateWorld(EWorldType::Game, false)) +{ + check(IsInGameThread()); + auto& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Game); + WorldContext.SetCurrentWorld(World); + PrevWorld = &*GWorld; + OldFrameCounter = GFrameCounter; + GWorld = World; + World->InitializeActorsForPlay(FURL()); + auto* Settings = World->GetWorldSettings(); + Settings->MinUndilatedFrameTime = 0.0001; + Settings->MaxUndilatedFrameTime = 10; + World->BeginPlay(); +} + +FTestWorld::~FTestWorld() +{ + GEngine->DestroyWorldContext(World); + World->DestroyWorld(true); + GWorld = PrevWorld; + GFrameCounter = OldFrameCounter; + CollectGarbage(RF_NoFlags); +} + +void FTestWorld::Tick(float DeltaSeconds) +{ + check(IsInGameThread()); + StaticTick(DeltaSeconds); + World->Tick(LEVELTICK_All, DeltaSeconds); + EndTick(); + + FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GameThread); + + // Other things that need ticking. Reference: FEngineLoop::Tick() + FTicker::GetCoreTicker().Tick(FApp::GetDeltaTime()); + FThreadManager::Get().Tick(); + GEngine->TickDeferredCommands(); +} + +void FTestWorld::EndTick() +{ + check(IsInGameThread()); + ++GFrameCounter; +} + +void FTestHelper::PumpGameThread(FTestWorld& World, + std::function ExitCondition) +{ + while (!ExitCondition()) + World.Tick(); +} diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/Private/UE5CoroTestObject.h b/Plugins/UE5Coro/Source/UE5CoroTests/Private/UE5CoroTestObject.h new file mode 100644 index 00000000..b245c4fc --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/Private/UE5CoroTestObject.h @@ -0,0 +1,76 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#include "Kismet/KismetSystemLibrary.h" +#include "UE5Coro/LatentAwaiters.h" +#include "UE5CoroTestObject.generated.h" + +class UUE5CoroTestObject; + +DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE(FUE5CoroTestSparseDelegate, + UUE5CoroTestObject, SparseDelegate); +DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_TwoParams( + FUE5CoroTestSparseParamsDelegate, UUE5CoroTestObject, SparseParamsDelegate, + int, A, const int&, B); + +UCLASS(MinimalAPI) +class UUE5CoroTestObject : public UObject +{ + GENERATED_BODY() + +public: + UPROPERTY(BlueprintAssignable) + FUE5CoroTestSparseDelegate SparseDelegate; + UPROPERTY(BlueprintAssignable) + FUE5CoroTestSparseParamsDelegate SparseParamsDelegate; + + std::function Callback; + + UFUNCTION() + void Core() + { + if (Callback) + Callback(); + } + + virtual UWorld* GetWorld() const override { return GWorld; } + + void Latent(FLatentActionInfo LatentInfo) + { + UKismetSystemLibrary::Delay(this, 0, LatentInfo); + } + + FAsyncCoroutine ObjectDestroyedTest(int&, bool&, bool&, FLatentActionInfo); +}; diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/Private/UE5CoroTests.cpp b/Plugins/UE5Coro/Source/UE5CoroTests/Private/UE5CoroTests.cpp new file mode 100644 index 00000000..a2c93487 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/Private/UE5CoroTests.cpp @@ -0,0 +1,38 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "Modules/ModuleManager.h" + +class FUE5CoroTestsModule : public IModuleInterface +{ +}; + +IMPLEMENT_MODULE(FUE5CoroTestsModule, UE5CoroTests); diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/Public/TestWorld.h b/Plugins/UE5Coro/Source/UE5CoroTests/Public/TestWorld.h new file mode 100644 index 00000000..300e1971 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/Public/TestWorld.h @@ -0,0 +1,92 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#include +#include "UE5Coro/AsyncCoroutine.h" +#include "UE5Coro/UE5CoroSubsystem.h" + +#define CORO [&](T...) -> FAsyncCoroutine +#define CORO_R(Type) [&](T...) -> TCoroutine +#define IF_CORO_LATENT if constexpr (sizeof...(T) == 1) +#define IF_NOT_CORO_LATENT if constexpr (sizeof...(T) != 1) + +namespace UE5Coro::Private::Test +{ +class UE5COROTESTS_API FTestWorld +{ + UWorld* World; + + UWorld* PrevWorld; + decltype(GFrameCounter) OldFrameCounter; + +public: + FTestWorld(); + ~FTestWorld(); + + UWorld* operator->() const { return World; } + + void Tick(float DeltaSeconds = 0.125); + void EndTick(); + + template + std::invoke_result_t Run(T Fn) + { + // Extend the lifetime of Fn's lambda captures until it's complete + auto* Copy = new T(std::move(Fn)); + auto Coro = (*Copy)(); + Coro.ContinueWith([=] { delete Copy; }); + return Coro; + } + + template + std::invoke_result_t Run(T Fn) + { + auto* Sys = World->GetSubsystem(); + auto LatentInfo = Sys->MakeLatentInfo(); + + auto* Copy = new T(std::move(Fn)); + auto Coro = (*Copy)(LatentInfo); + Coro.ContinueWith([=] { delete Copy; }); + return Coro; + } +}; + +class UE5COROTESTS_API FTestHelper +{ +public: + static void PumpGameThread(FTestWorld& World, + std::function ExitCondition); +}; +} diff --git a/Plugins/UE5Coro/Source/UE5CoroTests/UE5CoroTests.Build.cs b/Plugins/UE5Coro/Source/UE5CoroTests/UE5CoroTests.Build.cs new file mode 100644 index 00000000..85db0ed7 --- /dev/null +++ b/Plugins/UE5Coro/Source/UE5CoroTests/UE5CoroTests.Build.cs @@ -0,0 +1,45 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using UnrealBuildTool; + +public class UE5CoroTests : UE5CoroModuleRules +{ + public UE5CoroTests(ReadOnlyTargetRules Target) + : base(Target) + { + PublicDependencyModuleNames.AddRange(new[] + { + "HTTP", + "UE5Coro", + }); + } +} diff --git a/Plugins/UE5Coro/UE5Coro.uplugin b/Plugins/UE5Coro/UE5Coro.uplugin new file mode 100644 index 00000000..83e91829 --- /dev/null +++ b/Plugins/UE5Coro/UE5Coro.uplugin @@ -0,0 +1,35 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.10.2-ue4", + "FriendlyName": "UE5Coro (UE4 edition)", + "Description": "C++20 coroutine implementation for Unreal Engine 4", + "Category": "Programming", + "CreatedBy": "Laura Andelare", + "CreatedByURL": "https://github.com/landelare/ue5coro", + "DocsURL": "", + "MarketplaceURL": "", + "SupportURL": "", + "EnabledByDefault": true, + "CanContainContent": false, + "IsBetaVersion": false, + "IsExperimentalVersion": true, + "Installed": false, + "Modules": [ + { + "Name": "UE5Coro", + "Type": "Runtime", + "LoadingPhase": "PostConfigInit" + }, + { + "Name": "UE5CoroK2", + "Type": "UncookedOnly", + "LoadingPhase": "PostConfigInit" + }, + { + "Name": "UE5CoroTests", + "Type": "UncookedOnly", + "LoadingPhase": "PostConfigInit" + } + ] +} diff --git a/Plugins/UE5CoroAI/Resources/Icon128.png b/Plugins/UE5CoroAI/Resources/Icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..a3a963f3d0640f8b2910ca407d239610fa152374 GIT binary patch literal 17774 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 Y2h-un`t$)J3jhEB07*qoM6N<$f*QfUMgRZ+ literal 0 HcmV?d00001 diff --git a/Plugins/UE5CoroAI/Source/UE5CoroAI/Private/AIAwaiters.cpp b/Plugins/UE5CoroAI/Source/UE5CoroAI/Private/AIAwaiters.cpp new file mode 100644 index 00000000..2573dead --- /dev/null +++ b/Plugins/UE5CoroAI/Source/UE5CoroAI/Private/AIAwaiters.cpp @@ -0,0 +1,333 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5CoroAI/AIAwaiters.h" +#include "AIController.h" +#include "NavigationSystem.h" +#include "UE5CoroAICallbackTarget.h" + +using namespace UE5Coro; +using namespace UE5Coro::AI; +using namespace UE5Coro::Private; + +namespace +{ +template +concept TGoal = std::same_as || std::same_as; + +struct FFindPathState +{ + TWeakObjectPtr NS1; + uint32 QueryID; + TTuple Result; +}; +using FFindPathSharedPtr = TSharedPtr; + +bool ShouldResumeFindPath(void* State, bool bCleanup) +{ + auto* This = static_cast(State); + if (UNLIKELY(bCleanup)) + { + if (auto* NS1 = (*This)->NS1.Get(); + NS1 && (*This)->QueryID != INVALID_NAVQUERYID) + NS1->AbortAsyncFindPathRequest((*This)->QueryID); + delete This; + } + + return (*This)->QueryID == INVALID_NAVQUERYID; +} + +bool ShouldResumeMoveTo(void* State, bool bCleanup) +{ + auto* Target = static_cast*>(State); + if (UNLIKELY(bCleanup)) + { + delete Target; + return false; + } + + return (*Target)->GetResult().has_value(); +} + +constexpr bool IsValid(const FVector&) +{ + return true; +} + +FMoveToAwaiter AIMoveToCore(AAIController* Controller, TGoal auto Target, + float AcceptanceRadius, + EAIOptionFlag::Type StopOnOverlap, + EAIOptionFlag::Type AcceptPartialPath, + bool bUsePathfinding, bool bLockAILogic, + bool bUseContinuousGoalTracking, + EAIOptionFlag::Type ProjectGoalOnNavigation) +{ + checkf(IsInGameThread(), + TEXT("This method may only be called from the game thread")); + checkf(IsValid(Controller), TEXT("Attempting to move invalid controller")); + checkf(IsValid(Target), TEXT("Attempting to move to invalid target")); +#if ENABLE_NAN_DIAGNOSTIC + if (FMath::IsNaN(AcceptanceRadius)) + { + logOrEnsureNanError(TEXT("AsyncMoveTo started with NaN radius")); + } +#endif + + FVector Vector; + AActor* Actor; + if constexpr (std::convertible_to) + std::tie(Vector, Actor) = std::tuple(Target, nullptr); + else + std::tie(Vector, Actor) = std::tuple(FVector::ZeroVector, Target); + return FMoveToAwaiter(UAITask_MoveTo::AIMoveTo( + Controller, Vector, Actor, AcceptanceRadius, StopOnOverlap, + AcceptPartialPath, bUsePathfinding, bLockAILogic, + bUseContinuousGoalTracking, ProjectGoalOnNavigation)); +} + +FSimpleMoveToAwaiter SimpleMoveToCore(AController* Controller, TGoal auto Target) +{ + checkf(IsInGameThread(), + TEXT("This method may only be called from the game thread")); + checkf(IsValid(Controller), TEXT("Attempting to move invalid controller")); + checkf(IsValid(Controller->GetPawn()), + TEXT("Attempting to move invalid pawn")); + checkf(IsValid(Target), TEXT("Attempting to move to invalid target")); + + auto* World = Controller->GetWorld(); + auto* NS1 = FNavigationSystem::GetCurrent(World); + checkf(IsValid(NS1), TEXT("Cannot perform move without navigation system")); + + // This recreates InitNavigationControl's component injection + UPathFollowingComponent* PathFollow; + if (auto* AIC = Cast(Controller); IsValid(AIC)) + PathFollow = AIC->GetPathFollowingComponent(); + else + { + PathFollow = Controller->FindComponentByClass(); + if (!IsValid(PathFollow)) + { + PathFollow = NewObject(Controller); + PathFollow->RegisterComponentWithWorld(Controller->GetWorld()); + // The original does not call AddInstanceComponent + PathFollow->Initialize(); + } + } + + // Fail instantly if the PFC can't be used + if (!IsValid(PathFollow) || !PathFollow->IsPathFollowingAllowed()) + return FSimpleMoveToAwaiter(EPathFollowingResult::Invalid); + + FVector From = Controller->GetNavAgentLocation(); + FVector To; + if constexpr (std::convertible_to) + To = Target->GetActorLocation(); + else + To = Target; + + bool bAlreadyThere = PathFollow->HasReached( + To, EPathFollowingReachMode::OverlapAgentAndGoal); + + // Abort the previous move if there was any + constexpr auto Flags = FPathFollowingResultFlags::ForcedScript | + FPathFollowingResultFlags::NewRequest; + if (PathFollow->GetStatus() != EPathFollowingStatus::Idle) + PathFollow->AbortMove(*NS1, Flags, FAIRequestID::AnyRequest, + bAlreadyThere ? EPathFollowingVelocityMode::Reset + : EPathFollowingVelocityMode::Keep); + + // Early exits for immediate failures/successes + ANavigationData* NavData = NS1->GetNavDataForProps( + Controller->GetNavAgentPropertiesRef(), From); + if (!IsValid(NavData)) + return FSimpleMoveToAwaiter(EPathFollowingResult::Invalid); + + if (bAlreadyThere) + { + PathFollow->RequestMoveWithImmediateFinish(EPathFollowingResult::Success); + return FSimpleMoveToAwaiter(EPathFollowingResult::Success); + } + + FPathFindingQuery Query(Controller, *NavData, From, To); + // Not calling FindPathAsync to match the original + FPathFindingResult Result = NS1->FindPathSync(Query); + if (Result.IsSuccessful()) + { + if constexpr (std::convertible_to) + // Matching the hardcoded constant from UAIBlueprintHelperLibrary + Result.Path->SetGoalActorObservation(*Target, 100); + FAIRequestID ID = PathFollow->RequestMove(FAIMoveRequest(To), + Result.Path); + + // The interesting case + return FSimpleMoveToAwaiter(PathFollow, ID); + } + + if (PathFollow->GetStatus() != EPathFollowingStatus::Idle) + { + PathFollow->RequestMoveWithImmediateFinish(EPathFollowingResult::Invalid); + return FSimpleMoveToAwaiter(EPathFollowingResult::Invalid); + } + + return FSimpleMoveToAwaiter(EPathFollowingResult::Invalid); +} +} + +FPathFindingAwaiter::FPathFindingAwaiter(void* State) + : FLatentAwaiter(State, &ShouldResumeFindPath) +{ +} + +auto FPathFindingAwaiter::await_resume() + -> TTuple +{ + auto* This = static_cast(State); + checkf((*This)->QueryID == INVALID_NAVQUERYID, + TEXT("Internal error: spurious resume")); + return (*This)->Result; +} + + +FMoveToAwaiter::FMoveToAwaiter(UAITask_MoveTo* Task) + : FLatentAwaiter(new TStrongObjectPtr(NewObject() + ->SetTask(Task)), &ShouldResumeMoveTo) +{ +} + +EPathFollowingResult::Type FMoveToAwaiter::await_resume() noexcept +{ + auto* Target = static_cast*>(State); + checkf((*Target)->GetResult().has_value(), + TEXT("Internal error: resuming with no result")); + return *(*Target)->GetResult(); +} + +void FSimpleMoveToAwaiter::FComplexData::RequestFinished( + FAIRequestID InID, const FPathFollowingResult& InResult) +{ + if (RequestID == InID) + Result = InResult; +} + +bool FSimpleMoveToAwaiter::ShouldResume(void* State, bool bCleanup) +{ + auto* Data = static_cast(State); + if (UNLIKELY(bCleanup)) + { + if (auto* Component = Data->PathFollow.Get()) + Component->OnRequestFinished.Remove(Data->Handle); + delete Data; + return false; + } + return Data->Result.has_value(); +} + +FSimpleMoveToAwaiter::FSimpleMoveToAwaiter(EPathFollowingResult::Type Result) + : FLatentAwaiter(new FComplexData{.Result = {Result}}, &ShouldResume) +{ +} + +FSimpleMoveToAwaiter::FSimpleMoveToAwaiter(UPathFollowingComponent* PFC, + FAIRequestID ID) + : FLatentAwaiter(new FComplexData{.RequestID = ID, .PathFollow = PFC}, + &ShouldResume) +{ + auto* Data = static_cast(State); + Data->Handle = PFC->OnRequestFinished.AddRaw(Data, + &FComplexData::RequestFinished); +} + +FPathFollowingResult FSimpleMoveToAwaiter::await_resume() noexcept +{ + auto* Data = static_cast(State); + checkf(Data->Result.has_value(), TEXT("Internal error: spurious wakeup")); + return *Data->Result; +} + +FPathFindingAwaiter AI::FindPath(UObject* WorldContextObject, + const FPathFindingQuery& Query, + EPathFindingMode::Type Mode) +{ + checkf(IsValid(WorldContextObject), TEXT("Invalid WCO supplied")); + auto* World = WorldContextObject->GetWorld(); + checkf(IsValid(World), TEXT("Invalid world from WCO")); + auto* NS1 = CastChecked(World->GetNavigationSystem()); + auto State = MakeShared(); + auto Delegate = FNavPathQueryDelegate::CreateLambda( + [State](uint32 QueryID, ENavigationQueryResult::Type Result, + FNavPathSharedPtr Path) + { + checkf(QueryID == State->QueryID, + TEXT("Internal error: QueryID mismatch")); + State->Result = MakeTuple(Result, std::move(Path)); + State->QueryID = INVALID_NAVQUERYID; + }); + State->NS1 = NS1; + State->QueryID = NS1->FindPathAsync(Query.NavAgentProperties, Query, + Delegate, Mode); + return FPathFindingAwaiter(new FFindPathSharedPtr(std::move(State))); +} + +FMoveToAwaiter AI::AIMoveTo(AAIController* Controller, FVector Target, + float AcceptanceRadius, + EAIOptionFlag::Type StopOnOverlap, + EAIOptionFlag::Type AcceptPartialPath, + bool bUsePathfinding, bool bLockAILogic, + bool bUseContinuousGoalTracking, + EAIOptionFlag::Type ProjectGoalOnNavigation) +{ + return AIMoveToCore(Controller, Target, AcceptanceRadius, StopOnOverlap, + AcceptPartialPath, bUsePathfinding, bLockAILogic, + bUseContinuousGoalTracking, ProjectGoalOnNavigation); +} + +FMoveToAwaiter AI::AIMoveTo(AAIController* Controller, AActor* Target, + float AcceptanceRadius, + EAIOptionFlag::Type StopOnOverlap, + EAIOptionFlag::Type AcceptPartialPath, + bool bUsePathfinding, bool bLockAILogic, + bool bUseContinuousGoalTracking, + EAIOptionFlag::Type ProjectGoalOnNavigation) +{ + return AIMoveToCore(Controller, Target, AcceptanceRadius, StopOnOverlap, + AcceptPartialPath, bUsePathfinding, bLockAILogic, + bUseContinuousGoalTracking, ProjectGoalOnNavigation); +} + +FSimpleMoveToAwaiter AI::SimpleMoveTo(AController* Controller, FVector Target) +{ + return SimpleMoveToCore(Controller, Target); +} + +FSimpleMoveToAwaiter AI::SimpleMoveTo(AController* Controller, AActor* Target) +{ + return SimpleMoveToCore(Controller, Target); +} diff --git a/Plugins/UE5CoroAI/Source/UE5CoroAI/Private/UE5CoroAI.cpp b/Plugins/UE5CoroAI/Source/UE5CoroAI/Private/UE5CoroAI.cpp new file mode 100644 index 00000000..ed659a45 --- /dev/null +++ b/Plugins/UE5CoroAI/Source/UE5CoroAI/Private/UE5CoroAI.cpp @@ -0,0 +1,39 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5CoroAI.h" +#include "Modules/ModuleManager.h" + +class FUE5CoroAIModule : public IModuleInterface +{ +}; + +IMPLEMENT_MODULE(FUE5CoroAIModule, UE5CoroAI); diff --git a/Plugins/UE5CoroAI/Source/UE5CoroAI/Private/UE5CoroAICallbackTarget.cpp b/Plugins/UE5CoroAI/Source/UE5CoroAI/Private/UE5CoroAICallbackTarget.cpp new file mode 100644 index 00000000..6428b3f3 --- /dev/null +++ b/Plugins/UE5CoroAI/Source/UE5CoroAI/Private/UE5CoroAICallbackTarget.cpp @@ -0,0 +1,75 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5CoroAICallbackTarget.h" +#include "Tasks/AITask_MoveTo.h" + +auto UUE5CoroAICallbackTarget::SetTask(UAITask_MoveTo* InTask) -> ThisClass* +{ + checkf(InTask, TEXT("Internal error: Setting up null task")); + checkf(Task.IsExplicitlyNull(), + TEXT("Internal error: reused AI callback target")); + Task = InTask; + + struct UTaskBinder final : UAITask_MoveTo + { + void Bind(UUE5CoroAICallbackTarget* Target) + { + OnMoveFinished.AddDynamic(Target, &UUE5CoroAICallbackTarget::Core); + OnRequestFailed.AddDynamic(Target, &UUE5CoroAICallbackTarget::Error); + } + }; + auto* UnlockedTask = std::launder(static_cast(InTask)); + UnlockedTask->Bind(this); + UnlockedTask->ReadyForActivation(); + return this; +} + +auto UUE5CoroAICallbackTarget::GetResult() const + -> std::optional +{ + checkf(IsInGameThread(), + TEXT("Internal error: polling AI callback off the game thread")); + if (Task.IsStale() && !Result.has_value()) + return EPathFollowingResult::Aborted; // Invent a result if the task died + return Result; +} + +void UUE5CoroAICallbackTarget::Core( + TEnumAsByte InResult, AAIController*) +{ + Result = InResult; +} + +void UUE5CoroAICallbackTarget::Error() +{ + Core(EPathFollowingResult::Aborted, nullptr); +} diff --git a/Plugins/UE5CoroAI/Source/UE5CoroAI/Private/UE5CoroAICallbackTarget.h b/Plugins/UE5CoroAI/Source/UE5CoroAI/Private/UE5CoroAICallbackTarget.h new file mode 100644 index 00000000..90842fc9 --- /dev/null +++ b/Plugins/UE5CoroAI/Source/UE5CoroAI/Private/UE5CoroAICallbackTarget.h @@ -0,0 +1,59 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#include +#include "Navigation/PathFollowingComponent.h" +#include "UE5CoroAICallbackTarget.generated.h" + +UCLASS() +class UUE5CoroAICallbackTarget : public UObject +{ + GENERATED_BODY() + + TWeakObjectPtr Task = nullptr; + std::optional Result; + +public: + ThisClass* SetTask(UAITask_MoveTo*); + std::optional GetResult() const; + +private: + UFUNCTION() + void Core(TEnumAsByte Result, + AAIController* AIController); + + UFUNCTION() + void Error(); +}; diff --git a/Plugins/UE5CoroAI/Source/UE5CoroAI/Public/UE5CoroAI.h b/Plugins/UE5CoroAI/Source/UE5CoroAI/Public/UE5CoroAI.h new file mode 100644 index 00000000..656f9f82 --- /dev/null +++ b/Plugins/UE5CoroAI/Source/UE5CoroAI/Public/UE5CoroAI.h @@ -0,0 +1,35 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "UE5Coro/Definitions.h" +#include "UE5CoroAI/AIAwaiters.h" diff --git a/Plugins/UE5CoroAI/Source/UE5CoroAI/Public/UE5CoroAI/AIAwaiters.h b/Plugins/UE5CoroAI/Source/UE5CoroAI/Public/UE5CoroAI/AIAwaiters.h new file mode 100644 index 00000000..ae0f8592 --- /dev/null +++ b/Plugins/UE5CoroAI/Source/UE5CoroAI/Public/UE5CoroAI/AIAwaiters.h @@ -0,0 +1,135 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#if !UE5CORO_CPP20 +#error UE5CoroAI does not support C++17. +#endif +#include +#include "AITypes.h" +#include "Tasks/AITask_MoveTo.h" +#include "UE5Coro/LatentAwaiters.h" + +namespace UE5Coro::Private +{ +class FPathFindingAwaiter; +class FMoveToAwaiter; +class FSimpleMoveToAwaiter; +} + +namespace UE5Coro::AI +{ +/** Starts an async pathfinding operation, resumes the awaiting coroutine once + * it finishes.
+ * The result of the co_await expression is + * TTuple. */ +UE5COROAI_API Private::FPathFindingAwaiter FindPath( + UObject* WorldContextObject, const FPathFindingQuery& Query, + EPathFindingMode::Type Mode = EPathFindingMode::Regular); + +/** Issues a "move to" command to the specified controller, resumes the awaiting + * coroutine once it finishes.
+ * The result of the co_await expression is EPathFollowingResult. */ +UE5COROAI_API Private::FMoveToAwaiter AIMoveTo( + AAIController* Controller, FVector Target, float AcceptanceRadius = -1, + EAIOptionFlag::Type StopOnOverlap = EAIOptionFlag::Default, + EAIOptionFlag::Type AcceptPartialPath = EAIOptionFlag::Default, + bool bUsePathfinding = true, bool bLockAILogic = true, + bool bUseContinuousGoalTracking = false, + EAIOptionFlag::Type ProjectGoalOnNavigation = EAIOptionFlag::Default); + +/** Issues a "move to" command to the specified controller, resumes the awaiting + * coroutine once it finishes.
+ * The result of the co_await expression is EPathFollowingResult. */ +UE5COROAI_API Private::FMoveToAwaiter AIMoveTo( + AAIController* Controller, AActor* Target, float AcceptanceRadius = -1, + EAIOptionFlag::Type StopOnOverlap = EAIOptionFlag::Default, + EAIOptionFlag::Type AcceptPartialPath = EAIOptionFlag::Default, + bool bUsePathfinding = true, bool bLockAILogic = true, + bool bUseContinuousGoalTracking = false, + EAIOptionFlag::Type ProjectGoalOnNavigation = EAIOptionFlag::Default); + +/** Performs similar behavior to UAIBlueprintHelperLibrary's SimpleMoveTo, + * such as injecting components into the controller, issues a "move to" + * command, and resumes the awaiting coroutine once it finishes.
+ * The result of the co_await expression is FPathFollowingResult. */ +UE5COROAI_API auto SimpleMoveTo(AController* Controller, FVector Target) + -> Private::FSimpleMoveToAwaiter; + +/** Performs similar behavior to UAIBlueprintHelperLibrary's SimpleMoveTo, + * such as injecting components into the controller, issues a "move to" + * command, and resumes the awaiting coroutine once it finishes.
+ * The result of the co_await expression is FPathFollowingResult. */ +UE5COROAI_API auto SimpleMoveTo(AController* Controller, AActor* Target) + -> Private::FSimpleMoveToAwaiter; +} + +namespace UE5Coro::Private +{ +class [[nodiscard]] UE5COROAI_API FPathFindingAwaiter : public FLatentAwaiter +{ +public: + explicit FPathFindingAwaiter(void*); + TTuple await_resume(); +}; + +class [[nodiscard]] UE5COROAI_API FMoveToAwaiter : public FLatentAwaiter +{ +public: + explicit FMoveToAwaiter(UAITask_MoveTo*); + EPathFollowingResult::Type await_resume() noexcept; +}; + +class [[nodiscard]] UE5COROAI_API FSimpleMoveToAwaiter : public FLatentAwaiter +{ + struct FComplexData + { + FAIRequestID RequestID; + TWeakObjectPtr PathFollow; + FDelegateHandle Handle; + std::optional Result; + void RequestFinished(FAIRequestID, const FPathFollowingResult&); + }; + static bool ShouldResume(void* State, bool bCleanup); + +public: + explicit FSimpleMoveToAwaiter(EPathFollowingResult::Type); + explicit FSimpleMoveToAwaiter(UPathFollowingComponent*, FAIRequestID); + FPathFollowingResult await_resume() noexcept; +}; + +static_assert(sizeof(FPathFindingAwaiter) == sizeof(FLatentAwaiter)); +static_assert(sizeof(FMoveToAwaiter) == sizeof(FLatentAwaiter)); +static_assert(sizeof(FSimpleMoveToAwaiter) == sizeof(FLatentAwaiter)); +} diff --git a/Plugins/UE5CoroAI/Source/UE5CoroAI/UE5CoroAI.Build.cs b/Plugins/UE5CoroAI/Source/UE5CoroAI/UE5CoroAI.Build.cs new file mode 100644 index 00000000..7db05f8c --- /dev/null +++ b/Plugins/UE5CoroAI/Source/UE5CoroAI/UE5CoroAI.Build.cs @@ -0,0 +1,47 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using UnrealBuildTool; + +public class UE5CoroAI : UE5CoroModuleRules +{ + public UE5CoroAI(ReadOnlyTargetRules Target) + : base(Target) + { + PublicDependencyModuleNames.AddRange(new[] + { + "AIModule", + "GameplayTasks", + "NavigationSystem", + "UE5Coro", + }); + } +} diff --git a/Plugins/UE5CoroAI/Source/UE5CoroAITests/Private/AITest.cpp b/Plugins/UE5CoroAI/Source/UE5CoroAITests/Private/AITest.cpp new file mode 100644 index 00000000..0652a524 --- /dev/null +++ b/Plugins/UE5CoroAI/Source/UE5CoroAITests/Private/AITest.cpp @@ -0,0 +1,46 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5Coro.h" +#include "UE5CoroAI/AIAwaiters.h" + +using namespace UE5Coro; +using namespace UE5Coro::AI; + +TCoroutine<> UE5CoroAICompileTest() +{ + FPathFindingQuery Query; + co_await FindPath(nullptr, Query); + co_await AIMoveTo(nullptr, FVector()); + co_await AIMoveTo(nullptr, static_cast(nullptr)); + co_await SimpleMoveTo(nullptr, FVector()); + co_await SimpleMoveTo(nullptr, static_cast(nullptr)); +} diff --git a/Plugins/UE5CoroAI/Source/UE5CoroAITests/Private/UE5CoroAITests.cpp b/Plugins/UE5CoroAI/Source/UE5CoroAITests/Private/UE5CoroAITests.cpp new file mode 100644 index 00000000..a44c3d43 --- /dev/null +++ b/Plugins/UE5CoroAI/Source/UE5CoroAITests/Private/UE5CoroAITests.cpp @@ -0,0 +1,38 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "Modules/ModuleManager.h" + +class FUE5CoroAITestsModule : public IModuleInterface +{ +}; + +IMPLEMENT_MODULE(FUE5CoroAITestsModule, UE5CoroAITests); diff --git a/Plugins/UE5CoroAI/Source/UE5CoroAITests/UE5CoroAITests.Build.cs b/Plugins/UE5CoroAI/Source/UE5CoroAITests/UE5CoroAITests.Build.cs new file mode 100644 index 00000000..6906718d --- /dev/null +++ b/Plugins/UE5CoroAI/Source/UE5CoroAITests/UE5CoroAITests.Build.cs @@ -0,0 +1,47 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using UnrealBuildTool; + +public class UE5CoroAITests : UE5CoroModuleRules +{ + public UE5CoroAITests(ReadOnlyTargetRules Target) + : base(Target) + { + PublicDependencyModuleNames.AddRange(new[] + { + "NavigationSystem", + "UE5Coro", + "UE5CoroAI", + "UE5CoroTests", + }); + } +} diff --git a/Plugins/UE5CoroAI/UE5CoroAI.uplugin b/Plugins/UE5CoroAI/UE5CoroAI.uplugin new file mode 100644 index 00000000..88e48b85 --- /dev/null +++ b/Plugins/UE5CoroAI/UE5CoroAI.uplugin @@ -0,0 +1,36 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "0.10.2-ue4", + "FriendlyName": "UE5Coro – AI (UE4 edition)", + "Description": "C++20 coroutines for AI", + "Category": "Programming", + "CreatedBy": "Laura Andelare", + "CreatedByURL": "https://github.com/landelare/ue5coro", + "DocsURL": "", + "MarketplaceURL": "", + "SupportURL": "", + "EnabledByDefault": false, + "CanContainContent": false, + "IsBetaVersion": false, + "IsExperimentalVersion": true, + "Installed": false, + "Modules": [ + { + "Name": "UE5CoroAI", + "Type": "Runtime", + "LoadingPhase": "PreDefault" + }, + { + "Name": "UE5CoroAITests", + "Type": "UncookedOnly", + "LoadingPhase": "PreDefault" + } + ], + "Plugins": [ + { + "Name": "UE5Coro", + "Enabled": true + } + ] +} diff --git a/Plugins/UE5CoroGAS/Resources/Icon128.png b/Plugins/UE5CoroGAS/Resources/Icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..b5ebf754b9e488f2964b101ed98ddc235ef6b055 GIT binary patch literal 15687 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 literal 0 HcmV?d00001 diff --git a/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Private/AbilityPromises.cpp b/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Private/AbilityPromises.cpp new file mode 100644 index 00000000..f9e7014d --- /dev/null +++ b/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Private/AbilityPromises.cpp @@ -0,0 +1,103 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5CoroGAS/AbilityPromises.h" +#include "UE5CoroGAS/UE5CoroAbilityTask.h" +#include "UE5CoroGAS/UE5CoroGameplayAbility.h" + +using namespace UE5Coro; +using namespace UE5Coro::GAS; +using namespace UE5Coro::Private; + +FAbilityCoroutine::FAbilityCoroutine(std::shared_ptr Extras) + : TCoroutine(std::move(Extras)) +{ +} + +FLatentActionInfo FAbilityPromise::MakeLatentInfo(UObject& Task) +{ + static int DummyId = 0; + return {0, DummyId++, TEXT("None"), &Task}; +} + +FAbilityPromise::FAbilityPromise(UObject& Target, UWorld* MaybeWorld) + : Super(Target, MaybeWorld, MakeLatentInfo(Target)) +{ + checkf(IsInGameThread(), + TEXT("Internal error: Expected to start on the game thread")); +} + +FAbilityCoroutine FAbilityPromise::get_return_object() noexcept +{ + return FAbilityCoroutine(Extras); +} + +FFinalSuspend FAbilityPromise::final_suspend() noexcept +{ + // Skip triggering a BP link because there isn't one + return Super::final_suspend(); +} + +template +void TAbilityPromise::Init(T& Target) +{ + checkf(bCalledFromActivate, TEXT("Do not call Execute coroutines directly!")); + bCalledFromActivate = false; + Target.CoroutineStarting(this); +} + +UWorld* FAbilityPromise::TryGetWorld(FGameplayAbilitySpecHandle Handle, + const FGameplayAbilityActorInfo* ActorInfo, + FGameplayAbilityActivationInfo ActivationInfo, + const FGameplayEventData* TriggerEventData) +{ + // UAbilitySystemComponent::InternalTryActivateAbility should prevent these + checkf(ActorInfo, TEXT("Expected ability activation with valid actor info")); + checkf(IsValid(ActorInfo->OwnerActor.Get()), + TEXT("Expected ability activation with valid owner")); + checkf(IsValid(ActorInfo->AvatarActor.Get()), + TEXT("Expected ability activation with valid avatar")); + + if (auto* World = ActorInfo->OwnerActor.Get()->GetWorld(); + ensureMsgf(IsValid(World), + TEXT("Expected ability activation in valid world"))) + return World; + else + return nullptr; +} + +namespace UE5Coro::Private +{ +template +bool TAbilityPromise::bCalledFromActivate = false; +template class UE5COROGAS_API TAbilityPromise; +template class UE5COROGAS_API TAbilityPromise; +} diff --git a/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Private/UE5CoroAbilityTask.cpp b/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Private/UE5CoroAbilityTask.cpp new file mode 100644 index 00000000..4f3664ed --- /dev/null +++ b/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Private/UE5CoroAbilityTask.cpp @@ -0,0 +1,104 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5CoroGAS/UE5CoroAbilityTask.h" + +using namespace UE5Coro::Private; + +void UUE5CoroAbilityTask::Activate() +{ + Super::Activate(); + + // Is there a use for this? PerformActivation seems to guard against it. + ensureMsgf(!Promise, TEXT("Multiple overlapping activations")); + + checkf(!TAbilityPromise::bCalledFromActivate, + TEXT("Internal error: Activate() recursion")); + TAbilityPromise::bCalledFromActivate = true; + auto Coroutine = Execute(); + checkf(!TAbilityPromise::bCalledFromActivate, + TEXT("Did you implement Execute() with a coroutine?")); +#if UE5CORO_CPP20 + Coroutine.ContinueWithWeak(this, [=, this] +#else + Coroutine.ContinueWithWeak(this, [=] +#endif + { + checkf(IsInGameThread(), + TEXT("Internal error: Expected to continue on the game thread")); + checkf(Promise, + TEXT("Internal error: Expected to be the active coroutine")); + Promise = nullptr; + Super::EndTask(); + if (Coroutine.WasSuccessful()) + Succeeded(); + else + Failed(); + }); +} + +void UUE5CoroAbilityTask::EndTask() +{ + check(!"Do not call EndTask() manually"); +} + +void UUE5CoroAbilityTask::OnDestroy(bool bInOwnerFinished) +{ + checkf(IsInGameThread(), + TEXT("Internal error: Expected to be destroyed on the game thread")); + + // GAS itself relies on this hack in TaskOwnerEnded... :( + if (!IsValid(this)) + return; + + // A forced cancellation would be more appropriate because this is a + // destruction, but the coroutine might be running (and NOT suspended) + if (Promise) + Promise->Cancel(); + + Super::OnDestroy(bInOwnerFinished); + checkf(!IsValid(this), TEXT("Internal error: expected MarkAsGarbage()")); +} + +void UUE5CoroAbilityTask::CoroutineStarting(TAbilityPromise* InPromise) +{ + Promise = InPromise; +} + +void UUE5CoroSimpleAbilityTask::Succeeded() +{ + OnSucceeded.Broadcast(); +} + +void UUE5CoroSimpleAbilityTask::Failed() +{ + OnFailed.Broadcast(); +} diff --git a/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Private/UE5CoroGAS.cpp b/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Private/UE5CoroGAS.cpp new file mode 100644 index 00000000..6d8e29b7 --- /dev/null +++ b/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Private/UE5CoroGAS.cpp @@ -0,0 +1,39 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5CoroGAS.h" +#include "Modules/ModuleManager.h" + +class FUE5CoroGASModule : public IModuleInterface +{ +}; + +IMPLEMENT_MODULE(FUE5CoroGASModule, UE5CoroGAS); diff --git a/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Private/UE5CoroGameplayAbility.cpp b/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Private/UE5CoroGameplayAbility.cpp new file mode 100644 index 00000000..9b0fb517 --- /dev/null +++ b/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Private/UE5CoroGameplayAbility.cpp @@ -0,0 +1,208 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5CoroGAS/UE5CoroGameplayAbility.h" +#include "GameplayTask.h" +#include "Kismet/BlueprintAsyncActionBase.h" +#include "UE5CoroTaskCallbackTarget.h" +#include "UE5Coro/LatentAwaiters.h" + +using namespace UE5Coro; +using namespace UE5Coro::Private; + +namespace +{ +bool GCoroutineEnded = false; +FPredictionKey GCurrentPredictionKey; + +// Workaround for member IsTemplate being unreliable in destructors +bool IsTemplate(UObject* Object) +{ + // Deliberately not using IsValid here + checkf(Object, TEXT("Internal error: corrupted object")); + // Outer is explicitly not checked, it might be destroyed already + return Object->HasAnyFlags(RF_ArchetypeObject | RF_ClassDefaultObject); +} +} + +UUE5CoroGameplayAbility::UUE5CoroGameplayAbility() +{ + if (::IsTemplate(this)) + Activations = new TMap*>; + else + Activations = GetDefault(GetClass())->Activations; + checkf(Activations, TEXT("Internal error: non-template object before CDO")); + + // For consistency. + // Super::ActivateAbility is not called, so these aren't really used. + bHasBlueprintActivate = false; + bHasBlueprintActivateFromEvent = false; +} + +UUE5CoroGameplayAbility::~UUE5CoroGameplayAbility() +{ + if (::IsTemplate(this)) + delete Activations; +#if UE5CORO_DEBUG + Activations = nullptr; +#endif +} + +FLatentAwaiter UUE5CoroGameplayAbility::Task(UObject* Object) +{ + checkf(IsInGameThread(), + TEXT("This method is only available on the game thread")); + checkf(IsValid(Object), TEXT("Attempting to await invalid object")); + // Find BlueprintAssignable properties + auto* Class = Object->GetClass(); + FProperty* Property = nullptr; + for (auto* i = Class->PropertyLink; i; i = i->NextRef) + { + if (!i->HasAnyPropertyFlags(CPF_BlueprintAssignable)) + continue; + checkf(!Property, + TEXT("Only one BlueprintAssignable UPROPERTY is supported.")); + Property = i; + if constexpr (!UE5CORO_DEBUG) // Keep looking for others in debug + break; + } + checkf(Property, TEXT("A BlueprintAssignable UPROPERTY is required.")); + auto* DelegateProp = CastFieldChecked(Property); + + auto* Target = NewObject(this); + FScriptDelegate Delegate; + Delegate.BindUFunction(Target, NAME_Core); + DelegateProp->AddDelegate(std::move(Delegate), Object); + + // Activate some well-known base classes (IsValid was checked above) + if (auto* Task = Cast(Object)) + Task->ReadyForActivation(); + else if (auto* Action = Cast(Object)) + Action->Activate(); + + return FLatentAwaiter(new TStrongObjectPtr(Target), &ShouldResumeTask); +} + +void UUE5CoroGameplayAbility::ActivateAbility( + FGameplayAbilitySpecHandle Handle, + const FGameplayAbilityActorInfo* ActorInfo, + FGameplayAbilityActivationInfo ActivationInfo, + const FGameplayEventData* TriggerEventData) +{ + // Super::ActivateAbility has a weird piece of example code in it that + // forcibly commits the ability - not calling it + + checkf(IsInGameThread(), + TEXT("Internal error: Expected GA activation on the game thread")); + GCurrentPredictionKey = ActivationInfo.GetActivationPredictionKey(); + checkf(!TAbilityPromise::bCalledFromActivate, + TEXT("Internal error: ActivateAbility recursion")); + TAbilityPromise::bCalledFromActivate = true; + auto Coroutine = ExecuteAbility(Handle, ActorInfo, ActivationInfo, + TriggerEventData); + checkf(!TAbilityPromise::bCalledFromActivate, + TEXT("Did you implement ExecuteAbility with a coroutine?")); + +#if UE5CORO_CPP20 + Coroutine.ContinueWithWeak(this, [=, ActorInfoCopy = *ActorInfo, this] +#else + Coroutine.ContinueWithWeak(this, [=, ActorInfoCopy = *ActorInfo] +#endif + { + checkf(IsInGameThread(), + TEXT("Internal error: Expected to continue on the game thread")); + GCoroutineEnded = true; + EndAbility(Handle, &ActorInfoCopy, ActivationInfo, bReplicateAbilityEnd, + !Coroutine.WasSuccessful()); + checkf(!GCoroutineEnded, TEXT("Internal error: unexpected state")); + }); +} + +void UUE5CoroGameplayAbility::EndAbility( + FGameplayAbilitySpecHandle Handle, + const FGameplayAbilityActorInfo* ActorInfo, + FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, + bool bWasCanceled) +{ + checkf(IsInGameThread(), TEXT("Abilities may only end on the game thread")); + + // CancelAbility might also call this, but externally initiated + bool bCoroutineEnded = GCoroutineEnded; + // Prepare for the Super call possibly ending something else recursively + GCoroutineEnded = false; + + Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, + bWasCanceled); + + auto PredictionKey = ActivationInfo.GetActivationPredictionKey(); + if (!PredictionKey.IsValidKey()) + return; + + TAbilityPromise* Promise; + bool bFound = Activations->RemoveAndCopyValue(PredictionKey, Promise); + + // Nothing to do if the coroutine has ended already + if (bCoroutineEnded) + return; + + // If the coroutine hasn't ended, why was it not in the map? + checkf(bFound, TEXT("Internal error: Unexpected EndAbility call")); + + // Cancel the coroutine. Depending on instancing policy, there will be a + // second, forced cancellation coming when the latent action manager + // processes the action's removal. + checkf(!Promise->get_return_object().IsDone(), + TEXT("Internal error: unexpected coroutine state")); + Promise->Cancel(); +} + +void UUE5CoroGameplayAbility::CoroutineStarting(TAbilityPromise* Promise) +{ + checkf(IsInGameThread(), + TEXT("Internal error: expected coroutine on the game thread")); + checkf(GCurrentPredictionKey.IsValidKey(), + TEXT("Attempting to start ability with invalid prediction key")); + checkf(!Activations->Contains(GCurrentPredictionKey), + TEXT("Overlapping ability activations with the same prediction key")); + // Promise is not fully-constructed yet, but its address is known + Activations->Add(GCurrentPredictionKey, Promise); +} + +bool UUE5CoroGameplayAbility::ShouldResumeTask(void* State, bool bCleanup) +{ + auto* Ptr = static_cast*>(State); + if (UNLIKELY(bCleanup)) + { + delete Ptr; + return false; + } + return (*Ptr)->bExecuted; +} diff --git a/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Private/UE5CoroTaskCallbackTarget.cpp b/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Private/UE5CoroTaskCallbackTarget.cpp new file mode 100644 index 00000000..1ca73a19 --- /dev/null +++ b/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Private/UE5CoroTaskCallbackTarget.cpp @@ -0,0 +1,37 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5CoroTaskCallbackTarget.h" + +void UUE5CoroTaskCallbackTarget::Core() +{ + bExecuted = true; +} diff --git a/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Private/UE5CoroTaskCallbackTarget.h b/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Private/UE5CoroTaskCallbackTarget.h new file mode 100644 index 00000000..addf4d83 --- /dev/null +++ b/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Private/UE5CoroTaskCallbackTarget.h @@ -0,0 +1,48 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#include "UE5CoroTaskCallbackTarget.generated.h" + +UCLASS(Within = UE5CoroGameplayAbility) +class UUE5CoroTaskCallbackTarget : public UObject +{ + GENERATED_BODY() + +public: + bool bExecuted = false; + + UFUNCTION() + void Core(); +}; diff --git a/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Public/UE5CoroGAS.h b/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Public/UE5CoroGAS.h new file mode 100644 index 00000000..e39e9341 --- /dev/null +++ b/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Public/UE5CoroGAS.h @@ -0,0 +1,36 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "UE5Coro/Definitions.h" +#include "UE5CoroGAS/UE5CoroAbilityTask.h" +#include "UE5CoroGAS/UE5CoroGameplayAbility.h" diff --git a/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Public/UE5CoroGAS/AbilityPromises.h b/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Public/UE5CoroGAS/AbilityPromises.h new file mode 100644 index 00000000..30ddc243 --- /dev/null +++ b/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Public/UE5CoroGAS/AbilityPromises.h @@ -0,0 +1,125 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +/****************************************************************************** + * This file only contains private implementation details. * + ******************************************************************************/ + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#include "Abilities/GameplayAbilityTypes.h" +#include "UE5Coro/AsyncCoroutine.h" + +class UUE5CoroAbilityTask; +class UUE5CoroGameplayAbility; + +namespace UE5Coro +{ +namespace Private +{ +class FAbilityPromise; +} + +namespace GAS +{ +/** A special marker type for ability coroutines, needed due to C++ limitations. + * Only use it when overriding methods that already have this return type. */ +class UE5COROGAS_API FAbilityCoroutine : public TCoroutine<> +{ + friend Private::FAbilityPromise; + explicit FAbilityCoroutine(std::shared_ptr); +}; +} + +namespace Private +{ +class [[nodiscard]] UE5COROGAS_API FAbilityPromise + : public TCoroutinePromise +{ + using Super = TCoroutinePromise; + static FLatentActionInfo MakeLatentInfo(UObject&); + +protected: + UWorld* TryGetWorld() { return nullptr; } + UWorld* TryGetWorld(FGameplayAbilitySpecHandle Handle, + const FGameplayAbilityActorInfo* ActorInfo, + FGameplayAbilityActivationInfo ActivationInfo, + const FGameplayEventData* TriggerEventData); + explicit FAbilityPromise(UObject&, UWorld* = nullptr); + +public: + GAS::FAbilityCoroutine get_return_object() noexcept; + FFinalSuspend final_suspend() noexcept; +}; + +template +class [[nodiscard]] UE5COROGAS_API TAbilityPromise final : public FAbilityPromise +{ + void Init(T&); + +public: + static bool bCalledFromActivate; + template + explicit TAbilityPromise(U& Target, A&... Args) + : FAbilityPromise(static_cast(Target), TryGetWorld(Args...)) + { + Init(Target); + } +}; + +// C++17 SFINAE helpers +template +using TAbilityCoroutine = GAS::FAbilityCoroutine; + +template +using TAbilityCoroutineIfBaseOf = TAbilityCoroutine< + decltype(sizeof(Derived)), // Filter incomplete types before enable_if_t + std::enable_if_t>>; +} +} + +template +struct UE5Coro::Private::stdcoro::coroutine_traits< + UE5Coro::Private::TAbilityCoroutineIfBaseOf, T&> +{ + using promise_type = UE5Coro::Private::TAbilityPromise; +}; + +template +struct UE5Coro::Private::stdcoro::coroutine_traits< + UE5Coro::Private::TAbilityCoroutineIfBaseOf, T&, + FGameplayAbilitySpecHandle, const FGameplayAbilityActorInfo*, + FGameplayAbilityActivationInfo, const FGameplayEventData*> +{ + using promise_type = UE5Coro::Private::TAbilityPromise; +}; diff --git a/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Public/UE5CoroGAS/UE5CoroAbilityTask.h b/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Public/UE5CoroGAS/UE5CoroAbilityTask.h new file mode 100644 index 00000000..b345de8d --- /dev/null +++ b/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Public/UE5CoroGAS/UE5CoroAbilityTask.h @@ -0,0 +1,94 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#include "Abilities/Tasks/AbilityTask.h" +#include "UE5CoroGAS/AbilityPromises.h" +#include "UE5CoroAbilityTask.generated.h" + +/** + * Usage summary: + * - Add the necessary static UFUNCTION in your subclass + * - Add your own delegates or use UUE5CoroSimpleAbilityTask + * - Override Execute instead of Activate + * - Run to completion to succeed, cancel to fail + * - Invoke your delegates from Succeeded or Failed, not from Execute + */ +UCLASS(Abstract, NotBlueprintable) +class UE5COROGAS_API UUE5CoroAbilityTask : public UAbilityTask +{ + GENERATED_BODY() + friend UE5Coro::Private::TAbilityPromise; + + UE5Coro::Private::TAbilityPromise* Promise = nullptr; + +protected: + /** Override this with a coroutine instead of Activate. Do not call directly. + * The returned coroutine's completion will call Succeeded or Failed. + * The coroutine will run in latent mode and can self-cancel to indicate + * failure. */ + virtual UE5Coro::GAS::FAbilityCoroutine Execute() + PURE_VIRTUAL(UUE5CoroGameplayTask::Execute, co_return;); + + /** Called if Execute successfully runs to completion. */ + virtual void Succeeded() { } + + /** Called if Execute completes unsuccessfully, e.g., due to cancellation. */ + virtual void Failed() { } + +private: + /** Do not use. */ + virtual void Activate() final override; + /** Do not call. Let the coroutine complete to end the task. */ + void EndTask(); + virtual void OnDestroy(bool bInOwnerFinished) final override; + void CoroutineStarting(UE5Coro::Private::TAbilityPromise*); +}; + +UCLASS(Abstract) +class UE5COROGAS_API UUE5CoroSimpleAbilityTask : public UUE5CoroAbilityTask +{ + GENERATED_BODY() + +public: + UPROPERTY(BlueprintAssignable) + FGenericGameplayTaskDelegate OnSucceeded; + + UPROPERTY(BlueprintAssignable) + FGenericGameplayTaskDelegate OnFailed; + +protected: + virtual void Succeeded() override; + virtual void Failed() override; +}; diff --git a/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Public/UE5CoroGAS/UE5CoroGameplayAbility.h b/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Public/UE5CoroGAS/UE5CoroGameplayAbility.h new file mode 100644 index 00000000..a1aaf461 --- /dev/null +++ b/Plugins/UE5CoroGAS/Source/UE5CoroGAS/Public/UE5CoroGAS/UE5CoroGameplayAbility.h @@ -0,0 +1,102 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "CoreMinimal.h" +#include "UE5Coro/Definitions.h" +#include "Abilities/GameplayAbility.h" +#include "UE5CoroGAS/AbilityPromises.h" +#include "UE5CoroGameplayAbility.generated.h" + +/** + * Usage summary: + * - Override ExecuteAbility instead of ActivateAbility + * - Call CommitAbility like usual but do *not* call EndAbility + * - Any other method not mentioned above can be overridden and used normally + * - Every instancing policy is supported + */ +UCLASS(Abstract, NotBlueprintable) +class UE5COROGAS_API UUE5CoroGameplayAbility : public UGameplayAbility +{ + GENERATED_BODY() + friend UE5Coro::Private::TAbilityPromise; + + // One shared per class to support every instancing policy including derived + // classes changing their minds at runtime. The real one is on the CDO. + TMap*>* Activations; + +public: + UUE5CoroGameplayAbility(); + virtual ~UUE5CoroGameplayAbility() override; + +protected: + /** If true when ExecuteAbility co_returns, the ability's end will be + * replicated. This value may be freely changed at any time. */ + UPROPERTY(BlueprintReadWrite) + bool bReplicateAbilityEnd = true; + + /** Override this with a coroutine instead of ActivateAbility. + * The returned coroutine's completion will call EndAbility, and will be + * canceled if the ability's execution is canceled. + * The coroutine will run in latent mode and can self-cancel. + * Do not call directly. */ + virtual UE5Coro::GAS::FAbilityCoroutine + ExecuteAbility(FGameplayAbilitySpecHandle Handle, + const FGameplayAbilityActorInfo* ActorInfo, + FGameplayAbilityActivationInfo ActivationInfo, + const FGameplayEventData* TriggerEventData) + PURE_VIRTUAL(UUE5CoroGameplayAbility::ExecuteAbility, co_return;); + + /** Given a UObject* with a single BlueprintAssignable UPROPERTY, + * the return value of this function lets you co_await that delegate safely, + * handling cancellations even if the delegate never Broadcasts.
+ * If the parameter is a UGameplayTask or UBlueprintAsyncActionBase, it + * will be activated before returning. */ + UE5Coro::Private::FLatentAwaiter Task(UObject*); + +private: + /** Override ExecuteAbility instead. */ + virtual void ActivateAbility(FGameplayAbilitySpecHandle, + const FGameplayAbilityActorInfo*, + FGameplayAbilityActivationInfo, + const FGameplayEventData*) final override; + + /** Do not use. */ + virtual void EndAbility(FGameplayAbilitySpecHandle, + const FGameplayAbilityActorInfo*, + FGameplayAbilityActivationInfo, bool, + bool) final override; + + void CoroutineStarting(UE5Coro::Private::TAbilityPromise*); + + static bool ShouldResumeTask(void*, bool); +}; diff --git a/Plugins/UE5CoroGAS/Source/UE5CoroGAS/UE5CoroGAS.Build.cs b/Plugins/UE5CoroGAS/Source/UE5CoroGAS/UE5CoroGAS.Build.cs new file mode 100644 index 00000000..4eb03a3f --- /dev/null +++ b/Plugins/UE5CoroGAS/Source/UE5CoroGAS/UE5CoroGAS.Build.cs @@ -0,0 +1,46 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using UnrealBuildTool; + +public class UE5CoroGAS : UE5CoroModuleRules +{ + public UE5CoroGAS(ReadOnlyTargetRules Target) + : base(Target) + { + PublicDependencyModuleNames.AddRange(new[] + { + "GameplayAbilities", + "GameplayTasks", + "UE5Coro", + }); + } +} diff --git a/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/AbilityTaskTests.cpp b/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/AbilityTaskTests.cpp new file mode 100644 index 00000000..fb13cbbe --- /dev/null +++ b/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/AbilityTaskTests.cpp @@ -0,0 +1,96 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "GASTestWorld.h" +#include "Misc/AutomationTest.h" +#include "UE5CoroGASTestAbilityTask.h" +#include "UE5CoroGASTestGameplayAbility.h" + +using namespace UE5Coro::Private::Test; + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FAbilityTaskTest, "UE5Coro.GAS.AbilityTask", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +bool FAbilityTaskTest::RunTest(const FString& Parameters) +{ + FGASTestWorld World; + UUE5CoroGASTestGameplayAbility::SetInstancingPolicy( + EGameplayAbilityInstancingPolicy::InstancedPerExecution); + UUE5CoroGASTestGameplayAbility::Reset(); + World.Run(UUE5CoroGASTestGameplayAbility::StaticClass()); + UUE5CoroGASTestGameplayAbility* Ability = nullptr; + for (auto* Obj : TObjectRange()) + Ability = Obj; + TestNotNull(TEXT("Ability found"), Ability); + + { + auto* Task = UUE5CoroGASTestAbilityTask::Run(Ability); + Task->ReadyForActivation(); + World.EndTick(); + TestEqual(TEXT("Started"), Task->State, 1); + World.Tick(); + TestEqual(TEXT("Waited 1"), Task->State, 3); + World.Tick(); + TestEqual(TEXT("Waited 2"), Task->State, 4); + Task->PerformLastStep.Execute(); + TestEqual(TEXT("Finished"), Task->State, 10); + TestFalse(TEXT("Garbage"), IsValid(Task)); + } + + { + auto* Task = UUE5CoroGASTestAbilityTask::Run(Ability); + Task->ReadyForActivation(); + Task->TaskOwnerEnded(); + World.EndTick(); + TestEqual(TEXT("Started"), Task->State, 1); + World.Tick(); + TestEqual(TEXT("Waited"), Task->State, 2); // Forced cancellation + World.Tick(); + World.Tick(); + TestEqual(TEXT("No progress"), Task->State, 2); + TestFalse(TEXT("Garbage"), IsValid(Task)); + } + + { + auto* Task = UUE5CoroGASTestAbilityTask::Run(Ability); + Task->ReadyForActivation(); + Task->bSoftCancel = true; + World.EndTick(); + TestEqual(TEXT("Started"), Task->State, 1); + World.Tick(); + TestEqual(TEXT("Failed"), Task->State, 11); + TestFalse(TEXT("Garbage"), IsValid(Task)); + } + + return true; +} diff --git a/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/GASTestWorld.cpp b/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/GASTestWorld.cpp new file mode 100644 index 00000000..0e9b7a26 --- /dev/null +++ b/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/GASTestWorld.cpp @@ -0,0 +1,64 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "GASTestWorld.h" +#include "AbilitySystemComponent.h" +#include "UE5CoroGASTestAvatar.h" + +using namespace UE5Coro::Private::Test; + +FGASTestWorld::FGASTestWorld() +{ + checkf(!bTestWorldActive, + TEXT("Internal error: This test world type is not reentrant")); + bTestWorldActive = true; + + Avatar = (*this)->SpawnActor(); + Controller = (*this)->SpawnActor(); + Controller->Possess(Avatar); + Tick(); +} + +FGASTestWorld::~FGASTestWorld() +{ + Avatar->Destroy(); + Controller->Destroy(); + bTestWorldActive = false; +} + +void FGASTestWorld::Run(UClass* Class) +{ + auto* ASC = Avatar->GetAbilitySystemComponent(); + FGameplayAbilitySpec Spec(Class); + ASC->GiveAbility(Spec); + ASC->TryActivateAbility(Spec.Handle); + EndTick(); +} diff --git a/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/GASTestWorld.h b/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/GASTestWorld.h new file mode 100644 index 00000000..c66cb1f9 --- /dev/null +++ b/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/GASTestWorld.h @@ -0,0 +1,55 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "TestWorld.h" + +class AUE5CoroGASTestAvatar; +class UUE5CoroGameplayAbility; + +namespace UE5Coro::Private::Test +{ +class FGASTestWorld : public FTestWorld +{ + // CDOs get modified, so make sure multiple tests don't run concurrently + static inline bool bTestWorldActive = false; + +public: + AUE5CoroGASTestAvatar* Avatar; + APlayerController* Controller; + + FGASTestWorld(); + ~FGASTestWorld(); + + void Run(UClass*); +}; +} diff --git a/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/GameplayAbilityTests.cpp b/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/GameplayAbilityTests.cpp new file mode 100644 index 00000000..5939856f --- /dev/null +++ b/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/GameplayAbilityTests.cpp @@ -0,0 +1,137 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "GASTestWorld.h" +#include "Misc/AutomationTest.h" +#include "UE5CoroGASTestGameplayAbility.h" + +using namespace UE5Coro::Private::Test; + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FGameplayAbilityTestNonInstanced, + "UE5Coro.GAS.GameplayAbility.NonInstanced", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FGameplayAbilityTestPerActor, + "UE5Coro.GAS.GameplayAbility.PerActor", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FGameplayAbilityTestPerExecution, + "UE5Coro.GAS.GameplayAbility.PerExecution", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::HighPriority | + EAutomationTestFlags::ProductFilter) + +namespace +{ +void DoTest(FAutomationTestBase& Test, + EGameplayAbilityInstancingPolicy::Type Policy) +{ + bool bInstanced = Policy != EGameplayAbilityInstancingPolicy::NonInstanced; + auto* CDO = GetMutableDefault(); + UUE5CoroGASTestGameplayAbility::SetInstancingPolicy(Policy); + int& State = UUE5CoroGASTestGameplayAbility::State; + + { + FGASTestWorld World; + UUE5CoroGASTestGameplayAbility::Reset(); + Test.TestEqual(TEXT("Clean"), State, 0); + World.Run(UUE5CoroGASTestGameplayAbility::StaticClass()); + Test.TestEqual(TEXT("Started"), State, 1); + World.Tick(); + Test.TestEqual(TEXT("Waited 1"), State, 3); + World.Tick(); + Test.TestEqual(TEXT("Waited 2"), State, 4); + if (bInstanced) + { + World.Tick(2); + // Give the latent action an opportunity to poll + if (State != 5) + World.Tick(0); + Test.TestEqual(TEXT("Task ran"), State, 5); + } + UUE5CoroGASTestGameplayAbility::PerformLastStep.Execute(); + Test.TestEqual(TEXT("Task completed"), State, 6); + } + + { + FGASTestWorld World; + UUE5CoroGASTestGameplayAbility::Reset(); + Test.TestEqual(TEXT("Clean"), State, 0); + World.Run(UUE5CoroGASTestGameplayAbility::StaticClass()); + UUE5CoroGASTestGameplayAbility* Ability = nullptr; + if (!bInstanced) + Ability = CDO; + else + for (auto* Obj : TObjectRange()) + Ability = Obj; + Test.TestNotNull(TEXT("Ability found"), Ability); + Test.TestEqual(TEXT("Started"), State, 1); + Ability->CancelAbility(UUE5CoroGASTestGameplayAbility::Handle, + UUE5CoroGASTestGameplayAbility::ActorInfo, + UUE5CoroGASTestGameplayAbility::ActivationInfo, + false); + Test.TestEqual(TEXT("Cancellation not processed yet"), State, 1); + World.Tick(); + // Instanced force cancels (2), non-instanced is a regular cancel (2->3) + Test.TestEqual(TEXT("Canceled"), State, bInstanced ? 2 : 3); + } + + { + FGASTestWorld World; + UUE5CoroGASTestGameplayAbility::Reset(); + Test.TestEqual(TEXT("Clean"), State, 0); + World.Run(UUE5CoroGASTestGameplayAbility::StaticClass()); + } // Force cancel by destroying the world + Test.TestEqual(TEXT("Canceled"), State, 2); +} +} + +bool FGameplayAbilityTestNonInstanced::RunTest(const FString& Parameters) +{ + DoTest(*this, EGameplayAbilityInstancingPolicy::NonInstanced); + return true; +} + +bool FGameplayAbilityTestPerActor::RunTest(const FString& Parameters) +{ + DoTest(*this, EGameplayAbilityInstancingPolicy::InstancedPerActor); + return true; +} + +bool FGameplayAbilityTestPerExecution::RunTest(const FString& Parameters) +{ + DoTest(*this, EGameplayAbilityInstancingPolicy::InstancedPerExecution); + return true; +} diff --git a/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTestAbilityTask.cpp b/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTestAbilityTask.cpp new file mode 100644 index 00000000..fc836608 --- /dev/null +++ b/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTestAbilityTask.cpp @@ -0,0 +1,70 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5CoroGASTestAbilityTask.h" +#include "UE5Coro/AsyncAwaiters.h" +#include "UE5Coro/Cancellation.h" +#include "UE5Coro/LatentAwaiters.h" + +using namespace UE5Coro::Latent; + +auto UUE5CoroGASTestAbilityTask::Run(UGameplayAbility* InAbility) -> ThisClass* +{ + return NewAbilityTask(InAbility); +} + +UE5Coro::GAS::FAbilityCoroutine UUE5CoroGASTestAbilityTask::Execute() +{ + State = 1; + { + UE5Coro::FCancellationGuard _; + ON_SCOPE_EXIT { State = 2; }; + co_await NextTick(); + } + State = 3; + if (bSoftCancel) + co_await Cancel(); + co_await NextTick(); + State = 4; + + ON_SCOPE_EXIT { State = 5; }; + co_await PerformLastStep; +} + +void UUE5CoroGASTestAbilityTask::Succeeded() +{ + State = 10; +} + +void UUE5CoroGASTestAbilityTask::Failed() +{ + State = 11; +} diff --git a/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTestAbilityTask.h b/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTestAbilityTask.h new file mode 100644 index 00000000..87fe3a31 --- /dev/null +++ b/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTestAbilityTask.h @@ -0,0 +1,52 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "UE5CoroGAS/UE5CoroAbilityTask.h" +#include "UE5CoroGASTestAbilityTask.generated.h" + +UCLASS(MinimalAPI) +class UUE5CoroGASTestAbilityTask : public UUE5CoroAbilityTask +{ + GENERATED_BODY() + +public: + int State = 0; + bool bSoftCancel = false; + TDelegate PerformLastStep; + + static ThisClass* Run(UGameplayAbility*); + + virtual UE5Coro::GAS::FAbilityCoroutine Execute() override; + virtual void Succeeded() override; + virtual void Failed() override; +}; diff --git a/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTestAvatar.cpp b/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTestAvatar.cpp new file mode 100644 index 00000000..98557046 --- /dev/null +++ b/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTestAvatar.cpp @@ -0,0 +1,49 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5CoroGASTestAvatar.h" +#include "AbilitySystemComponent.h" + +AUE5CoroGASTestAvatar::AUE5CoroGASTestAvatar() +{ + ASC = CreateDefaultSubobject("ASC"); +} + +void AUE5CoroGASTestAvatar::BeginPlay() +{ + Super::BeginPlay(); + ASC->InitAbilityActorInfo(this, this); +} + +UAbilitySystemComponent* AUE5CoroGASTestAvatar::GetAbilitySystemComponent() const +{ + return ASC; +} diff --git a/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTestAvatar.h b/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTestAvatar.h new file mode 100644 index 00000000..6a7fcec0 --- /dev/null +++ b/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTestAvatar.h @@ -0,0 +1,50 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "AbilitySystemInterface.h" +#include "GameFramework/Pawn.h" +#include "UE5CoroGASTestAvatar.generated.h" + +UCLASS(MinimalAPI) +class AUE5CoroGASTestAvatar : public APawn, public IAbilitySystemInterface +{ + GENERATED_BODY() + +public: + UPROPERTY(VisibleAnywhere) + UAbilitySystemComponent* ASC; + + AUE5CoroGASTestAvatar(); + virtual void BeginPlay() override; + virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override; +}; diff --git a/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTestGameplayAbility.cpp b/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTestGameplayAbility.cpp new file mode 100644 index 00000000..8811bdd3 --- /dev/null +++ b/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTestGameplayAbility.cpp @@ -0,0 +1,96 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "UE5CoroGASTestGameplayAbility.h" +#include "Tasks/GameplayTask_WaitDelay.h" +#include "UE5Coro/AsyncAwaiters.h" +#include "UE5Coro/Cancellation.h" +#include "UE5Coro/LatentAwaiters.h" + +using namespace UE5Coro; +using namespace UE5Coro::GAS; +using namespace UE5Coro::Latent; + +void UUE5CoroGASTestGameplayAbility::SetInstancingPolicy( + EGameplayAbilityInstancingPolicy::Type Policy) +{ + GetMutableDefault()->InstancingPolicy = Policy; +} + +void UUE5CoroGASTestGameplayAbility::Reset() +{ + State = 0; + Handle = {}; + ActorInfo = nullptr; + ActivationInfo = {}; + TriggerEventData = nullptr; +} + +FAbilityCoroutine UUE5CoroGASTestGameplayAbility::ExecuteAbility( + FGameplayAbilitySpecHandle InHandle, + const FGameplayAbilityActorInfo* InActorInfo, + FGameplayAbilityActivationInfo InActivationInfo, + const FGameplayEventData* InTriggerEventData) +{ + Handle = InHandle; + ActorInfo = InActorInfo; + ActivationInfo = InActivationInfo; + TriggerEventData = InTriggerEventData; + + CommitAbility(InHandle, InActorInfo, InActivationInfo); + + State = 1; + { + FCancellationGuard _; + // Instanced abilities remove latent actions when canceled, so the guard + // will be ignored if the ability is canceled here + ON_SCOPE_EXIT { State = 2; }; + co_await NextTick(); + } + State = 3; + co_await NextTick(); + State = 4; + + // UGameplayTask_WaitDelay only works on instanced abilities + if (GetInstancingPolicy() != EGameplayAbilityInstancingPolicy::NonInstanced) + { + // UGameplayTask_WaitDelay is MinimalAPI + auto* Class = UGameplayTask_WaitDelay::StaticClass(); + auto* Fn = Class->FindFunctionByName("TaskWaitDelay"); + TTuple, float, uint8, + UGameplayTask_WaitDelay*> Params{this, 1, 192, nullptr}; + GetMutableDefault()->ProcessEvent(Fn, &Params); + co_await Task(Params.Get()); + State = 5; + } + ON_SCOPE_EXIT { State = 6; }; + co_await PerformLastStep; +} diff --git a/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTestGameplayAbility.h b/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTestGameplayAbility.h new file mode 100644 index 00000000..2752c6c5 --- /dev/null +++ b/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTestGameplayAbility.h @@ -0,0 +1,60 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "UE5CoroGAS/UE5CoroGameplayAbility.h" +#include "UE5CoroGASTestGameplayAbility.generated.h" + +UCLASS(MinimalAPI) +class UUE5CoroGASTestGameplayAbility : public UUE5CoroGameplayAbility +{ + GENERATED_BODY() + +public: + static void SetInstancingPolicy(EGameplayAbilityInstancingPolicy::Type); + static void Reset(); + + static inline int State; + static inline TDelegate PerformLastStep; + + static inline FGameplayAbilitySpecHandle Handle; + static inline const FGameplayAbilityActorInfo* ActorInfo; + static inline FGameplayAbilityActivationInfo ActivationInfo; + static inline const FGameplayEventData* TriggerEventData; + +protected: + virtual UE5Coro::GAS::FAbilityCoroutine + ExecuteAbility(FGameplayAbilitySpecHandle Handle, + const FGameplayAbilityActorInfo* ActorInfo, + FGameplayAbilityActivationInfo ActivationInfo, + const FGameplayEventData* TriggerEventData) override; +}; diff --git a/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTests.cpp b/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTests.cpp new file mode 100644 index 00000000..37730ed3 --- /dev/null +++ b/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/Private/UE5CoroGASTests.cpp @@ -0,0 +1,38 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "Modules/ModuleManager.h" + +class FUE5CoroGASTestsModule : public IModuleInterface +{ +}; + +IMPLEMENT_MODULE(FUE5CoroGASTestsModule, UE5CoroGASTests); diff --git a/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/UE5CoroGASTests.Build.cs b/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/UE5CoroGASTests.Build.cs new file mode 100644 index 00000000..f940bbe8 --- /dev/null +++ b/Plugins/UE5CoroGAS/Source/UE5CoroGASTests/UE5CoroGASTests.Build.cs @@ -0,0 +1,48 @@ +// Copyright © Laura Andelare +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted (subject to the limitations in the disclaimer +// below) provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using UnrealBuildTool; + +public class UE5CoroGASTests : UE5CoroModuleRules +{ + public UE5CoroGASTests(ReadOnlyTargetRules Target) + : base(Target) + { + PublicDependencyModuleNames.AddRange(new[] + { + "GameplayAbilities", + "GameplayTasks", + "UE5Coro", + "UE5CoroGAS", + "UE5CoroTests", + }); + } +} diff --git a/Plugins/UE5CoroGAS/UE5CoroGAS.uplugin b/Plugins/UE5CoroGAS/UE5CoroGAS.uplugin new file mode 100644 index 00000000..231d6ad8 --- /dev/null +++ b/Plugins/UE5CoroGAS/UE5CoroGAS.uplugin @@ -0,0 +1,40 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.10.2-ue4", + "FriendlyName": "UE5Coro – Gameplay Ability System (UE4 edition)", + "Description": "C++20 coroutines for GAS", + "Category": "Programming", + "CreatedBy": "Laura Andelare", + "CreatedByURL": "https://github.com/landelare/ue5coro", + "DocsURL": "", + "MarketplaceURL": "", + "SupportURL": "", + "EnabledByDefault": false, + "CanContainContent": false, + "IsBetaVersion": false, + "IsExperimentalVersion": true, + "Installed": false, + "Modules": [ + { + "Name": "UE5CoroGAS", + "Type": "Runtime", + "LoadingPhase": "PreDefault" + }, + { + "Name": "UE5CoroGASTests", + "Type": "UncookedOnly", + "LoadingPhase": "PreDefault" + } + ], + "Plugins": [ + { + "Name": "GameplayAbilities", + "Enabled": true + }, + { + "Name": "UE5Coro", + "Enabled": true + } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 00000000..81979784 --- /dev/null +++ b/README.md @@ -0,0 +1,212 @@ +# UE5Coro 1, UE4 edition + +These plugins implement C\+\+ +[coroutines](https://en.cppreference.com/w/cpp/language/coroutines) for +Unreal Engine 4 with a focus on gameplay logic and BP integration. + +> [!CAUTION] +> You're looking at a special, unsupported release. + +## Installation + +Download the release that you wish to use from the +[Releases](https://github.com/landelare/ue5coro/releases) page, and copy the +contents of its Plugins folder into your project's Plugins folder. +Done correctly, you should end up with +`YourProject\Plugins\UE5Coro\UE5Coro.uplugin`. + +## Project setup + +You will need to use `CppStandardVersion.Latest` to get C++20. + +As this is an unsupported release, detailed instructions are not provided. + +## Usage + +The UE5Coro plugin containing core functionality is enabled by default. +Reference the `"UE5Coro"` module from your Build.cs as you would any other +module and `#include "UE5Coro.h"`. + +Other plugins, such as UE5CoroAI, UE5CoroGAS need to be manually enabled and +referenced normally: e.g., `"UE5CoroAI"` in Build.cs, `#include "UE5CoroAI.h"` +in your code. +All UE5Coro plugins follow this pattern of providing a single header. + +Using these meta-headers is the recommended and supported approach. +You may opt to IWYU the various smaller headers, but no guidance is given as to +which feature requires which header. +IDEs most commonly used with Unreal Engine are known to fail to suggest the +correct header for some features. + +## Feature overview + +Click these links for the detailed description of the main features provided +by these plugins, or keep reading for a few highlights. + +### UE5Coro + +* [Async coroutines](Docs/Async.md) control their own resumption by awaiting +various awaiter objects. They can be used to implement BP latent actions such as +Delay, or as a generic fork in code execution like AsyncTask, but not +necessarily involving multithreading. + * [Cancellation](Docs/Cancellation.md) support has its own page. +* [Generators](Docs/Generator.md) are caller-controlled and return a variable +number of results without having to allocate and go through a temporary TArray. +* [Overview of built-in awaiters](Docs/Awaiters.md) that you can use with async +coroutines. + +### UE5CoroAI + +* [AI module and navigation system](Docs/AI.md) integration. + +### UE5CoroGAS + +* [Gameplay Ability System](Docs/GAS.md) integration. + +## Async coroutine examples + +Return `UE5Coro::TCoroutine<>` from a function to make it coroutine enabled and +support co_await inside. +UFUNCTIONs need to use the `FAsyncCoroutine` wrapper. + +Having a `FLatentActionInfo` parameter makes the coroutine implement a BP latent +action. +You do not need to do anything with this parameter, just have it and UE5Coro +will register it with the latent action manager. +World context objects are also supported and automatically processed. +It's recommended to have them as the first parameter. +Don't forget the necessary UFUNCTION metadata to make this a latent node in BP! + +```cpp +UFUNCTION(BlueprintCallable, Meta = (Latent, LatentInfo = "LatentInfo")) +FAsyncCoroutine AExampleActor::Latent(FLatentActionInfo LatentInfo) +{ + // This will *not* block the game thread for a second! + co_await UE5Coro::Latent::Seconds(1.0); + OneSecondLater(); +} +``` + +The returned struct has no use in BP and is automatically hidden: +![AExampleActor::Latent as a BP node](Docs/latent_node.png) + +You're not limited to BP latent actions, or UCLASS members: + +```cpp +UE5Coro::TCoroutine<> MyGlobalHelperFunction() +{ + co_await UE5Coro::Latent::Seconds(1.0); + OneSecondLater(); +} +``` + +Or even regular functions: + +```cpp +void Example(int Value) +{ + auto Lambda = [Value]() -> UE5Coro::TCoroutine + { + co_await UE5Coro::Async::MoveToThread( + ENamedThreads::AnyBackgroundThreadNormalTask); + co_return PerformExpensiveTask(Value); + }; + int ExpensiveResult = Lambda().GetResult(); +} +``` + +Both BP latent actions and free-running asynchronous coroutines have a unified +feature set: you can seamlessly co_await the same things from both and if +needed, your BP latent action becomes a threading placeholder or additional +behind-the-scenes latent actions are started as needed. + +BP Latent actions are considered complete for BP when control leaves the scope +of the coroutine body completely, either implicitly (running to the final `}`) +or explicitly via `co_return;`. + +Asynchronous coroutines (in both modes) synchronously return to their callers at +the first co_await or co_return that they encounter and the rest of the function +body runs either independently (in async mode) or through the latent action +manager (in latent mode). + +Everything co_awaitable works in every asynchronous coroutine, regardless of its +BP integration: + +```cpp +using namespace UE5Coro; + +UFUNCTION(BlueprintCallable, Meta = (Latent, LatentInfo = "LatentInfo")) +FAsyncCoroutine UExampleFunctionLibrary::K2_Foo(FLatentActionInfo LatentInfo) +{ + // You can freely hop between threads even though this is BP: + co_await Async::MoveToThread(ENamedThreads::AnyBackgroundThreadNormalTask); + DoExpensiveThingOnBackgroundThread(); + + // However, awaiting latent actions has to be started from the game thread: + co_await Async::MoveToGameThread(); + co_await Latent::Seconds(1.0f); +} +``` + +There are various other engine features with coroutine support including some +engine types that are made directly co_awaitable in `TCoroutine`s. +Check out the [Awaiters](Docs/Awaiters.md) page for an overview. + +## Generator examples + +Generators can be used to return an arbitrary number of items from a function +without having to pass them through temp arrays, etc. +In C# they're known as iterators. + +Returning `UE5Coro::TGenerator` makes a function coroutine enabled, +supporting `co_yield`: + +```cpp +using namespace UE5Coro; + +TGenerator MakeParkingSpaces(int Num) +{ + for (int i = 1; i <= Num; ++i) + co_yield FString::Printf(TEXT("🅿️ %d"), i); +} + +// Elsewhere +for (const FString& Str : MakeParkingSpaces(123)) + Process(Str); +``` + +co_yield and co_await cannot be mixed. +Asynchronous coroutines control their own execution and wait for certain events, +while generators are caller-controlled and yield values on demand. + +In particular, it's not guaranteed that your generator function body will even +run to completion if your caller decides to stop early. +This enables scenarios where generators may co_yield an infinite number of +elements and callers only taking a finite few: + +```cpp +using namespace UE5Coro; + +TGenerator NotTrulyInfinite() +{ + FString WillBeDestroyed = TEXT("Read on"); + int* Dangerous = new int; + for (;;) + co_yield 1; + delete Dangerous; +} + +// Elsewhere: +TGenerator Generator = NotTrulyInfinite(); +for (int i = 0; i < 5; ++i) + Generator.Resume(); +``` + +In this case, your coroutine stack will be unwound when the TGenerator object +is destroyed, and the destructors of locals within the coroutine run like usual, +as if the last `co_yield` was a `throw` (but no exceptions are involved). + +In the example above, the FString will be freed but the `delete` line will never +run. +Use RAII or helpers such as `ON_SCOPE_EXIT` if you expect to not run to +completion.