Lua API: Plugins can now execute gamestate code once they are in the game context.
This commit is contained in:
parent
cc1069734c
commit
3387eb2c26
8 changed files with 282 additions and 6 deletions
|
@ -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
|
||||
|
|
|
@ -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(); });
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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_;
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue