Add HTTP API to main menu (#9998)

This commit is contained in:
rubenwardy 2020-06-06 17:17:08 +01:00 committed by GitHub
parent 7ec0e3df35
commit 60bab8b2d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 289 additions and 112 deletions

@ -67,3 +67,22 @@ function dialog_create(name,get_formspec,buttonhandler,eventhandler)
ui.add(self) ui.add(self)
return self return self
end end
function messagebox(name, message)
return dialog_create(name,
function()
return ([[
formspec_version[3]
size[8,3]
textarea[0.375,0.375;7.25,1.2;;;%s]
button[3,1.825;2,0.8;ok;%s]
]]):format(message, fgettext("OK"))
end,
function(this, fields)
if fields.ok then
this:delete()
return true
end
end,
nil)
end

@ -85,7 +85,7 @@ function ui.update()
"box[0.5,1.2;13,5;#000]", "box[0.5,1.2;13,5;#000]",
("textarea[0.5,1.2;13,5;;%s;%s]"):format( ("textarea[0.5,1.2;13,5;;%s;%s]"):format(
error_title, error_message), error_title, error_message),
"button[5,6.6;4,1;btn_error_confirm;" .. fgettext("Ok") .. "]" "button[5,6.6;4,1;btn_error_confirm;" .. fgettext("OK") .. "]"
} }
else else
local active_toplevel_ui_elements = 0 local active_toplevel_ui_elements = 0

@ -36,6 +36,7 @@ dofile(commonpath .. "misc_helpers.lua")
if INIT == "game" then if INIT == "game" then
dofile(gamepath .. "init.lua") dofile(gamepath .. "init.lua")
assert(not core.get_http_api)
elseif INIT == "mainmenu" then elseif INIT == "mainmenu" then
local mm_script = core.settings:get("main_menu_script") local mm_script = core.settings:get("main_menu_script")
if mm_script and mm_script ~= "" then if mm_script and mm_script ~= "" then

@ -1,5 +1,5 @@
--Minetest --Minetest
--Copyright (C) 2018 rubenwardy --Copyright (C) 2018-20 rubenwardy
-- --
--This program is free software; you can redistribute it and/or modify --This program is free software; you can redistribute it and/or modify
--it under the terms of the GNU Lesser General Public License as published by --it under the terms of the GNU Lesser General Public License as published by
@ -15,8 +15,18 @@
--with this program; if not, write to the Free Software Foundation, Inc., --with this program; if not, write to the Free Software Foundation, Inc.,
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. --51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
if not minetest.get_http_api then
function create_store_dlg()
return messagebox("store",
fgettext("ContentDB is not available when Minetest was compiled without cURL"))
end
return
end
local store = { packages = {}, packages_full = {} } local store = { packages = {}, packages_full = {} }
local http = minetest.get_http_api()
-- Screenshot -- Screenshot
local screenshot_dir = core.get_cache_path() .. DIR_DELIM .. "cdb" local screenshot_dir = core.get_cache_path() .. DIR_DELIM .. "cdb"
assert(core.create_dir(screenshot_dir)) assert(core.create_dir(screenshot_dir))
@ -171,11 +181,6 @@ local function get_screenshot(package)
end end
function store.load() function store.load()
local tmpdir = os.tempfolder()
local target = tmpdir .. DIR_DELIM .. "packages.json"
assert(core.create_dir(tmpdir))
local version = core.get_version() local version = core.get_version()
local base_url = core.settings:get("contentdb_url") local base_url = core.settings:get("contentdb_url")
local url = base_url .. local url = base_url ..
@ -189,31 +194,29 @@ function store.load()
end end
end end
core.download_file(url, target) local timeout = tonumber(minetest.settings:get("curl_file_download_timeout"))
local response = http.fetch_sync({ url = url, timeout = timeout })
if not response.succeeded then
return
end
local file = io.open(target, "r") store.packages_full = core.parse_json(response.data) or {}
if file then
store.packages_full = core.parse_json(file:read("*all")) or {}
file:close()
for _, package in pairs(store.packages_full) do for _, package in pairs(store.packages_full) do
package.url = base_url .. "/packages/" .. package.url = base_url .. "/packages/" ..
package.author .. "/" .. package.name .. package.author .. "/" .. package.name ..
"/releases/" .. package.release .. "/download/" "/releases/" .. package.release .. "/download/"
local name_len = #package.name local name_len = #package.name
if package.type == "game" and name_len > 5 and package.name:sub(name_len - 4) == "_game" then if package.type == "game" and name_len > 5 and package.name:sub(name_len - 4) == "_game" then
package.id = package.author:lower() .. "/" .. package.name:sub(1, name_len - 5) package.id = package.author:lower() .. "/" .. package.name:sub(1, name_len - 5)
else else
package.id = package.author:lower() .. "/" .. package.name package.id = package.author:lower() .. "/" .. package.name
end
end end
store.packages = store.packages_full
store.loaded = true
end end
core.delete_dir(tmpdir) store.packages = store.packages_full
store.loaded = true
end end
function store.update_paths() function store.update_paths()

