Files
Mike NelsonandJohn Fernandez 880bfb11a7 Upgrade lua enumeration handling (#7423)
* 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]>
2026-06-05 11:33:42 +02:00

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