mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-08 07:28:22 -10:00
* upgrade lua enumeration handling * some cleanup * add lua enum descriptions * minor fixes * Update code/scripting/api/objs/enums.cpp Co-authored-by: John Fernandez <[email protected]> * include name --------- Co-authored-by: John Fernandez <[email protected]>
439 lines
13 KiB
C++
439 lines
13 KiB
C++
|
|
#include "async.h"
|
|
|
|
#include "executor/CombinedExecutionContext.h"
|
|
#include "executor/GameStateExecutionContext.h"
|
|
#include "executor/global_executors.h"
|
|
#include "scripting/api/LuaCoroutineRunner.h"
|
|
#include "scripting/api/LuaExecutionContext.h"
|
|
#include "scripting/api/objs/execution_context.h"
|
|
#include "scripting/api/objs/executor.h"
|
|
#include "scripting/api/objs/promise.h"
|
|
#include "scripting/lua/LuaThread.h"
|
|
|
|
#include "freespace.h"
|
|
|
|
namespace scripting {
|
|
namespace api {
|
|
|
|
//**********LIBRARY: Async
|
|
ADE_LIB(l_Async, "Async", "async", "Support library for asynchronous operations");
|
|
|
|
int executorGetter(lua_State* L, const std::shared_ptr<executor::Executor>& executor)
|
|
{
|
|
if (ADE_SETTING_VAR) {
|
|
LuaError(L, "This property is read only!");
|
|
return ADE_RETURN_NIL;
|
|
}
|
|
|
|
return ade_set_args(L, "o", l_Executor.Set(executor_h(executor)));
|
|
}
|
|
|
|
ADE_VIRTVAR(OnFrameExecutor,
|
|
l_Async,
|
|
nullptr,
|
|
"An executor that executes operations at the end of rendering a frame.",
|
|
"executor",
|
|
"The executor handle")
|
|
{
|
|
return executorGetter(L, executor::OnFrameExecutor);
|
|
}
|
|
|
|
ADE_VIRTVAR(OnSimulationExecutor,
|
|
l_Async,
|
|
nullptr,
|
|
"An executor that executes operations after all object simulation has been done but before rendering starts. This "
|
|
"is the place to do physics manipulations.",
|
|
"executor",
|
|
"The executor handle")
|
|
{
|
|
return executorGetter(L, executor::OnSimulationExecutor);
|
|
}
|
|
|
|
/**
|
|
* @brief A simple run context that allows a promise to be resolved by calling a lua function
|
|
*
|
|
* This is very similar to JavaScript Promises where a user defined function is passed a special "resolve" function that
|
|
* should be called when the coroutine is finished.
|
|
*/
|
|
class lua_func_resolve_context : public resolve_context {
|
|
public:
|
|
lua_func_resolve_context(lua_State* L, luacpp::LuaFunction callback) : _luaState(L), _callback(std::move(callback))
|
|
{
|
|
}
|
|
|
|
void setResolver(Resolver resolver) override
|
|
{
|
|
struct shared_state {
|
|
Resolver resolver;
|
|
bool alreadyCalled = false;
|
|
};
|
|
|
|
auto state = std::make_shared<shared_state>();
|
|
state->resolver = std::move(resolver);
|
|
|
|
const auto resolveFunc = luacpp::LuaFunction::createFromStdFunction(_callback.getLuaState(),
|
|
[state](lua_State* L, const luacpp::LuaValueList& resolveVals) {
|
|
if (state->alreadyCalled) {
|
|
LuaError(L, "Promise has already been resolved or rejected before!");
|
|
return luacpp::LuaValueList();
|
|
}
|
|
|
|
state->resolver(false, resolveVals);
|
|
state->alreadyCalled = true;
|
|
|
|
state->resolver = nullptr;
|
|
return luacpp::LuaValueList();
|
|
});
|
|
|
|
const auto rejectFunc = luacpp::LuaFunction::createFromStdFunction(_callback.getLuaState(),
|
|
[state](lua_State* L, const luacpp::LuaValueList& resolveVals) {
|
|
if (state->alreadyCalled) {
|
|
LuaError(L, "Promise has already been resolved or rejected before!");
|
|
return luacpp::LuaValueList();
|
|
}
|
|
|
|
state->resolver(true, resolveVals);
|
|
state->alreadyCalled = true;
|
|
|
|
state->resolver = nullptr;
|
|
return luacpp::LuaValueList();
|
|
});
|
|
|
|
_callback(_luaState, {resolveFunc, rejectFunc});
|
|
}
|
|
|
|
private:
|
|
lua_State* _luaState = nullptr;
|
|
luacpp::LuaFunction _callback;
|
|
};
|
|
|
|
ADE_FUNC(promise,
|
|
l_Async,
|
|
"function(function(any resolveVal) => void resolve, function(any errorVal) => void reject) => void body",
|
|
"Creates a promise that resolves when the resolve function of the callback is called or errors if the reject "
|
|
"function is called. The function will be called "
|
|
"on its own.",
|
|
"promise",
|
|
"The promise or nil on error")
|
|
{
|
|
luacpp::LuaFunction callback;
|
|
if (!ade_get_args(L, "u", &callback)) {
|
|
return ADE_RETURN_NIL;
|
|
}
|
|
|
|
if (!callback.isValid()) {
|
|
LuaError(L, "Invalid function supplied!");
|
|
return ADE_RETURN_NIL;
|
|
}
|
|
|
|
std::unique_ptr<resolve_context> resolveCtx(new lua_func_resolve_context(L, std::move(callback)));
|
|
|
|
return ade_set_args(L, "o", l_Promise.Set(LuaPromise(std::move(resolveCtx))));
|
|
}
|
|
|
|
ADE_FUNC(resolved,
|
|
l_Async,
|
|
"any... resolveValues",
|
|
"Creates a resolved promise with the values passed to this function.",
|
|
"promise",
|
|
"Resolved promise")
|
|
{
|
|
auto nargs = lua_gettop(L);
|
|
luacpp::LuaValueList values;
|
|
values.reserve(nargs);
|
|
|
|
for (int i = 1; i <= nargs; ++i) {
|
|
luacpp::LuaValue val;
|
|
val.setReference(luacpp::UniqueLuaReference::create(L, i));
|
|
values.push_back(val);
|
|
}
|
|
|
|
return ade_set_args(L, "o", l_Promise.Set(LuaPromise::resolved(std::move(values))));
|
|
}
|
|
|
|
ADE_FUNC(errored,
|
|
l_Async,
|
|
"any... errorValues",
|
|
"Creates an errored promise with the values passed to this function.",
|
|
"promise",
|
|
"Errored promise")
|
|
{
|
|
auto nargs = lua_gettop(L);
|
|
luacpp::LuaValueList values;
|
|
values.reserve(nargs);
|
|
|
|
for (int i = 1; i <= nargs; ++i) {
|
|
luacpp::LuaValue val;
|
|
val.setReference(luacpp::UniqueLuaReference::create(L, i));
|
|
values.push_back(val);
|
|
}
|
|
|
|
return ade_set_args(L, "o", l_Promise.Set(LuaPromise::errored(std::move(values))));
|
|
}
|
|
|
|
ADE_FUNC(run,
|
|
l_Async,
|
|
"function() => any body, [executor executeOn = nil, boolean|execution_context captureContext = true /* Captures "
|
|
"game state context by default */]",
|
|
"Runs an asynchronous function. Inside this function you can use async.await to suspend the function until a "
|
|
"promise resolves. Also allows to specify an executor on which the code of the coroutine should be executed. If "
|
|
"captureContext is true then the game context (the game state) at the time of the call is captured and the "
|
|
"coroutine is only run if that state is still active.",
|
|
"promise",
|
|
"A promise that resolves with the return value of the body when it reaches a return statement")
|
|
{
|
|
luacpp::LuaFunction body;
|
|
executor_h executor;
|
|
std::shared_ptr<executor::IExecutionContext> context;
|
|
|
|
if (lua_type(L, 3) == LUA_TUSERDATA) {
|
|
execution_context_h* ctx = nullptr;
|
|
if (!ade_get_args(L, "u|oo", &body, l_Executor.Get(&executor), l_ExecutionContext.GetPtr(&ctx))) {
|
|
return ADE_RETURN_NIL;
|
|
}
|
|
if (ctx != nullptr) {
|
|
context = ctx->getExecutionContext();
|
|
}
|
|
} else {
|
|
bool captureContext = true;
|
|
if (!ade_get_args(L, "u|ob", &body, l_Executor.Get(&executor), &captureContext)) {
|
|
return ADE_RETURN_NIL;
|
|
}
|
|
if (executor.isValid() && captureContext) {
|
|
context = executor::GameStateExecutionContext::captureContext();
|
|
}
|
|
}
|
|
|
|
auto coroutine = luacpp::LuaThread::create(L, body);
|
|
coroutine.setErrorCallback([](lua_State*, lua_State* thread) {
|
|
LuaError(thread);
|
|
return true;
|
|
});
|
|
|
|
return ade_set_args(L,
|
|
"o",
|
|
l_Promise.Set(runAsyncCoroutine(std::move(coroutine), executor.getExecutor(), std::move(context))));
|
|
}
|
|
|
|
ADE_FUNC(awaitRunOnFrame,
|
|
l_Async,
|
|
"function() => any body, [boolean allowMultiProcessing = false]",
|
|
"Runs an asynchronous function in an OnFrameExecutor context and busy-waits for the coroutine to finish. "
|
|
"Inside this function you can use async.await to suspend the function until a promise resolves. "
|
|
"This is useful for cases where you need a scripting process to run over multiple frames, even when "
|
|
"the engine is not in a stable game state (such as showing animations during game state switches, etc.).",
|
|
"any... | nil",
|
|
"The result of the function body or nil if the function errored")
|
|
{
|
|
luacpp::LuaFunction body;
|
|
bool allowMulti = false;
|
|
std::shared_ptr<executor::IExecutionContext> context;
|
|
|
|
if (!ade_get_args(L, "u|b", &body, &allowMulti)) {
|
|
return ADE_RETURN_NIL;
|
|
}
|
|
|
|
context = executor::GameStateExecutionContext::captureContext();
|
|
|
|
|
|
auto coroutine = luacpp::LuaThread::create(L, body);
|
|
coroutine.setErrorCallback([](lua_State*, lua_State* thread) {
|
|
LuaError(thread);
|
|
return true;
|
|
});
|
|
|
|
auto promise = runAsyncCoroutine(std::move(coroutine), executor::OnFrameExecutor, std::move(context));
|
|
|
|
int screen_id = gr_save_screen();
|
|
|
|
//Keep drawing pretend-frames until we have the promise resolved
|
|
while (promise.isValid() && !(promise.isResolved() || promise.isErrored())){
|
|
os_poll();
|
|
|
|
game_set_frametime(-1);
|
|
game_do_state_common(gameseq_get_state(), allowMulti ? 0 : 1); // do stuff common to all states
|
|
|
|
gr_restore_screen(screen_id);
|
|
gr_flip();
|
|
}
|
|
|
|
gr_free_screen(screen_id);
|
|
|
|
if (!promise.isValid() || promise.isErrored())
|
|
return ADE_RETURN_NIL;
|
|
|
|
// We can't use our usual functions for returning here since we return a variable amount of values
|
|
const auto& retVals = promise.resolveValue();
|
|
for (const auto& retVal : retVals) {
|
|
retVal.pushValue(L);
|
|
}
|
|
return static_cast<int>(retVals.size());
|
|
}
|
|
|
|
ADE_FUNC(await,
|
|
l_Async,
|
|
"promise",
|
|
"Suspends an asynchronous coroutine until the passed promise resolves.",
|
|
"any",
|
|
"The resolve value of the promise")
|
|
{
|
|
// await cannot be used on the main thread since there is nothing that will wait for the promise
|
|
if (lua_pushthread(L)) {
|
|
// We are the main thread
|
|
lua_pop(L, 1);
|
|
|
|
LuaError(L, "Tried to await something on the main thread! That is not supported.");
|
|
return ADE_RETURN_NIL;
|
|
}
|
|
lua_pop(L, 1);
|
|
|
|
LuaPromise* promise = nullptr;
|
|
if (!ade_get_args(L, "o", l_Promise.GetPtr(&promise))) {
|
|
return ADE_RETURN_NIL;
|
|
}
|
|
|
|
if (promise == nullptr || !promise->isValid()) {
|
|
LuaError(L, "Invalid promise detected. This should not happen. Please contact a developer.");
|
|
return ADE_RETURN_NIL;
|
|
}
|
|
|
|
if (promise->isResolved()) {
|
|
// No need to suspend if the promise is already resolved. Just take the resolve values from the promise and
|
|
// return them
|
|
const auto& retVals = promise->resolveValue();
|
|
for (const auto& retVal : retVals) {
|
|
retVal.pushValue(L); // Push onto this stack to ensure it is actually returned from this function
|
|
}
|
|
return static_cast<int>(retVals.size());
|
|
}
|
|
|
|
// Return the promise via the yield so that the resumer can register themself on the promise to resume when that
|
|
// resolves
|
|
return lua_yield(L, 1);
|
|
}
|
|
|
|
ADE_FUNC(yield,
|
|
l_Async,
|
|
nullptr,
|
|
"Returns a promise that will resolve on the next execution of the current executor. Effectively allows to "
|
|
"asynchronously wait until the next frame.",
|
|
"promise",
|
|
"The promise")
|
|
{
|
|
if (executor::currentExecutor() == nullptr) {
|
|
LuaError(L,
|
|
"There is no running executor at the moment. This function needs to be called from a executor callback.");
|
|
return ADE_RETURN_NIL;
|
|
}
|
|
|
|
class yield_resolve_context : public resolve_context {
|
|
public:
|
|
explicit yield_resolve_context(executor::Executor* executor) : m_exec(executor) {}
|
|
void setResolver(Resolver resolver) override
|
|
{
|
|
m_exec->post([resolver = std::move(resolver)]() {
|
|
resolver(false, luacpp::LuaValueList());
|
|
return executor::Executor::CallbackResult::Done;
|
|
});
|
|
}
|
|
|
|
private:
|
|
executor::Executor* m_exec = nullptr;
|
|
};
|
|
return ade_set_args(L,
|
|
"o",
|
|
l_Promise.Set(LuaPromise(std::make_shared<yield_resolve_context>(executor::currentExecutor()))));
|
|
}
|
|
|
|
ADE_FUNC(error,
|
|
l_Async,
|
|
"any... errorValues",
|
|
"Causes the currently running coroutine to fail with an error with the specified values.",
|
|
nullptr,
|
|
"Does not return")
|
|
{
|
|
// error cannot be used on the main thread since there is nothing that will wait for the promise
|
|
if (lua_pushthread(L)) {
|
|
// We are the main thread
|
|
lua_pop(L, 1);
|
|
|
|
LuaError(L, "Tried to cause a coroutine error on the main thread! That is not supported.");
|
|
return ADE_RETURN_NIL;
|
|
}
|
|
lua_pop(L, 1);
|
|
|
|
auto nargs = lua_gettop(L);
|
|
luacpp::LuaValueList values;
|
|
values.reserve(nargs);
|
|
|
|
for (int i = 1; i <= nargs; ++i) {
|
|
luacpp::LuaValue val;
|
|
val.setReference(luacpp::UniqueLuaReference::create(L, i));
|
|
values.push_back(val);
|
|
}
|
|
|
|
// Push an errored promise on the stack and then yield the coroutine. The awaiter will get this promise and stop
|
|
// execution.
|
|
ade_set_args(L, "o", l_Promise.Set(LuaPromise::errored(std::move(values))));
|
|
return lua_yield(L, 1);
|
|
}
|
|
|
|
//**********SUBLIBRARY: async/context
|
|
ADE_LIB_DERIV(l_Async_Context, "context", nullptr, "Support library for creating execution contexts.", l_Async);
|
|
|
|
ADE_FUNC(captureGameState,
|
|
l_Async_Context,
|
|
nullptr,
|
|
"Captures the current game state as an execution context",
|
|
"execution_context",
|
|
"The execution context or invalid handle on error")
|
|
{
|
|
return ade_set_args(L,
|
|
"o",
|
|
l_ExecutionContext.Set(execution_context_h(executor::GameStateExecutionContext::captureContext())));
|
|
}
|
|
|
|
ADE_FUNC(createLuaState,
|
|
l_Async_Context,
|
|
"function() => enumeration /* CONTEXT_* */",
|
|
"Creates an execution state by storing the passed function and calling that when the state is required. The function must return a CONTEXT_* enumeration.",
|
|
"execution_context",
|
|
"The execution context or invalid handle on error")
|
|
{
|
|
luacpp::LuaFunction body;
|
|
if (!ade_get_args(L, "u", &body)) {
|
|
return ade_set_args(L, "o", l_ExecutionContext.Set(execution_context_h(nullptr)));
|
|
}
|
|
|
|
return ade_set_args(L,
|
|
"o",
|
|
l_ExecutionContext.Set(execution_context_h(std::make_shared<LuaExecutionContext>(std::move(body)))));
|
|
}
|
|
|
|
ADE_FUNC(combineContexts,
|
|
l_Async_Context,
|
|
"execution_context... contexts",
|
|
"Combines several execution contexts into a single one by only return a valid state if all contexts are valid.",
|
|
"execution_context",
|
|
"The execution context or invalid handle on error")
|
|
{
|
|
SCP_vector<std::shared_ptr<executor::IExecutionContext>> contexts;
|
|
for (int i = 0; i < lua_gettop(L); ++i) {
|
|
execution_context_h* ctx;
|
|
internal::Ade_get_args_skip = i;
|
|
if (!ade_get_args(L, "o", l_ExecutionContext.GetPtr(&ctx))) {
|
|
return ade_set_args(L, "o", l_ExecutionContext.Set(execution_context_h(nullptr)));
|
|
}
|
|
contexts.emplace_back(ctx->getExecutionContext());
|
|
}
|
|
|
|
return ade_set_args(L,
|
|
"o",
|
|
l_ExecutionContext.Set(
|
|
execution_context_h(std::make_shared<executor::CombinedExecutionContext>(std::move(contexts)))));
|
|
}
|
|
|
|
} // namespace api
|
|
} // namespace scripting
|