@ -101,6 +101,9 @@ dialog_create(name, cbf_formspec, cbf_button_handler, cbf_events)
^ cbf_events: function to handle events ^ cbf_events: function to handle events
function(dialog, event) function(dialog, event)
messagebox(name, message)
^ creates a message dialog
Class reference dialog: Class reference dialog:
methods: methods:

@ -3,34 +3,53 @@ Minetest Lua Mainmenu API Reference 5.3.0
Introduction Introduction
------------- -------------
The main menu is defined as a formspec by Lua in builtin/mainmenu/ The main menu is defined as a formspec by Lua in builtin/mainmenu/
Description of formspec language to show your menu is in lua_api.txt Description of formspec language to show your menu is in lua_api.txt
Callbacks Callbacks
--------- ---------
core.buttonhandler(fields): called when a button is pressed. core.buttonhandler(fields): called when a button is pressed.
^ fields = {name1 = value1, name2 = value2, ...} ^ fields = {name1 = value1, name2 = value2, ...}
core.event_handler(event) core.event_handler(event)
^ event: "MenuQuit", "KeyEnter", "ExitButton" or "EditBoxEnter" ^ event: "MenuQuit", "KeyEnter", "ExitButton" or "EditBoxEnter"
Gamedata Gamedata
-------- --------
The "gamedata" table is read when calling core.start(). It should contain: The "gamedata" table is read when calling core.start(). It should contain:
{ {
playername = <name>, playername = <name>,
password = <password>, password = <password>,
address = <IP/adress>, address = <IP/adress>,
port = <port>, port = <port>,
selected_world = <index>, -- 0 for client mode selected_world = <index>, -- 0 for client mode
singleplayer = <true/false>, singleplayer = <true/false>,
} }
Functions Functions
--------- ---------
core.start() core.start()
core.close() core.close()
core.get_min_supp_proto()
^ returns the minimum supported network protocol version
core.get_max_supp_proto()
^ returns the maximum supported network protocol version
core.open_url(url)
^ opens the URL in a web browser, returns false on failure.
^ Must begin with http:// or https://
core.get_version() (possible in async calls)
^ returns current core version
Filesystem
----------
Filesystem:
core.get_builtin_path() core.get_builtin_path()
^ returns path to builtin root ^ returns path to builtin root
core.create_dir(absolute_path) (possible in async calls) core.create_dir(absolute_path) (possible in async calls)
@ -48,12 +67,6 @@ core.extract_zip(zipfile,destination) [unzip within path required]
^ zipfile to extract ^ zipfile to extract
^ destination folder to extract to ^ destination folder to extract to
^ returns true/false ^ returns true/false
core.download_file(url,target) (possible in async calls)
^ url to download
^ target to store to
^ returns true/false
core.get_version() (possible in async calls)
^ returns current core version
core.sound_play(spec, looped) -> handle core.sound_play(spec, looped) -> handle
^ spec = SimpleSoundSpec (see lua-api.txt) ^ spec = SimpleSoundSpec (see lua-api.txt)
^ looped = bool ^ looped = bool
@ -67,7 +80,82 @@ core.get_mapgen_names([include_hidden=false]) -> table of map generator algorith
registered in the core (possible in async calls) registered in the core (possible in async calls)
core.get_cache_path() -> path of cache core.get_cache_path() -> path of cache
Formspec:
HTTP Requests
-------------
* core.download_file(url, target) (possible in async calls)
* url to download, and target to store to
* returns true/false
* `minetest.get_http_api()` (possible in async calls)
* returns `HTTPApiTable` containing http functions.
* The returned table contains the functions `fetch_sync`, `fetch_async` and
`fetch_async_get` described below.
* Function only exists if minetest server was built with cURL support.
* `HTTPApiTable.fetch_sync(HTTPRequest req)`: returns HTTPRequestResult
* Performs given request synchronously
* `HTTPApiTable.fetch_async(HTTPRequest req)`: returns handle
* Performs given request asynchronously and returns handle for
`HTTPApiTable.fetch_async_get`
* `HTTPApiTable.fetch_async_get(handle)`: returns HTTPRequestResult
* Return response data for given asynchronous HTTP request
### `HTTPRequest` definition
Used by `HTTPApiTable.fetch` and `HTTPApiTable.fetch_async`.
{
url = "http://example.org",
timeout = 10,
-- Timeout for connection in seconds. Default is 3 seconds.
post_data = "Raw POST request data string" OR {field1 = "data1", field2 = "data2"},
-- Optional, if specified a POST request with post_data is performed.
-- Accepts both a string and a table. If a table is specified, encodes
-- table as x-www-form-urlencoded key-value pairs.
-- If post_data is not specified, a GET request is performed instead.
user_agent = "ExampleUserAgent",
-- Optional, if specified replaces the default minetest user agent with
-- given string
extra_headers = { "Accept-Language: en-us", "Accept-Charset: utf-8" },
-- Optional, if specified adds additional headers to the HTTP request.
-- You must make sure that the header strings follow HTTP specification
-- ("Key: Value").
multipart = boolean
-- Optional, if true performs a multipart HTTP request.
-- Default is false.
}
### `HTTPRequestResult` definition
Passed to `HTTPApiTable.fetch` callback. Returned by
`HTTPApiTable.fetch_async_get`.
{
completed = true,
-- If true, the request has finished (either succeeded, failed or timed
-- out)
succeeded = true,
-- If true, the request was successful
timeout = false,
-- If true, the request timed out
code = 200,
-- HTTP status code
data = "response"
}
Formspec
--------
core.update_formspec(formspec) core.update_formspec(formspec)
core.get_table_index(tablename) -> index core.get_table_index(tablename) -> index
^ can also handle textlists ^ can also handle textlists
@ -82,7 +170,10 @@ core.explode_textlist_event(string) -> table
core.set_formspec_prepend(formspec) core.set_formspec_prepend(formspec)
^ string to be added to every mainmenu formspec, to be used for theming. ^ string to be added to every mainmenu formspec, to be used for theming.
GUI:
GUI
---
core.set_background(type, texturepath,[tile],[minsize]) core.set_background(type, texturepath,[tile],[minsize])
^ type: "background", "overlay", "header" or "footer" ^ type: "background", "overlay", "header" or "footer"
^ tile: tile the image instead of scaling (background only) ^ tile: tile the image instead of scaling (background only)
@ -102,86 +193,96 @@ core.show_path_select_dialog(formname, caption, is_file_select)
^ returns nil or selected file/folder ^ returns nil or selected file/folder
core.get_screen_info() core.get_screen_info()
^ returns { ^ returns {
density = <screen density 0.75,1.0,2.0,3.0 ... (dpi)>, density = <screen density 0.75,1.0,2.0,3.0 ... (dpi)>,
display_width = <width of display>, display_width = <width of display>,
display_height = <height of display>, display_height = <height of display>,
window_width = <current window width>, window_width = <current window width>,
window_height = <current window height> window_height = <current window height>
} }
### Content and Packages
Content and Packages
--------------------
Content - an installed mod, modpack, game, or texture pack (txt) Content - an installed mod, modpack, game, or texture pack (txt)
Package - content which is downloadable from the content db, may or may not be installed. Package - content which is downloadable from the content db, may or may not be installed.
* core.get_modpath() (possible in async calls) * core.get_modpath() (possible in async calls)
* returns path to global modpath * returns path to global modpath
* core.get_clientmodpath() (possible in async calls) * core.get_clientmodpath() (possible in async calls)
* returns path to global client-side modpath * returns path to global client-side modpath
* core.get_gamepath() (possible in async calls) * core.get_gamepath() (possible in async calls)
* returns path to global gamepath * returns path to global gamepath
* core.get_texturepath() (possible in async calls) * core.get_texturepath() (possible in async calls)
* returns path to default textures * returns path to default textures
* core.get_game(index) * core.get_game(index)
* returns: * returns:
{ {
id = <id>, id = <id>,
path = <full path to game>, path = <full path to game>,
gamemods_path = <path>, gamemods_path = <path>,
name = <name of game>, name = <name of game>,
menuicon_path = <full path to menuicon>, menuicon_path = <full path to menuicon>,
author = "author", author = "author",
DEPRECATED: DEPRECATED:
addon_mods_paths = {[1] = <path>,}, addon_mods_paths = {[1] = <path>,},
} }
* core.get_games() -> table of all games in upper format (possible in async calls) * core.get_games() -> table of all games in upper format (possible in async calls)
* core.get_content_info(path) * core.get_content_info(path)
* returns * returns
{ {
name = "name of content", name = "name of content",
type = "mod" or "modpack" or "game" or "txp", type = "mod" or "modpack" or "game" or "txp",
description = "description", description = "description",
author = "author", author = "author",
path = "path/to/content", path = "path/to/content",
depends = {"mod", "names"}, -- mods only depends = {"mod", "names"}, -- mods only
optional_depends = {"mod", "names"}, -- mods only optional_depends = {"mod", "names"}, -- mods only
} }
Favorites: Favorites
---------
core.get_favorites(location) -> list of favorites (possible in async calls) core.get_favorites(location) -> list of favorites (possible in async calls)
^ location: "local" or "online" ^ location: "local" or "online"
^ returns { ^ returns {
[1] = { [1] = {
clients = <number of clients/nil>, clients = <number of clients/nil>,
clients_max = <maximum number of clients/nil>, clients_max = <maximum number of clients/nil>,
version = <server version/nil>, version = <server version/nil>,
password = <true/nil>, password = <true/nil>,
creative = <true/nil>, creative = <true/nil>,
damage = <true/nil>, damage = <true/nil>,
pvp = <true/nil>, pvp = <true/nil>,
description = <server description/nil>, description = <server description/nil>,
name = <server name/nil>, name = <server name/nil>,
address = <address of server/nil>, address = <address of server/nil>,
port = <port> port = <port>
clients_list = <array of clients/nil> clients_list = <array of clients/nil>
mods = <array of mods/nil> mods = <array of mods/nil>
}, },
... ...
} }
core.delete_favorite(id, location) -> success core.delete_favorite(id, location) -> success
Logging:
Logging
-------
core.debug(line) (possible in async calls) core.debug(line) (possible in async calls)
^ Always printed to stderr and logfile (print() is redirected here) ^ Always printed to stderr and logfile (print() is redirected here)
core.log(line) (possible in async calls) core.log(line) (possible in async calls)
core.log(loglevel, line) (possible in async calls) core.log(loglevel, line) (possible in async calls)
^ loglevel one of "error", "action", "info", "verbose" ^ loglevel one of "error", "action", "info", "verbose"
Settings:
Settings
--------
core.settings:set(name, value) core.settings:set(name, value)
core.settings:get(name) -> string or nil (possible in async calls) core.settings:get(name) -> string or nil (possible in async calls)
core.settings:set_bool(name, value) core.settings:set_bool(name, value)
@ -191,19 +292,25 @@ core.settings:save() -> nil, save all settings to config file
For a complete list of methods of the Settings object see For a complete list of methods of the Settings object see
[lua_api.txt](https://github.com/minetest/minetest/blob/master/doc/lua_api.txt) [lua_api.txt](https://github.com/minetest/minetest/blob/master/doc/lua_api.txt)
Worlds:
Worlds
------
core.get_worlds() -> list of worlds (possible in async calls) core.get_worlds() -> list of worlds (possible in async calls)
^ returns { ^ returns {
[1] = { [1] = {
path = <full path to world>, path = <full path to world>,
name = <name of world>, name = <name of world>,
gameid = <gameid of world>, gameid = <gameid of world>,
}, },
} }
core.create_world(worldname, gameid) core.create_world(worldname, gameid)
core.delete_world(index) core.delete_world(index)
Helpers:
Helpers
-------
core.get_us_time() core.get_us_time()
^ returns time with microsecond precision ^ returns time with microsecond precision
core.gettext(string) -> string core.gettext(string) -> string
@ -228,18 +335,10 @@ minetest.encode_base64(string) (possible in async calls)
minetest.decode_base64(string) (possible in async calls) minetest.decode_base64(string) (possible in async calls)
^ Decodes a string encoded in base64. ^ Decodes a string encoded in base64.
Version compat:
core.get_min_supp_proto()
^ returns the minimum supported network protocol version
core.get_max_supp_proto()
^ returns the maximum supported network protocol version
Other: Async
core.open_url(url) -----
^ opens the URL in a web browser, returns false on failure.
^ Must begin with http:// or https://
Async:
core.handle_async(async_job,parameters,finished) core.handle_async(async_job,parameters,finished)
^ execute a function asynchronously ^ execute a function asynchronously
^ async_job is a function receiving one parameter and returning one parameter ^ async_job is a function receiving one parameter and returning one parameter
@ -250,11 +349,13 @@ core.handle_async(async_job,parameters,finished)
Limitations of Async operations Limitations of Async operations
-No access to global lua variables, don't even try -No access to global lua variables, don't even try
-Limited set of available functions -Limited set of available functions
e.g. No access to functions modifying menu like core.start,core.close, e.g. No access to functions modifying menu like core.start,core.close,
core.show_path_select_dialog core.show_path_select_dialog
Background music Background music
---------------- ----------------
The main menu supports background music. The main menu supports background music.
It looks for a `main_menu` sound in `$USER_PATH/sounds`. The same naming It looks for a `main_menu` sound in `$USER_PATH/sounds`. The same naming
conventions as for normal sounds apply. conventions as for normal sounds apply.

@ -83,6 +83,24 @@ void ModApiHttp::push_http_fetch_result(lua_State *L, HTTPFetchResult &res, bool
setstringfield(L, -1, "data", res.data); setstringfield(L, -1, "data", res.data);
} }
// http_api.fetch_sync(HTTPRequest definition)
int ModApiHttp::l_http_fetch_sync(lua_State *L)
{
NO_MAP_LOCK_REQUIRED;
HTTPFetchRequest req;
read_http_fetch_request(L, req);
infostream << "Mod performs HTTP request with URL " << req.url << std::endl;
HTTPFetchResult res;
httpfetch_sync(req, res);
push_http_fetch_result(L, res, true);
return 1;
}
// http_api.fetch_async(HTTPRequest definition) // http_api.fetch_async(HTTPRequest definition)
int ModApiHttp::l_http_fetch_async(lua_State *L) int ModApiHttp::l_http_fetch_async(lua_State *L)
{ {
@ -180,11 +198,35 @@ int ModApiHttp::l_request_http_api(lua_State *L)
return 1; return 1;
} }
int ModApiHttp::l_get_http_api(lua_State *L)
{
NO_MAP_LOCK_REQUIRED;
lua_newtable(L);
HTTP_API(fetch_async);
HTTP_API(fetch_async_get);
HTTP_API(fetch_sync);
return 1;
}
#endif #endif
void ModApiHttp::Initialize(lua_State *L, int top) void ModApiHttp::Initialize(lua_State *L, int top)
{ {
#if USE_CURL #if USE_CURL
API_FCT(request_http_api);
bool isMainmenu = false;
#ifndef SERVER
isMainmenu = ModApiBase::getGuiEngine(L) != nullptr;
#endif
if (isMainmenu) {
API_FCT(get_http_api);
} else {
API_FCT(request_http_api);
}
#endif #endif
} }

