Lua API: Plugins can now execute gamestate code once they are in the game context.

This commit is contained in:
Celtic Minstrel 2024-09-17 13:59:34 -04:00
parent cc1069734c
commit 3387eb2c26
8 changed files with 282 additions and 6 deletions

View file

@ -4,7 +4,6 @@
if wesnoth.kernel_type() == "Application Lua Kernel" then
print("Loading plugin module...")
wesnoth.plugin = {}
---Yields control back to the game until the next slice.
---@return WMLTable

View file

@ -280,6 +280,7 @@ void play_controller::init(const config& level)
plugins_context_->set_callback("save_game", [this](const config& cfg) { save_game_auto(cfg["filename"]); }, true);
plugins_context_->set_callback("save_replay", [this](const config& cfg) { save_replay_auto(cfg["filename"]); }, true);
plugins_context_->set_callback("quit", [](const config&) { throw_quit_game_exception(); }, false);
plugins_context_->set_callback_execute(*resources::lua_kernel);
plugins_context_->set_accessor_string("scenario_name", [this](config) { return get_scenario_name(); });
});
}

View file

@ -36,6 +36,7 @@
#include "scripting/lua_preferences.hpp"
#include "scripting/plugins/context.hpp"
#include "scripting/plugins/manager.hpp"
#include "scripting/push_check.hpp"
#ifdef DEBUG_LUA
#include "scripting/debug_lua.hpp"
@ -52,7 +53,6 @@
#include "lua/wrapper_lauxlib.h"
static lg::log_domain log_scripting_lua("scripting/lua");
#define DBG_LUA LOG_STREAM(debug, log_scripting_lua)
#define LOG_LUA LOG_STREAM(info, log_scripting_lua)
@ -93,6 +93,8 @@ static int intf_delay(lua_State* L)
return 0;
}
static int intf_execute(lua_State* L);
application_lua_kernel::application_lua_kernel()
: lua_kernel_base()
{
@ -108,6 +110,14 @@ application_lua_kernel::application_lua_kernel()
// Create the preferences table.
cmd_log_ << lua_preferences::register_table(mState);
// Create the wesnoth.plugin table
luaW_getglobal(mState, "wesnoth");
lua_newtable(mState);
lua_pushcfunction(mState, intf_execute);
lua_setfield(mState, -2, "execute");
lua_setfield(mState, -2, "plugin");
lua_pop(mState, 1);
}
application_lua_kernel::thread::thread(application_lua_kernel& owner, lua_State * T) : owner_(owner), T_(T), started_(false) {}
@ -216,6 +226,7 @@ application_lua_kernel::thread * application_lua_kernel::load_script_from_file(c
struct lua_context_backend {
std::vector<plugins_manager::event> requests;
lua_kernel_base* execute;
bool valid;
lua_context_backend()
@ -257,6 +268,47 @@ static int impl_context_accessor(lua_State * L, std::shared_ptr<lua_context_back
}
}
static int intf_execute(lua_State* L)
{
static const int CTX = 1, FUNC = 2, EVT = 3, EXEC = 4;
if(lua_gettop(L) == 2) lua_pushnil(L);
if(!luaW_table_get_def(L, CTX, "valid", false)) {
lua_pushboolean(L, false);
lua_pushstring(L, "context not valid");
return 2;
}
if(!luaW_tableget(L, CTX, "execute")) {
lua_pushboolean(L, false);
lua_pushstring(L, "context cannot execute");
return 2;
}
if(!lua_islightuserdata(L, EXEC)) {
lua_pushboolean(L, false);
lua_pushstring(L, "execute is not a thread");
return 2;
}
try {
config data = luaW_serialize_function(L, FUNC);
if(data["params"] != 0) {
lua_pushboolean(L, false);
lua_pushstring(L, "cannot execute function with parameters");
return 2;
}
if(!lua_isnil(L, EVT)) data["name"] = luaL_checkstring(L, EVT);
lua_pushvalue(L, FUNC);
data["ref"] = luaL_ref(L, LUA_REGISTRYINDEX);
std::shared_ptr<lua_context_backend>* context = static_cast<std::shared_ptr<lua_context_backend>*>(lua_touserdata(L, EXEC));
luaW_pushconfig(L, data);
impl_context_backend(L, *context, "execute");
} catch(luafunc_serialize_error& e) {
lua_pushboolean(L, false);
lua_pushstring(L, e.what());
return 2;
}
lua_pushboolean(L, true);
return 1;
}
bool luaW_copy_upvalues(lua_State* L, const config& cfg);
application_lua_kernel::request_list application_lua_kernel::thread::run_script(const plugins_context & ctxt, const std::vector<plugins_manager::event> & queue)
{
// There are two possibilities: (1) this is the first execution, and the C function is the only thing on the stack
@ -281,6 +333,11 @@ application_lua_kernel::request_list application_lua_kernel::thread::run_script(
lua_cpp::push_function(T_, std::bind(&impl_context_backend, std::placeholders::_1, this_context_backend, key));
lua_settable(T_, -3);
}
if(ctxt.execute_kernel_) {
lua_pushstring(T_, "execute");
lua_pushlightuserdata(T_, &this_context_backend);
lua_settable(T_, -3);
}
// Now we have to create the info object (context accessors). It is arranged as a table of boost functions.
lua_newtable(T_); // this will be the info table
@ -351,8 +408,59 @@ application_lua_kernel::request_list application_lua_kernel::thread::run_script(
application_lua_kernel::request_list results;
for (const plugins_manager::event & req : this_context_backend->requests) {
if(ctxt.execute_kernel_ && req.name == "execute") {
results.push_back([this, lk = ctxt.execute_kernel_, data = req.data]() {
auto result = lk->run_binary_lua_tag(data);
int ref = result["ref"];
auto func = result.mandatory_child("executed");
result.remove_children("executed");
result.remove_attribute("ref");
plugins_manager::get()->notify_event(result["name"], result);
lua_rawgeti(T_, LUA_REGISTRYINDEX, ref);
luaW_copy_upvalues(T_, func);
luaL_unref(T_, LUA_REGISTRYINDEX, ref);
lua_pop(T_, 1);
return true;
});
continue;
}
results.push_back(std::bind(ctxt.callbacks_.find(req.name)->second, req.data));
//results.emplace_back(ctxt.callbacks_.find(req.name)->second, req.data);
}
return results;
}
bool luaW_copy_upvalues(lua_State* L, const config& cfg)
{
if(auto upvalues = cfg.optional_child("upvalues")) {
lua_pushvalue(L, -1); // duplicate function because lua_getinfo will pop it
lua_Debug info;
lua_getinfo(L, ">u", &info);
int funcindex = lua_absindex(L, -1);
for(int i = 1; i <= info.nups; i++, lua_pop(L, 1)) {
std::string_view name = lua_getupvalue(L, funcindex, i);
if(name == "_ENV") {
lua_pushglobaltable(L);
} else if(upvalues->has_attribute(name)) {
luaW_pushscalar(L, (*upvalues)[name]);
} else if(upvalues->has_child(name)) {
const auto& child = upvalues->mandatory_child(name);
if(child["upvalue_type"] == "array") {
auto children = upvalues->child_range(name);
lua_createtable(L, children.size(), 0);
for(const auto& cfg : children) {
luaW_pushscalar(L, cfg["value"]);
lua_rawseti(L, -2, lua_rawlen(L, -2) + 1);
}
} else if(child["upvalue_type"] == "config") {
luaW_pushconfig(L, child);
} else if(child["upvalue_type"] == "function") {
luaW_copy_upvalues(L, child);
lua_pushvalue(L, -1);
}
} else continue;
lua_setupvalue(L, funcindex, i);
}
}
return true;
}

View file

@ -1065,10 +1065,10 @@ bool lua_kernel_base::protected_call(lua_State * L, int nArgs, int nRets, error_
return true;
}
bool lua_kernel_base::load_string(char const * prog, const std::string& name, error_handler e_h)
bool lua_kernel_base::load_string(const std::string& prog, const std::string& name, error_handler e_h, bool allow_unsafe)
{
// pass 't' to prevent loading bytecode which is unsafe and can be used to escape the sandbox.
int errcode = luaL_loadbufferx(mState, prog, strlen(prog), name.empty() ? prog : name.c_str(), "t");
int errcode = luaL_loadbufferx(mState, prog.c_str(), prog.size(), name.empty() ? prog.c_str() : name.c_str(), allow_unsafe ? "tb" : "t");
if (errcode != LUA_OK) {
char const * msg = lua_tostring(mState, -1);
std::string message = msg ? msg : "null string";
@ -1101,6 +1101,147 @@ void lua_kernel_base::run_lua_tag(const config& cfg)
}
this->run(cfg["code"].str().c_str(), cfg["name"].str(), nArgs);
}
config luaW_serialize_function(lua_State* L, int func)
{
if(lua_iscfunction(L, func)) {
throw luafunc_serialize_error("cannot serialize C function");
}
if(!lua_isfunction(L, func)) {
throw luafunc_serialize_error("cannot serialize callable non-function");
}
config data;
lua_Debug info;
lua_pushvalue(L, func); // push copy of function because lua_getinfo will pop it
lua_getinfo(L, ">u", &info);
data["params"] = info.nparams;
luaW_getglobal(L, "string", "dump");
lua_pushvalue(L, func);
lua_call(L, 1, 1);
data["code"] = lua_check<std::string>(L, -1);
lua_pop(L, 1);
config upvalues;
for(int i = 1; i <= info.nups; i++, lua_pop(L, 1)) {
std::string_view name = lua_getupvalue(L, func, i);
if(name == "_ENV") {
upvalues.add_child(name)["upvalue_type"] = "_ENV";
continue;
}
int idx = lua_absindex(L, -1);
switch(lua_type(L, idx)) {
case LUA_TBOOLEAN: case LUA_TNUMBER: case LUA_TSTRING:
luaW_toscalar(L, idx, upvalues[name]);
break;
case LUA_TFUNCTION:
upvalues.add_child(name, luaW_serialize_function(L, idx))["upvalue_type"] = "function";
break;
case LUA_TTABLE:
if(config cfg; luaW_toconfig(L, idx, cfg)) {
upvalues.add_child(name, cfg)["upvalue_type"] = "config";
break;
} else {
for(size_t i = 1; i <= lua_rawlen(L, -1); i++, lua_pop(L, 1)) {
lua_rawgeti(L, idx, i);
config& cfg = upvalues.add_child(name);
luaW_toscalar(L, -1, cfg["value"]);
cfg["upvalue_type"] = "array";
}
bool found_non_array = false;
for(lua_pushnil(L); lua_next(L, idx); lua_pop(L, 1)) {
if(lua_type(L, -2) != LUA_TNUMBER) {
found_non_array = true;
break;
}
}
if(!found_non_array) break;
}
[[fallthrough]];
default:
std::ostringstream os;
os << "cannot serialize function with upvalue " << name << " = ";
luaW_getglobal(L, "wesnoth", "as_text");
lua_pushvalue(L, idx);
lua_call(L, 1, 1);
os << luaL_checkstring(L, -1);
lua_pushboolean(L, false);
throw luafunc_serialize_error(os.str());
}
}
if(!upvalues.empty()) data.add_child("upvalues", upvalues);
return data;
}
bool lua_kernel_base::load_binary(const config& cfg, error_handler eh)
{
if(!load_string(cfg["code"].str(), cfg["name"], eh, true)) return false;
if(auto upvalues = cfg.optional_child("upvalues")) {
lua_pushvalue(mState, -1); // duplicate function because lua_getinfo will pop it
lua_Debug info;
lua_getinfo(mState, ">u", &info);
int funcindex = lua_absindex(mState, -1);
for(int i = 1; i <= info.nups; i++) {
std::string_view name = lua_getupvalue(mState, funcindex, i);
lua_pop(mState, 1); // we only want the upvalue's name, not its value
if(name == "_ENV") {
lua_pushglobaltable(mState);
} else if(upvalues->has_attribute(name)) {
luaW_pushscalar(mState, (*upvalues)[name]);
} else if(upvalues->has_child(name)) {
const auto& child = upvalues->mandatory_child(name);
if(child["upvalue_type"] == "array") {
auto children = upvalues->child_range(name);
lua_createtable(mState, children.size(), 0);
for(const auto& cfg : children) {
luaW_pushscalar(mState, cfg["value"]);
lua_rawseti(mState, -2, lua_rawlen(mState, -2) + 1);
}
} else if(child["upvalue_type"] == "config") {
luaW_pushconfig(mState, child);
} else if(child["upvalue_type"] == "function") {
if(!load_binary(child, eh)) return false;
}
} else continue;
lua_setupvalue(mState, funcindex, i);
}
}
return true;
}
config lua_kernel_base::run_binary_lua_tag(const config& cfg)
{
int top = lua_gettop(mState);
try {
error_handler eh = std::bind(&lua_kernel_base::throw_exception, this, std::placeholders::_1, std::placeholders::_2 );
if(load_binary(cfg, eh)) {
lua_pushvalue(mState, -1);
protected_call(0, LUA_MULTRET, eh);
}
} catch (const game::lua_error & e) {
cmd_log_ << e.what() << "\n";
lua_kernel_base::log_error(e.what(), "In function lua_kernel::run()");
config error;
error["name"] = "execute_error";
error["error"] = e.what();
return error;
}
config result;
result["ref"] = cfg["ref"];
result.add_child("executed") = luaW_serialize_function(mState, top + 1);
lua_remove(mState, top + 1);
result["name"] = "execute_result";
for(int i = top + 1; i < lua_gettop(mState); i++) {
std::string index = std::to_string(i - top);
switch(lua_type(mState, i)) {
case LUA_TNUMBER: case LUA_TBOOLEAN: case LUA_TSTRING:
luaW_toscalar(mState, i, result[index]);
break;
case LUA_TTABLE:
luaW_toconfig(mState, i, result.add_child(index));
break;
}
}
return result;
}
// Call load_string and protected call. Make them throw exceptions.
//
void lua_kernel_base::throwing_run(const char * prog, const std::string& name, int nArgs, bool in_interpreter)

View file

@ -32,6 +32,9 @@ public:
/** Runs a [lua] tag. Doesn't throw lua_error.*/
void run_lua_tag(const config& cfg);
/** Runs a binary [lua] tag. Doesn't throw lua_error.*/
config run_binary_lua_tag(const config& cfg);
/** Runs a plain script. Doesn't throw lua_error.*/
void run(char const *prog, const std::string& name, int nArgs = 0);
@ -125,7 +128,8 @@ protected:
// Execute a protected call, taking a lua_State as argument. For functions pushed into the lua environment, this version should be used, or the function cannot be used by coroutines without segfaulting (since they have a different lua_State pointer). This version is called by the above version.
static bool protected_call(lua_State * L, int nArgs, int nRets, error_handler);
// Load a string onto the stack as a function. Returns true if successful, error handler is called if not.
bool load_string(char const * prog, const std::string& name, error_handler);
bool load_string(const std::string& prog, const std::string& name, error_handler, bool allow_unsafe = false);
bool load_binary(const config& func, error_handler);
virtual bool protected_call(int nArgs, int nRets); // select default error handler polymorphically
virtual bool load_string(char const * prog, const std::string& name); // select default error handler polymorphically
@ -146,6 +150,12 @@ private:
std::vector<std::tuple<std::string, std::string>> registered_widget_definitions_;
};
config luaW_serialize_function(lua_State* L, int func);
struct luafunc_serialize_error : public std::runtime_error {
using std::runtime_error::runtime_error;
};
std::vector<std::string> luaW_get_attributes(lua_State* L, int idx);
struct game_config_tag {

View file

@ -16,6 +16,7 @@
#include "scripting/plugins/context.hpp"
#include "scripting/plugins/manager.hpp"
#include "scripting/lua_kernel_base.hpp"
#include <cassert>
#include <functional>
@ -103,3 +104,7 @@ void plugins_context::set_callback(const std::string & name, std::function<void(
{
set_callback(name, [func, preserves_context](config cfg) { func(cfg); return preserves_context; });
}
void plugins_context::set_callback_execute(lua_kernel_base& kernel) {
execute_kernel_ = &kernel;
}

View file

@ -47,6 +47,7 @@ public:
void set_callback(const std::string & name, callback_function);
void set_callback(const std::string & name, std::function<void(config)> function, bool preserves_context);
void set_callback_execute(class lua_kernel_base& kernel);
std::size_t erase_callback(const std::string & name);
std::size_t clear_callbacks();
@ -67,4 +68,5 @@ private:
callback_list callbacks_;
accessor_list accessors_;
std::string name_;
lua_kernel_base* execute_kernel_;
};

View file

@ -10,4 +10,14 @@
---Contains accessors for the current context
---@class plugin_info
---@field name string The name of the current context.
---@field [string] plugin_accessor An accessor takes a WML table as its argument and returns a string, integer, or WML table.
---@field [string] plugin_accessor An accessor takes a WML table as its argument and returns a string, integer, or WML table.
---Execute a function within the current context's game state, if supported.
---Functions returning a value can request that the result be returned in an event in the next slice.
---If the function raises an error, that too will be returned as an event in the next slice.
---@param context plugin_context The current plugin context.
---@param fcn function An arbitrary function to execute. The function will be run in a different Lua kernel and thus cannot access the wesnoth.plugin module.
---@param event_name? string The name to use for the event that contains the function's result
---@return boolean #True if the function will be executed; false if unsupported
---@return string? #If the first value is false, this will hold an explanatory string
function wesnoth.plugin.execute(context, fcn, event_name) end