summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Ward <rw@rubenwardy.com>2018-03-28 22:14:16 +0100
committerGitHub <noreply@github.com>2018-03-28 22:14:16 +0100
commit71b2570f0919d3bb5575c9ec694ecd004222fcea (patch)
treef3f85d185dde66ab294abb1c15e349ad3b33e818
parentdfc81983491417c5cd1c99d7db05e421c409379d (diff)
downloadminetest-71b2570f0919d3bb5575c9ec694ecd004222fcea.tar.gz
minetest-71b2570f0919d3bb5575c9ec694ecd004222fcea.tar.bz2
minetest-71b2570f0919d3bb5575c9ec694ecd004222fcea.zip
Load dependencies and description from mod.conf
-rw-r--r--builtin/mainmenu/modmgr.lua31
-rw-r--r--builtin/mainmenu/tab_mods.lua41
-rw-r--r--doc/lua_api.txt33
-rw-r--r--doc/menu_lua_api.txt11
-rw-r--r--games/minimal/mods/bucket/depends.txt2
-rw-r--r--games/minimal/mods/bucket/mod.conf3
-rw-r--r--games/minimal/mods/default/mod.conf2
-rw-r--r--games/minimal/mods/experimental/depends.txt2
-rw-r--r--games/minimal/mods/experimental/mod.conf3
-rw-r--r--games/minimal/mods/give_initial_stuff/depends.txt2
-rw-r--r--games/minimal/mods/give_initial_stuff/mod.conf3
-rw-r--r--games/minimal/mods/legacy/depends.txt2
-rw-r--r--games/minimal/mods/legacy/mod.conf3
-rw-r--r--games/minimal/mods/stairs/depends.txt1
-rw-r--r--games/minimal/mods/stairs/mod.conf3
-rw-r--r--games/minimal/mods/test/mod.conf2
-rw-r--r--src/mods.cpp74
-rw-r--r--src/mods.h3
-rw-r--r--src/script/lua_api/l_mainmenu.cpp148
-rw-r--r--src/script/lua_api/l_mainmenu.h8
20 files changed, 237 insertions, 140 deletions
diff --git a/builtin/mainmenu/modmgr.lua b/builtin/mainmenu/modmgr.lua
index dee048982..185bcd639 100644
--- a/builtin/mainmenu/modmgr.lua
+++ b/builtin/mainmenu/modmgr.lua
@@ -271,34 +271,13 @@ function modmgr.render_modlist(render_list)
end
--------------------------------------------------------------------------------
-function modmgr.get_dependencies(modfolder)
- local toadd_hard = ""
- local toadd_soft = ""
- if modfolder ~= nil then
- local filename = modfolder ..
- DIR_DELIM .. "depends.txt"
-
- local hard_dependencies = {}
- local soft_dependencies = {}
- local dependencyfile = io.open(filename,"r")
- if dependencyfile then
- local dependency = dependencyfile:read("*l")
- while dependency do
- dependency = dependency:gsub("\r", "")
- if string.sub(dependency, -1, -1) == "?" then
- table.insert(soft_dependencies, string.sub(dependency, 1, -2))
- else
- table.insert(hard_dependencies, dependency)
- end
- dependency = dependencyfile:read()
- end
- dependencyfile:close()
- end
- toadd_hard = table.concat(hard_dependencies, ",")
- toadd_soft = table.concat(soft_dependencies, ",")
+function modmgr.get_dependencies(path)
+ if path == nil then
+ return "", ""
end
- return toadd_hard, toadd_soft
+ local info = core.get_mod_info(path)
+ return table.concat(info.depends, ","), table.concat(info.optional_depends, ",")
end
--------------------------------------------------------------------------------
diff --git a/builtin/mainmenu/tab_mods.lua b/builtin/mainmenu/tab_mods.lua
index 60e21ee27..7685bfcc4 100644
--- a/builtin/mainmenu/tab_mods.lua
+++ b/builtin/mainmenu/tab_mods.lua
@@ -40,12 +40,11 @@ local function get_formspec(tabview, name, tabdata)
end
if selected_mod ~= nil then
- local modscreenshot = nil
-
--check for screenshot beeing available
local screenshotfilename = selected_mod.path .. DIR_DELIM .. "screenshot.png"
- local error = nil
- local screenshotfile,error = io.open(screenshotfilename,"r")
+ local screenshotfile, error = io.open(screenshotfilename,"r")
+
+ local modscreenshot
if error == nil then
screenshotfile:close()
modscreenshot = screenshotfilename
@@ -55,33 +54,20 @@ local function get_formspec(tabview, name, tabdata)
modscreenshot = defaulttexturedir .. "no_screenshot.png"
end
- retval = retval
- .. "image[5.5,0;3,2;" .. core.formspec_escape(modscreenshot) .. "]"
- .. "label[8.25,0.6;" .. selected_mod.name .. "]"
-
- local descriptionlines = nil
- error = nil
- local descriptionfilename = selected_mod.path .. "description.txt"
- local descriptionfile,error = io.open(descriptionfilename,"r")
- if error == nil then
- local descriptiontext = descriptionfile:read("*all")
-
- descriptionlines = core.wrap_text(descriptiontext, 42, true)
- descriptionfile:close()
- else
- descriptionlines = {}
- descriptionlines[#descriptionlines + 1] = fgettext("No mod description available")
- end
-
retval = retval ..
- "label[5.5,1.7;".. fgettext("Mod Information:") .. "]" ..
- "textlist[5.5,2.2;6.2,2.4;description;"
+ "image[5.5,0;3,2;" .. core.formspec_escape(modscreenshot) .. "]" ..
+ "label[8.25,0.6;" .. selected_mod.name .. "]" ..
+ "label[5.5,1.7;".. fgettext("Mod Information:") .. "]" ..
+ "textlist[5.5,2.2;6.2,2.4;description;"
- for i=1,#descriptionlines,1 do
+
+ local info = core.get_mod_info(selected_mod.path)
+ local desc = info.description or fgettext("No mod description available")
+ local descriptionlines = core.wrap_text(desc, 42, true)
+ for i = 1, #descriptionlines do
retval = retval .. core.formspec_escape(descriptionlines[i]) .. ","
end
-
if selected_mod.is_modpack then
retval = retval .. ";0]" ..
"button[9.9,4.65;2,1;btn_mod_mgr_rename_modpack;" ..
@@ -90,7 +76,8 @@ local function get_formspec(tabview, name, tabdata)
.. fgettext("Uninstall Selected Modpack") .. "]"
else
--show dependencies
- local toadd_hard, toadd_soft = modmgr.get_dependencies(selected_mod.path)
+ local toadd_hard = table.concat(info.depends, ",")
+ local toadd_soft = table.concat(info.optional_depends, ",")
if toadd_hard == "" and toadd_soft == "" then
retval = retval .. "," .. fgettext("No dependencies.")
else
diff --git a/doc/lua_api.txt b/doc/lua_api.txt
index 413990f61..15036848b 100644
--- a/doc/lua_api.txt
+++ b/doc/lua_api.txt
@@ -130,9 +130,8 @@ Mod directory structure
mods
|-- modname
- | |-- depends.txt
+ | |-- mod.conf
| |-- screenshot.png
- | |-- description.txt
| |-- settingtypes.txt
| |-- init.lua
| |-- models
@@ -145,12 +144,32 @@ Mod directory structure
| `-- <custom data>
`-- another
-
### modname
The location of this directory can be fetched by using
`minetest.get_modpath(modname)`.
+### mod.conf
+A key-value store of mod details.
+
+* `name` - the mod name. Allows Minetest to determine the mod name even if the
+ folder is wrongly named.
+* `description` - Description of mod to be shown in the Mods tab of the mainmenu.
+* `depends` - A comma separated list of dependencies. These are mods that must
+ be loaded before this mod.
+* `optional_depends` - A comma separated list of optional dependencies.
+ Like a dependency, but no error if the mod doesn't exist.
+
+Note: to support 0.4.x, please also provide depends.txt.
+
+### `screenshot.png`
+A screenshot shown in the mod manager within the main menu. It should
+have an aspect ratio of 3:2 and a minimum size of 300×200 pixels.
+
### `depends.txt`
+**Deprecated:** you should use mod.conf instead.
+
+This file is used if there are no dependencies in mod.conf.
+
List of mods that have to be loaded before loading this mod.
A single line contains a single modname.
@@ -159,11 +178,11 @@ Optional dependencies can be defined by appending a question mark
to a single modname. This means that if the specified mod
is missing, it does not prevent this mod from being loaded.
-### `screenshot.png`
-A screenshot shown in the mod manager within the main menu. It should
-have an aspect ratio of 3:2 and a minimum size of 300×200 pixels.
-
### `description.txt`
+**Deprecated:** you should use mod.conf instead.
+
+This file is used if there is no description in mod.conf.
+
A file containing a description to be shown in the Mods tab of the mainmenu.
### `settingtypes.txt`
diff --git a/doc/menu_lua_api.txt b/doc/menu_lua_api.txt
index 1d158f304..49c6fbef0 100644
--- a/doc/menu_lua_api.txt
+++ b/doc/menu_lua_api.txt
@@ -111,7 +111,7 @@ core.get_screen_info()
window_height = <current window height>
}
-Games:
+Packages:
core.get_game(index)
^ returns {
id = <id>,
@@ -125,6 +125,15 @@ core.get_game(index)
core.get_games() -> table of all games in upper format (possible in async calls)
core.get_mapgen_names([include_hidden=false]) -> table of map generator algorithms
registered in the core (possible in async calls)
+core.get_mod_info(path)
+^ returns {
+ name = "name of mod",
+ type = "mod" or "modpack",
+ description = "description",
+ path = "path/to/mod",
+ depends = {"mod", "names"},
+ optional_depends = {"mod", "names"},
+}
Favorites:
core.get_favorites(location) -> list of favorites (possible in async calls)
diff --git a/games/minimal/mods/bucket/depends.txt b/games/minimal/mods/bucket/depends.txt
deleted file mode 100644
index 3a7daa1d7..000000000
--- a/games/minimal/mods/bucket/depends.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-default
-
diff --git a/games/minimal/mods/bucket/mod.conf b/games/minimal/mods/bucket/mod.conf
new file mode 100644
index 000000000..2a6086775
--- /dev/null
+++ b/games/minimal/mods/bucket/mod.conf
@@ -0,0 +1,3 @@
+name = bucket
+description = Minimal bucket to place and pick up liquids
+depends = default
diff --git a/games/minimal/mods/default/mod.conf b/games/minimal/mods/default/mod.conf
new file mode 100644
index 000000000..f6f7ea798
--- /dev/null
+++ b/games/minimal/mods/default/mod.conf
@@ -0,0 +1,2 @@
+name = default
+description = Minimal default, adds basic nodes
diff --git a/games/minimal/mods/experimental/depends.txt b/games/minimal/mods/experimental/depends.txt
deleted file mode 100644
index 3296b1e0a..000000000
--- a/games/minimal/mods/experimental/depends.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-default
-stairs
diff --git a/games/minimal/mods/experimental/mod.conf b/games/minimal/mods/experimental/mod.conf
new file mode 100644
index 000000000..018e761b3
--- /dev/null
+++ b/games/minimal/mods/experimental/mod.conf
@@ -0,0 +1,3 @@
+name = experimental
+description = Minimal mod to test features
+depends = default, stairs
diff --git a/games/minimal/mods/give_initial_stuff/depends.txt b/games/minimal/mods/give_initial_stuff/depends.txt
deleted file mode 100644
index 3a7daa1d7..000000000
--- a/games/minimal/mods/give_initial_stuff/depends.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-default
-
diff --git a/games/minimal/mods/give_initial_stuff/mod.conf b/games/minimal/mods/give_initial_stuff/mod.conf
new file mode 100644
index 000000000..bd293f418
--- /dev/null
+++ b/games/minimal/mods/give_initial_stuff/mod.conf
@@ -0,0 +1,3 @@
+name = give_initial_stuff
+description = Gives items to players on join
+depends = default
diff --git a/games/minimal/mods/legacy/depends.txt b/games/minimal/mods/legacy/depends.txt
deleted file mode 100644
index 3a7daa1d7..000000000
--- a/games/minimal/mods/legacy/depends.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-default
-
diff --git a/games/minimal/mods/legacy/mod.conf b/games/minimal/mods/legacy/mod.conf
new file mode 100644
index 000000000..12102e1ee
--- /dev/null
+++ b/games/minimal/mods/legacy/mod.conf
@@ -0,0 +1,3 @@
+name = legacy
+description = Aliases allowing support for 0.3.x worlds
+depends = default
diff --git a/games/minimal/mods/stairs/depends.txt b/games/minimal/mods/stairs/depends.txt
deleted file mode 100644
index 4ad96d515..000000000
--- a/games/minimal/mods/stairs/depends.txt
+++ /dev/null
@@ -1 +0,0 @@
-default
diff --git a/games/minimal/mods/stairs/mod.conf b/games/minimal/mods/stairs/mod.conf
new file mode 100644
index 000000000..32bda004a
--- /dev/null
+++ b/games/minimal/mods/stairs/mod.conf
@@ -0,0 +1,3 @@
+name = stairs
+description = Adds stairs and slabs
+depends = default
diff --git a/games/minimal/mods/test/mod.conf b/games/minimal/mods/test/mod.conf
new file mode 100644
index 000000000..0c9722fc3
--- /dev/null
+++ b/games/minimal/mods/test/mod.conf
@@ -0,0 +1,2 @@
+name = test
+description = Adds unit tests for the engine
diff --git a/src/mods.cpp b/src/mods.cpp
index 2cfc02b66..6fa578f2f 100644
--- a/src/mods.cpp
+++ b/src/mods.cpp
@@ -20,6 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include <cctype>
#include <fstream>
#include <json/json.h>
+#include <algorithm>
#include "mods.h"
#include "filesys.h"
#include "log.h"
@@ -28,14 +29,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "porting.h"
#include "convert_json.h"
-static bool parseDependsLine(std::istream &is,
- std::string &dep, std::set<char> &symbols)
+bool parseDependsString(std::string &dep,
+ std::unordered_set<char> &symbols)
{
- std::getline(is, dep);
dep = trim(dep);
symbols.clear();
size_t pos = dep.size();
- while(pos > 0 && !string_allowed(dep.substr(pos-1, 1), MODNAME_ALLOWED_CHARS)){
+ while (pos > 0 && !string_allowed(dep.substr(pos-1, 1), MODNAME_ALLOWED_CHARS)) {
// last character is a symbol, not part of the modname
symbols.insert(dep[pos-1]);
--pos;
@@ -60,28 +60,66 @@ void parseModContents(ModSpec &spec)
// Handle modpacks (defined by containing modpack.txt)
std::ifstream modpack_is((spec.path+DIR_DELIM+"modpack.txt").c_str());
- if(modpack_is.good()){ //a modpack, recursively get the mods in it
+ if (modpack_is.good()) { // a modpack, recursively get the mods in it
modpack_is.close(); // We don't actually need the file
spec.is_modpack = true;
spec.modpack_content = getModsInPath(spec.path, true);
-
// modpacks have no dependencies; they are defined and
// tracked separately for each mod in the modpack
- }
- else{ // not a modpack, parse the dependencies
- std::ifstream is((spec.path+DIR_DELIM+"depends.txt").c_str());
- while(is.good()){
- std::string dep;
- std::set<char> symbols;
- if(parseDependsLine(is, dep, symbols)){
- if(symbols.count('?') != 0){
- spec.optdepends.insert(dep);
- }
- else{
- spec.depends.insert(dep);
+
+ } else {
+ // Attempt to load dependencies from mod.conf
+ bool mod_conf_has_depends = false;
+ if (info.exists("depends")) {
+ mod_conf_has_depends = true;
+ std::string dep = info.get("depends");
+ dep.erase(std::remove_if(dep.begin(), dep.end(),
+ static_cast<int(*)(int)>(&std::isspace)), dep.end());
+ for (const auto &dependency : str_split(dep, ',')) {
+ spec.depends.insert(dependency);
+ }
+ }
+
+ if (info.exists("optional_depends")) {
+ mod_conf_has_depends = true;
+ std::string dep = info.get("optional_depends");
+ dep.erase(std::remove_if(dep.begin(), dep.end(),
+ static_cast<int(*)(int)>(&std::isspace)), dep.end());
+ for (const auto &dependency : str_split(dep, ',')) {
+ spec.optdepends.insert(dependency);
+ }
+ }
+
+ // Fallback to depends.txt
+ if (!mod_conf_has_depends) {
+ std::vector<std::string> dependencies;
+
+ std::ifstream is((spec.path + DIR_DELIM + "depends.txt").c_str());
+ while (is.good()) {
+ std::string dep;
+ std::getline(is, dep);
+ dependencies.push_back(dep);
+ }
+
+ for (auto &dependency : dependencies) {
+ std::unordered_set<char> symbols;
+ if (parseDependsString(dependency, symbols)) {
+ if (symbols.count('?') != 0) {
+ spec.optdepends.insert(dependency);
+ } else {
+ spec.depends.insert(dependency);
+ }
}
}
}
+
+ if (info.exists("description")) {
+ spec.desc = info.get("description");
+ } else {
+ std::ifstream is((spec.path + DIR_DELIM + "description.txt").c_str());
+ spec.desc = std::string((std::istreambuf_iterator<char>(is)),
+ std::istreambuf_iterator<char>());
+ }
}
}
diff --git a/src/mods.h b/src/mods.h
index 037d6bd1c..3063edaa2 100644
--- a/src/mods.h
+++ b/src/mods.h
@@ -37,6 +37,8 @@ struct ModSpec
{
std::string name;
std::string path;
+ std::string desc;
+
//if normal mod:
std::unordered_set<std::string> depends;
std::unordered_set<std::string> optdepends;
@@ -44,6 +46,7 @@ struct ModSpec
bool part_of_modpack = false;
bool is_modpack = false;
+
// if modpack:
std::map<std::string,ModSpec> modpack_content;
ModSpec(const std::string &name_ = "", const std::string &path_ = ""):
diff --git a/src/script/lua_api/l_mainmenu.cpp b/src/script/lua_api/l_mainmenu.cpp
index 95696bc20..027b7b0f8 100644
--- a/src/script/lua_api/l_mainmenu.cpp
+++ b/src/script/lua_api/l_mainmenu.cpp
@@ -258,56 +258,6 @@ int ModApiMainMenu::l_get_worlds(lua_State *L)
}
/******************************************************************************/
-int ModApiMainMenu::l_get_games(lua_State *L)
-{
- std::vector<SubgameSpec> games = getAvailableGames();
-
- lua_newtable(L);
- int top = lua_gettop(L);
- unsigned int index = 1;
-
- for (const SubgameSpec &game : games) {
- lua_pushnumber(L,index);
- lua_newtable(L);
- int top_lvl2 = lua_gettop(L);
-
- lua_pushstring(L,"id");
- lua_pushstring(L, game.id.c_str());
- lua_settable(L, top_lvl2);
-
- lua_pushstring(L,"path");
- lua_pushstring(L, game.path.c_str());
- lua_settable(L, top_lvl2);
-
- lua_pushstring(L,"gamemods_path");
- lua_pushstring(L, game.gamemods_path.c_str());
- lua_settable(L, top_lvl2);
-
- lua_pushstring(L,"name");
- lua_pushstring(L, game.name.c_str());
- lua_settable(L, top_lvl2);
-
- lua_pushstring(L,"menuicon_path");
- lua_pushstring(L, game.menuicon_path.c_str());
- lua_settable(L, top_lvl2);
-
- lua_pushstring(L,"addon_mods_paths");
- lua_newtable(L);
- int table2 = lua_gettop(L);
- int internal_index=1;
- for (const std::string &addon_mods_path : game.addon_mods_paths) {
- lua_pushnumber(L,internal_index);
- lua_pushstring(L, addon_mods_path.c_str());
- lua_settable(L, table2);
- internal_index++;
- }
- lua_settable(L, top_lvl2);
- lua_settable(L, top);
- index++;
- }
- return 1;
-}
-/******************************************************************************/
int ModApiMainMenu::l_get_favorites(lua_State *L)
{
std::string listtype = "local";
@@ -478,6 +428,103 @@ int ModApiMainMenu::l_delete_favorite(lua_State *L)
}
/******************************************************************************/
+int ModApiMainMenu::l_get_games(lua_State *L)
+{
+ std::vector<SubgameSpec> games = getAvailableGames();
+
+ lua_newtable(L);
+ int top = lua_gettop(L);
+ unsigned int index = 1;
+
+ for (const SubgameSpec &game : games) {
+ lua_pushnumber(L, index);
+ lua_newtable(L);
+ int top_lvl2 = lua_gettop(L);
+
+ lua_pushstring(L, "id");
+ lua_pushstring(L, game.id.c_str());
+ lua_settable(L, top_lvl2);
+
+ lua_pushstring(L, "path");
+ lua_pushstring(L, game.path.c_str());
+ lua_settable(L, top_lvl2);
+
+ lua_pushstring(L, "gamemods_path");
+ lua_pushstring(L, game.gamemods_path.c_str());
+ lua_settable(L, top_lvl2);
+
+ lua_pushstring(L, "name");
+ lua_pushstring(L, game.name.c_str());
+ lua_settable(L, top_lvl2);
+
+ lua_pushstring(L, "menuicon_path");
+ lua_pushstring(L, game.menuicon_path.c_str());
+ lua_settable(L, top_lvl2);
+
+ lua_pushstring(L, "addon_mods_paths");
+ lua_newtable(L);
+ int table2 = lua_gettop(L);
+ int internal_index = 1;
+ for (const std::string &addon_mods_path : game.addon_mods_paths) {
+ lua_pushnumber(L, internal_index);
+ lua_pushstring(L, addon_mods_path.c_str());
+ lua_settable(L, table2);
+ internal_index++;
+ }
+ lua_settable(L, top_lvl2);
+ lua_settable(L, top);
+ index++;
+ }
+ return 1;
+}
+
+/******************************************************************************/
+int ModApiMainMenu::l_get_mod_info(lua_State *L)
+{
+ std::string path = luaL_checkstring(L, 1);
+
+ ModSpec spec;
+ spec.path = path;
+ parseModContents(spec);
+
+ lua_newtable(L);
+
+ lua_pushstring(L, spec.name.c_str());
+ lua_setfield(L, -2, "name");
+
+ lua_pushstring(L, spec.is_modpack ? "modpack" : "mod");
+ lua_setfield(L, -2, "type");
+
+ lua_pushstring(L, spec.desc.c_str());
+ lua_setfield(L, -2, "description");
+
+ lua_pushstring(L, spec.path.c_str());
+ lua_setfield(L, -2, "path");
+
+ // Dependencies
+ lua_newtable(L);
+ int i = 1;
+ for (const auto &dep : spec.depends) {
+ lua_pushstring(L, dep.c_str());
+ lua_rawseti(L, -2, i);
+ i++;
+ }
+ lua_setfield(L, -2, "depends");
+
+ // Optional Dependencies
+ lua_newtable(L);
+ i = 1;
+ for (const auto &dep : spec.optdepends) {
+ lua_pushstring(L, dep.c_str());
+ lua_rawseti(L, -2, i);
+ i++;
+ }
+ lua_setfield(L, -2, "optional_depends");
+
+ return 1;
+}
+
+/******************************************************************************/
int ModApiMainMenu::l_show_keys_menu(lua_State *L)
{
GUIEngine* engine = getGuiEngine(L);
@@ -968,6 +1015,7 @@ void ModApiMainMenu::Initialize(lua_State *L, int top)
API_FCT(get_table_index);
API_FCT(get_worlds);
API_FCT(get_games);
+ API_FCT(get_mod_info);
API_FCT(start);
API_FCT(close);
API_FCT(get_favorites);
diff --git a/src/script/lua_api/l_mainmenu.h b/src/script/lua_api/l_mainmenu.h
index ffaab7dca..2faeaf63e 100644
--- a/src/script/lua_api/l_mainmenu.h
+++ b/src/script/lua_api/l_mainmenu.h
@@ -71,8 +71,6 @@ private:
static int l_get_worlds(lua_State *L);
- static int l_get_games(lua_State *L);
-
static int l_get_mapgen_names(lua_State *L);
static int l_get_favorites(lua_State *L);
@@ -81,6 +79,12 @@ private:
static int l_gettext(lua_State *L);
+ //packages
+
+ static int l_get_games(lua_State *L);
+
+ static int l_get_mod_info(lua_State *L);
+
//gui
static int l_show_keys_menu(lua_State *L);