@ -32,6 +32,9 @@ private:
static void read_http_fetch_request(lua_State *L, HTTPFetchRequest &req); static void read_http_fetch_request(lua_State *L, HTTPFetchRequest &req);
static void push_http_fetch_result(lua_State *L, HTTPFetchResult &res, bool completed = true); static void push_http_fetch_result(lua_State *L, HTTPFetchResult &res, bool completed = true);
// http_fetch_sync({url=, timeout=, post_data=})
static int l_http_fetch_sync(lua_State *L);
// http_fetch_async({url=, timeout=, post_data=}) // http_fetch_async({url=, timeout=, post_data=})
static int l_http_fetch_async(lua_State *L); static int l_http_fetch_async(lua_State *L);
@ -40,6 +43,9 @@ private:
// request_http_api() // request_http_api()
static int l_request_http_api(lua_State *L); static int l_request_http_api(lua_State *L);
// get_http_api()
static int l_get_http_api(lua_State *L);
#endif #endif
public: public:

@ -21,6 +21,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "content/mods.h" #include "content/mods.h"
#include "cpp_api/s_internal.h" #include "cpp_api/s_internal.h"
#include "lua_api/l_base.h" #include "lua_api/l_base.h"
#include "lua_api/l_http.h"
#include "lua_api/l_mainmenu.h" #include "lua_api/l_mainmenu.h"
#include "lua_api/l_sound.h" #include "lua_api/l_sound.h"
#include "lua_api/l_util.h" #include "lua_api/l_util.h"
@ -67,6 +68,7 @@ void MainMenuScripting::initializeModApi(lua_State *L, int top)
ModApiMainMenu::Initialize(L, top); ModApiMainMenu::Initialize(L, top);
ModApiUtil::Initialize(L, top); ModApiUtil::Initialize(L, top);
ModApiSound::Initialize(L, top); ModApiSound::Initialize(L, top);
ModApiHttp::Initialize(L, top);
asyncEngine.registerStateInitializer(registerLuaClasses); asyncEngine.registerStateInitializer(registerLuaClasses);
asyncEngine.registerStateInitializer(ModApiMainMenu::InitializeAsync); asyncEngine.registerStateInitializer(ModApiMainMenu::InitializeAsync);