diff --git a/LICENSE.txt b/LICENSE.txt index d1270c4b5..de76c7a80 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -61,6 +61,7 @@ Zughy: textures/base/pack/cdb_downloading.png textures/base/pack/cdb_queued.png textures/base/pack/cdb_update.png + textures/base/pack/cdb_update_cropped.png textures/base/pack/cdb_viewonline.png textures/base/pack/settings_btn.png textures/base/pack/settings_info.png diff --git a/builtin/fstk/tabview.lua b/builtin/fstk/tabview.lua index 4d1a74eb6..f09c4df2d 100644 --- a/builtin/fstk/tabview.lua +++ b/builtin/fstk/tabview.lua @@ -154,13 +154,17 @@ end local function tab_header(self, size) local toadd = "" - for i=1,#self.tablist,1 do - + for i = 1, #self.tablist do if toadd ~= "" then toadd = toadd .. "," end - toadd = toadd .. self.tablist[i].caption + local caption = self.tablist[i].caption + if type(caption) == "function" then + caption = caption(self) + end + + toadd = toadd .. caption end return string.format("tabheader[%f,%f;%f,%f;%s;%s;%i;true;false]", self.header_x, self.header_y, size.width, size.height, self.name, toadd, self.last_tab_index) diff --git a/builtin/mainmenu/dlg_contentstore.lua b/builtin/mainmenu/content/dlg_contentstore.lua similarity index 98% rename from builtin/mainmenu/dlg_contentstore.lua rename to builtin/mainmenu/content/dlg_contentstore.lua index cc7f74219..4f1400206 100644 --- a/builtin/mainmenu/dlg_contentstore.lua +++ b/builtin/mainmenu/content/dlg_contentstore.lua @@ -74,15 +74,6 @@ local REASON_UPDATE = "update" local REASON_DEPENDENCY = "dependency" --- encodes for use as URL parameter or path component -local function urlencode(str) - return str:gsub("[^%a%d()._~-]", function(char) - return ("%%%02X"):format(char:byte()) - end) -end -assert(urlencode("sample text?") == "sample%20text%3F") - - local function get_download_url(package, reason) local base_url = core.settings:get("contentdb_url") local ret = base_url .. ("/packages/%s/releases/%d/download/"):format( @@ -202,6 +193,10 @@ local function start_install(package, reason) end local function queue_download(package, reason) + if package.queued or package.downloading then + return + end + local max_concurrent_downloads = tonumber(core.settings:get("contentdb_max_concurrent_downloads")) if number_downloading < math.max(max_concurrent_downloads, 1) then start_install(package, reason) @@ -222,7 +217,7 @@ local function get_raw_dependencies(package) local url_fmt = "/api/packages/%s/dependencies/?only_hard=1&protocol_version=%s&engine_version=%s" local version = core.get_version() local base_url = core.settings:get("contentdb_url") - local url = base_url .. url_fmt:format(package.url_part, core.get_max_supp_proto(), urlencode(version.string)) + local url = base_url .. url_fmt:format(package.url_part, core.get_max_supp_proto(), core.urlencode(version.string)) local response = http.fetch_sync({ url = url }) if not response.succeeded then @@ -547,6 +542,9 @@ local function install_or_update_package(this, package) error("Unknown package type: " .. package.type) end + if package.queued or package.downloading then + return + end local function on_confirm() local deps = get_raw_dependencies(package) @@ -630,17 +628,17 @@ local function get_screenshot(package) return defaulttexturedir .. "loading_screenshot.png" end -local function fetch_pkgs(param) +local function fetch_pkgs() local version = core.get_version() local base_url = core.settings:get("contentdb_url") local url = base_url .. "/api/packages/?type=mod&type=game&type=txp&protocol_version=" .. - core.get_max_supp_proto() .. "&engine_version=" .. param.urlencode(version.string) + core.get_max_supp_proto() .. "&engine_version=" .. core.urlencode(version.string) for _, item in pairs(core.settings:get("contentdb_flag_blacklist"):split(",")) do item = item:trim() if item ~= "" then - url = url .. "&hide=" .. param.urlencode(item) + url = url .. "&hide=" .. core.urlencode(item) end end @@ -666,7 +664,7 @@ local function fetch_pkgs(param) package.id = package.id .. package.name end - package.url_part = param.urlencode(package.author) .. "/" .. param.urlencode(package.name) + package.url_part = core.urlencode(package.author) .. "/" .. core.urlencode(package.name) if package.aliases then for _, alias in ipairs(package.aliases) do @@ -701,7 +699,8 @@ local function resolve_auto_install_spec() for _, pkg in ipairs(store.packages_full_unordered) do if pkg.author == auto_install_spec.author and - pkg.name == auto_install_spec.name then + (pkg.name == auto_install_spec.name or + (pkg.type == "game" and pkg.name == auto_install_spec.name .. "_game")) then resolved = pkg break end @@ -752,7 +751,7 @@ function store.load() store.loading = true core.handle_async( fetch_pkgs, - { urlencode = urlencode }, + nil, function(result) if result then store.load_ok = true diff --git a/builtin/mainmenu/content/init.lua b/builtin/mainmenu/content/init.lua new file mode 100644 index 000000000..9b562fedc --- /dev/null +++ b/builtin/mainmenu/content/init.lua @@ -0,0 +1,22 @@ +--Minetest +--Copyright (C) 2023 rubenwardy +-- +--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 +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +local path = core.get_mainmenu_path() .. DIR_DELIM .. "content" + +dofile(path .. DIR_DELIM .. "pkgmgr.lua") +dofile(path .. DIR_DELIM .. "update_detector.lua") +dofile(path .. DIR_DELIM .. "dlg_contentstore.lua") diff --git a/builtin/mainmenu/pkgmgr.lua b/builtin/mainmenu/content/pkgmgr.lua similarity index 93% rename from builtin/mainmenu/pkgmgr.lua rename to builtin/mainmenu/content/pkgmgr.lua index 76aa02fa1..687812a5d 100644 --- a/builtin/mainmenu/pkgmgr.lua +++ b/builtin/mainmenu/content/pkgmgr.lua @@ -177,6 +177,7 @@ function pkgmgr.get_mods(path, virtual_path, listing, modpack) end end +-------------------------------------------------------------------------------- function pkgmgr.get_texture_packs() local txtpath = core.get_texturepath() local txtpath_system = core.get_texturepath_share() @@ -195,6 +196,23 @@ function pkgmgr.get_texture_packs() return retval end +-------------------------------------------------------------------------------- +function pkgmgr.get_all() + local result = {} + + for _, mod in pairs(pkgmgr.global_mods:get_list()) do + result[#result + 1] = mod + end + for _, game in pairs(pkgmgr.games) do + result[#result + 1] = game + end + for _, txp in pairs(pkgmgr.get_texture_packs()) do + result[#result + 1] = txp + end + + return result +end + -------------------------------------------------------------------------------- function pkgmgr.get_folder_type(path) local testfile = io.open(path .. DIR_DELIM .. "init.lua","r") @@ -260,7 +278,10 @@ function pkgmgr.is_valid_modname(modpath) end -------------------------------------------------------------------------------- -function pkgmgr.render_packagelist(render_list, use_technical_names, with_error) +--- @param render_list filterlist +--- @param use_technical_names boolean to show technical names instead of human-readable titles +--- @param with_icon table or nil, from virtual path to icon object +function pkgmgr.render_packagelist(render_list, use_technical_names, with_icon) if not render_list then if not pkgmgr.global_mods then pkgmgr.refresh_globals() @@ -273,10 +294,10 @@ function pkgmgr.render_packagelist(render_list, use_technical_names, with_error) for i, v in ipairs(list) do local color = "" local icon = 0 - local error = with_error and with_error[v.virtual_path] - local function update_error(val) - if val and (not error or (error.type == "warning" and val.type == "error")) then - error = val + local icon_info = with_icon and with_icon[v.virtual_path or v.path] + local function update_icon_info(val) + if val and (not icon_info or (icon_info.type == "warning" and val.type == "error")) then + icon_info = val end end @@ -286,8 +307,8 @@ function pkgmgr.render_packagelist(render_list, use_technical_names, with_error) for j = 1, #rawlist do if rawlist[j].modpack == list[i].name then - if with_error then - update_error(with_error[rawlist[j].virtual_path]) + if with_icon then + update_icon_info(with_icon[rawlist[j].virtual_path or rawlist[j].path]) end if rawlist[j].enabled then @@ -303,10 +324,10 @@ function pkgmgr.render_packagelist(render_list, use_technical_names, with_error) color = mt_color_blue local rawlist = render_list:get_raw_list() - if v.type == "game" and with_error then + if v.type == "game" and with_icon then for j = 1, #rawlist do if rawlist[j].is_game_content then - update_error(with_error[rawlist[j].virtual_path]) + update_icon_info(with_icon[rawlist[j].virtual_path or rawlist[j].path]) end end end @@ -315,13 +336,17 @@ function pkgmgr.render_packagelist(render_list, use_technical_names, with_error) color = mt_color_green end - if error then - if error.type == "warning" then + if icon_info then + if icon_info.type == "warning" then color = mt_color_orange icon = 2 - else + elseif icon_info.type == "error" then color = mt_color_red icon = 3 + elseif icon_info.type == "update" then + icon = 4 + else + error("Unknown icon type " .. icon_info.type) end end @@ -332,7 +357,7 @@ function pkgmgr.render_packagelist(render_list, use_technical_names, with_error) retval[#retval + 1] = "0" end - if with_error then + if with_icon then retval[#retval + 1] = icon end diff --git a/builtin/mainmenu/tests/pkgmgr_spec.lua b/builtin/mainmenu/content/tests/pkgmgr_spec.lua similarity index 99% rename from builtin/mainmenu/tests/pkgmgr_spec.lua rename to builtin/mainmenu/content/tests/pkgmgr_spec.lua index bf3ffb77c..37c1bb784 100644 --- a/builtin/mainmenu/tests/pkgmgr_spec.lua +++ b/builtin/mainmenu/content/tests/pkgmgr_spec.lua @@ -57,7 +57,7 @@ local function reset() end setfenv(loadfile("builtin/common/misc_helpers.lua"), env)() - setfenv(loadfile("builtin/mainmenu/pkgmgr.lua"), env)() + setfenv(loadfile("builtin/mainmenu/content/pkgmgr.lua"), env)() function env.pkgmgr.update_gamelist() table.insert(calls, { "update_gamelist" }) diff --git a/builtin/mainmenu/content/update_detector.lua b/builtin/mainmenu/content/update_detector.lua new file mode 100644 index 000000000..532966fd0 --- /dev/null +++ b/builtin/mainmenu/content/update_detector.lua @@ -0,0 +1,144 @@ +--Minetest +--Copyright (C) 2023 rubenwardy +-- +--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 +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +update_detector = {} + + +if not core.get_http_api then + update_detector.get_all = function() return {} end + update_detector.get_count = function() return 0 end + return +end + + +local has_fetched = false +local latest_releases + + +local function fetch_latest_releases() + local version = core.get_version() + local base_url = core.settings:get("contentdb_url") + local url = base_url .. + "/api/updates/?type=mod&type=game&type=txp&protocol_version=" .. + core.get_max_supp_proto() .. "&engine_version=" .. core.urlencode(version.string) + local http = core.get_http_api() + local response = http.fetch_sync({ url = url }) + if not response.succeeded then + return + end + + return core.parse_json(response.data) +end + + +--- Get a table from package key (author/name) to latest release id +--- +--- @param callback function that takes a single argument, table or nil +local function get_latest_releases(callback) + core.handle_async(fetch_latest_releases, nil, callback) +end + + +local function has_packages_from_cdb() + pkgmgr.refresh_globals() + pkgmgr.update_gamelist() + + for _, content in pairs(pkgmgr.get_all()) do + if content.author and content.release > 0 then + return true + end + end + return false +end + + +--- @returns a new table with all keys lowercase +local function lowercase_keys(tab) + local ret = {} + for key, value in pairs(tab) do + ret[key:lower()] = value + end + return ret +end + + +local function fetch() + if has_fetched or not has_packages_from_cdb() then + return + end + + has_fetched = true + + get_latest_releases(function(releases) + if not releases then + has_fetched = false + return + end + + latest_releases = lowercase_keys(releases) + if update_detector.get_count() > 0 then + local maintab = ui.find_by_name("maintab") + if not maintab.hidden then + ui.update() + end + end + end) +end + + +--- @returns a list of content with an update available +function update_detector.get_all() + if latest_releases == nil then + fetch() + return {} + end + + pkgmgr.refresh_globals() + pkgmgr.update_gamelist() + + local ret = {} + local all_content = pkgmgr.get_all() + for _, content in ipairs(all_content) do + if content.author and content.release > 0 then + -- The backend will account for aliases in `latest_releases` + local id = content.author:lower() .. "/" + if content.type == "game" then + id = id .. content.id + else + id = id .. content.name + end + + local latest_release = latest_releases[id] + if not latest_release and content.type == "game" then + latest_release = latest_releases[id .. "_game"] + end + + if latest_release and latest_release > content.release then + ret[#ret + 1] = content + end + end + end + + return ret +end + + +--- @return number of packages with updates available +function update_detector.get_count() + return #update_detector.get_all() +end diff --git a/builtin/mainmenu/init.lua b/builtin/mainmenu/init.lua index dccb1c540..388ab458d 100644 --- a/builtin/mainmenu/init.lua +++ b/builtin/mainmenu/init.lua @@ -37,13 +37,12 @@ dofile(basepath .. "fstk" .. DIR_DELIM .. "tabview.lua") dofile(basepath .. "fstk" .. DIR_DELIM .. "ui.lua") dofile(menupath .. DIR_DELIM .. "async_event.lua") dofile(menupath .. DIR_DELIM .. "common.lua") -dofile(menupath .. DIR_DELIM .. "pkgmgr.lua") dofile(menupath .. DIR_DELIM .. "serverlistmgr.lua") dofile(menupath .. DIR_DELIM .. "game_theme.lua") +dofile(menupath .. DIR_DELIM .. "content" .. DIR_DELIM .. "init.lua") dofile(menupath .. DIR_DELIM .. "dlg_config_world.lua") dofile(menupath .. DIR_DELIM .. "settings" .. DIR_DELIM .. "init.lua") -dofile(menupath .. DIR_DELIM .. "dlg_contentstore.lua") dofile(menupath .. DIR_DELIM .. "dlg_create_world.lua") dofile(menupath .. DIR_DELIM .. "dlg_delete_content.lua") dofile(menupath .. DIR_DELIM .. "dlg_delete_world.lua") diff --git a/builtin/mainmenu/tab_content.lua b/builtin/mainmenu/tab_content.lua index 8e92025f1..83e2b1082 100644 --- a/builtin/mainmenu/tab_content.lua +++ b/builtin/mainmenu/tab_content.lua @@ -16,6 +16,16 @@ --with this program; if not, write to the Free Software Foundation, Inc., --51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +local function get_content_icons(packages_with_updates) + local ret = {} + for _, content in ipairs(packages_with_updates) do + ret[content.virtual_path or content.path] = { type = "update" } + end + return ret +end + + local packages_raw, packages local function update_packages() @@ -62,14 +72,28 @@ local function get_formspec(tabview, name, tabdata) local use_technical_names = core.settings:get_bool("show_technical_names") + local packages_with_updates = update_detector.get_all() + local update_icons = get_content_icons(packages_with_updates) + local update_count = #packages_with_updates + local contentdb_label + if update_count == 0 then + contentdb_label = fgettext("Browse online content") + else + contentdb_label = fgettext("Browse online content [$1]", update_count) + end + local retval = { "label[0.4,0.4;", fgettext("Installed Packages:"), "]", - "tablecolumns[color;tree;text]", + "tablecolumns[color;tree;image,align=inline,width=1.5", + ",tooltip=", fgettext("Update available?"), + ",0=", core.formspec_escape(defaulttexturedir .. "blank.png"), + ",4=", core.formspec_escape(defaulttexturedir .. "cdb_update_cropped.png"), + ";text]", "table[0.4,0.8;6.3,4.8;pkglist;", - pkgmgr.render_packagelist(packages, use_technical_names), + pkgmgr.render_packagelist(packages, use_technical_names, update_icons), ";", tabdata.selected_pkg, "]", - "button[0.4,5.8;6.3,0.9;btn_contentdb;", fgettext("Browse online content"), "]" + "button[0.4,5.8;6.3,0.9;btn_contentdb;", contentdb_label, "]" } local selected_pkg @@ -104,15 +128,13 @@ local function get_formspec(tabview, name, tabdata) core.colorize("#BFBFBF", selected_pkg.name) end - table.insert_all(retval, { - "image[7.1,0.2;3,2;", core.formspec_escape(modscreenshot), "]", - "label[10.5,1;", core.formspec_escape(title_and_name), "]", - "box[7.1,2.4;8,3.1;#000]" - }) + local desc_height = 3.2 if selected_pkg.is_modpack then + desc_height = 2.1 + table.insert_all(retval, { - "button[11.1,5.8;4,0.9;btn_mod_mgr_rename_modpack;", + "button[7.1,4.7;8,0.9;btn_mod_mgr_rename_modpack;", fgettext("Rename"), "]" }) elseif selected_pkg.type == "mod" then @@ -136,25 +158,39 @@ local function get_formspec(tabview, name, tabdata) end end elseif selected_pkg.type == "txp" then + desc_height = 2.1 + if selected_pkg.enabled then table.insert_all(retval, { - "button[11.1,5.8;4,0.9;btn_mod_mgr_disable_txp;", + "button[7.1,4.7;8,0.9;btn_mod_mgr_disable_txp;", fgettext("Disable Texture Pack"), "]" }) else table.insert_all(retval, { - "button[11.1,5.8;4,0.9;btn_mod_mgr_use_txp;", + "button[7.1,4.7;8,0.9;btn_mod_mgr_use_txp;", fgettext("Use Texture Pack"), "]" }) end end - table.insert_all(retval, {"textarea[7.1,2.4;8,3.1;;;", desc, "]"}) + table.insert_all(retval, { + "image[7.1,0.2;3,2;", core.formspec_escape(modscreenshot), "]", + "label[10.5,1;", core.formspec_escape(title_and_name), "]", + "box[7.1,2.4;8,", tostring(desc_height), ";#000]", + "textarea[7.1,2.4;8,", tostring(desc_height), ";;;", desc, "]", + }) if core.may_modify_path(selected_pkg.path) then table.insert_all(retval, { "button[7.1,5.8;4,0.9;btn_mod_mgr_delete_mod;", - fgettext("Uninstall Package"), "]" + fgettext("Uninstall"), "]" + }) + end + + if update_icons[selected_pkg.virtual_path or selected_pkg.path] then + table.insert_all(retval, { + "button[11.1,5.8;4,0.9;btn_mod_mgr_update;", + fgettext("Update"), "]" }) end end @@ -216,6 +252,16 @@ local function handle_buttons(tabview, fields, tabname, tabdata) return true end + if fields.btn_mod_mgr_update then + local pkg = packages:get_list()[tabdata.selected_pkg] + local dlg = create_store_dlg(nil, { author = pkg.author, name = pkg.id or pkg.name }) + dlg:set_parent(tabview) + tabview:hide() + dlg:show() + packages = nil + return true + end + if fields.btn_mod_mgr_use_txp or fields.btn_mod_mgr_disable_txp then local txp_path = "" if fields.btn_mod_mgr_use_txp then @@ -235,7 +281,14 @@ end return { name = "content", - caption = fgettext("Content"), + caption = function() + local update_count = update_detector.get_count() + if update_count == 0 then + return fgettext("Content") + else + return fgettext("Content [$1]", update_count) + end + end, cbf_formspec = get_formspec, cbf_button_handler = handle_buttons, on_change = on_change diff --git a/doc/client_lua_api.md b/doc/client_lua_api.md index 0af102b10..2411d8fc1 100644 --- a/doc/client_lua_api.md +++ b/doc/client_lua_api.md @@ -672,6 +672,9 @@ Minetest namespace reference * If a flag in this table is set to true, the feature is RESTRICTED. * Possible flags: `load_client_mods`, `chat_messages`, `read_itemdefs`, `read_nodedefs`, `lookup_nodes`, `read_playerinfo` +* `minetest.urlencode(str)`: Encodes non-unreserved URI characters by a + percent sign followed by two hex digits. See + [RFC 3986, section 2.3](https://datatracker.ietf.org/doc/html/rfc3986#section-2.3). ### Logging * `minetest.debug(...)` @@ -1551,4 +1554,4 @@ Same as `image`, but does not accept a `position`; the position is instead deter texture = "image.png", -- ^ Uses texture (string) } -``` \ No newline at end of file +``` diff --git a/doc/fst_api.txt b/doc/fst_api.txt index 9ad06362d..c12093d91 100644 --- a/doc/fst_api.txt +++ b/doc/fst_api.txt @@ -50,7 +50,7 @@ methods: ^ tab: { name = "tabname", -- name of tab to create - caption = "tab caption", -- text to show for tab header + caption = "tab caption", -- text to show for tab header. Either a string or a function: (tabview) -> string cbf_button_handler = function(tabview, fields, tabname, tabdata), -- callback for button events --TODO cbf_events = function(tabview, event, tabname), -- callback for events cbf_formspec = function(tabview, name, tabdata), -- get formspec diff --git a/doc/lua_api.md b/doc/lua_api.md index af8656f65..213022c4f 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -5406,6 +5406,9 @@ Utilities use `colorspec_to_bytes` to generate raw RGBA values in a predictable way. The resulting PNG image is always 32-bit. Palettes are not supported at the moment. You may use this to procedurally generate textures during server init. +* `minetest.urlencode(str)`: Encodes non-unreserved URI characters by a + percent sign followed by two hex digits. See + [RFC 3986, section 2.3](https://datatracker.ietf.org/doc/html/rfc3986#section-2.3). Logging ------- diff --git a/doc/menu_lua_api.md b/doc/menu_lua_api.md index 0524d8273..1eec75228 100644 --- a/doc/menu_lua_api.md +++ b/doc/menu_lua_api.md @@ -101,7 +101,7 @@ 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) +* `core.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. @@ -394,10 +394,13 @@ Helpers * eg. `string.trim("\n \t\tfoo bar\t ")` == `"foo bar"` * `core.is_yes(arg)` (possible in async calls) * returns whether `arg` can be interpreted as yes -* `minetest.encode_base64(string)` (possible in async calls) +* `core.encode_base64(string)` (possible in async calls) * Encodes a string in base64. -* `minetest.decode_base64(string)` (possible in async calls) +* `core.decode_base64(string)` (possible in async calls) * Decodes a string encoded in base64. +* `core.urlencode(str)`: Encodes non-unreserved URI characters by a + percent sign followed by two hex digits. See + [RFC 3986, section 2.3](https://datatracker.ietf.org/doc/html/rfc3986#section-2.3). Async diff --git a/src/script/lua_api/l_util.cpp b/src/script/lua_api/l_util.cpp index 07cc77297..351d12205 100644 --- a/src/script/lua_api/l_util.cpp +++ b/src/script/lua_api/l_util.cpp @@ -651,6 +651,16 @@ int ModApiUtil::l_set_last_run_mod(lua_State *L) return 0; } +// urlencode(value) +int ModApiUtil::l_urlencode(lua_State *L) +{ + NO_MAP_LOCK_REQUIRED; + + const char *value = luaL_checkstring(L, 1); + lua_pushstring(L, urlencode(value).c_str()); + return 1; +} + void ModApiUtil::Initialize(lua_State *L, int top) { API_FCT(log); @@ -697,6 +707,8 @@ void ModApiUtil::Initialize(lua_State *L, int top) API_FCT(get_last_run_mod); API_FCT(set_last_run_mod); + API_FCT(urlencode); + LuaSettings::create(L, g_settings, g_settings_path); lua_setfield(L, top, "settings"); } @@ -723,6 +735,8 @@ void ModApiUtil::InitializeClient(lua_State *L, int top) API_FCT(colorspec_to_colorstring); API_FCT(colorspec_to_bytes); + API_FCT(urlencode); + LuaSettings::create(L, g_settings, g_settings_path); lua_setfield(L, top, "settings"); } @@ -766,6 +780,8 @@ void ModApiUtil::InitializeAsync(lua_State *L, int top) API_FCT(get_last_run_mod); API_FCT(set_last_run_mod); + API_FCT(urlencode); + LuaSettings::create(L, g_settings, g_settings_path); lua_setfield(L, top, "settings"); } diff --git a/src/script/lua_api/l_util.h b/src/script/lua_api/l_util.h index ec86c6632..07c4b86eb 100644 --- a/src/script/lua_api/l_util.h +++ b/src/script/lua_api/l_util.h @@ -128,6 +128,9 @@ private: // set_last_run_mod(modname) static int l_set_last_run_mod(lua_State *L); + // urlencode(value) + static int l_urlencode(lua_State *L); + public: static void Initialize(lua_State *L, int top); static void InitializeAsync(lua_State *L, int top); diff --git a/textures/base/pack/cdb_update_cropped.png b/textures/base/pack/cdb_update_cropped.png new file mode 100644 index 000000000..8161dd7e4 Binary files /dev/null and b/textures/base/pack/cdb_update_cropped.png differ