Skip to content

Commit

Permalink
UE4 edition
Browse files Browse the repository at this point in the history
  • Loading branch information
landelare committed Jul 26, 2024
0 parents commit cd24d33
Show file tree
Hide file tree
Showing 133 changed files with 15,628 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.uasset binary
*.umap binary
22 changes: 22 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
30 changes: 30 additions & 0 deletions COPYING
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 7 additions & 0 deletions Docs/AI.md
Original file line number Diff line number Diff line change
@@ -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".
285 changes: 285 additions & 0 deletions Docs/Async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
# Async coroutines

Returning `UE5Coro::TCoroutine<T>` 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<T>` co_returns T, `TCoroutine<>` co_returns void.
`TCoroutine<>` and `TCoroutine<void>` 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&lt;T&gt; 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&lt;T&gt; 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<UObject> 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<UObject>();
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!
}
```
Loading

0 comments on commit cd24d33

Please sign in to comment.