diff --git a/mod_sources.txt b/mod_sources.txt index 2e7537e..013da15 100644 --- a/mod_sources.txt +++ b/mod_sources.txt @@ -78,7 +78,7 @@ Mod: flora/farming origin https://github.com/daretmavi/i3.git (fetch) upstream https://github.com/minetest-mods/i3.git (fetch) -* main 7bd3e5d [origin/main] New Variable i3_no_trash_in_survival Better bool reading from settings +* main 4e39096 [origin/main] Merge pull request #4 from daretmavi/devtest Mod: gui/i3 origin https://repo.or.cz/minetest_hbarmor.git (fetch) @@ -118,7 +118,7 @@ origin https://notabug.org/tenplus1/mobs_redo (fetch) Mod: lib_api/mobs_redo origin https://github.com/appgurueu/modlib (fetch) -* master abced34 [origin/master] Deal with Lua log file memory leak +* master 35081f3 [origin/master] Localize global fields Mod: lib_api/modlib origin git@github.com:runsy/rcbows.git (fetch) diff --git a/mods/gui/i3/.editorconfig b/mods/gui/i3/.editorconfig new file mode 100644 index 0000000..89dddff --- /dev/null +++ b/mods/gui/i3/.editorconfig @@ -0,0 +1,9 @@ +[*] +end_of_line = lf + +[*.{lua,txt,md,json}] +charset = utf8 +indent_size = 8 +indent_style = tab +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/mods/gui/i3/etc/styles.lua b/mods/gui/i3/etc/styles.lua index da91599..fd3fa10 100644 --- a/mods/gui/i3/etc/styles.lua +++ b/mods/gui/i3/etc/styles.lua @@ -84,6 +84,9 @@ local styles = fmt([[ style[craft_rcp,craft_usg;noclip=true;font_size=16;sound=i3_craft; bgimg=i3_btn9.png;bgimg_hovered=i3_btn9_hovered.png; bgimg_pressed=i3_btn9_pressed.png;bgimg_middle=4,6] + style[confirm_trash_yes,confirm_trash_no;noclip=true;font_size=16; + bgimg=i3_btn9.png;bgimg_hovered=i3_btn9_hovered.png; + bgimg_pressed=i3_btn9_pressed.png;bgimg_middle=4,6] ]], PNG.slot, PNG.exit, PNG.exit_hover, diff --git a/mods/gui/i3/i3_mods_tabs.lua b/mods/gui/i3/i3_mods_tabs.lua new file mode 100644 index 0000000..2a32496 --- /dev/null +++ b/mods/gui/i3/i3_mods_tabs.lua @@ -0,0 +1,251 @@ +-- **************************************************************************** +-- Funnctions and Variables from i3 init.lua +local modpath = core.get_modpath "i3" + +local maxn, sort, concat, copy, insert, remove, indexof = + table.maxn, table.sort, table.concat, table.copy, + table.insert, table.remove, table.indexof + +local sprintf, find, gmatch, match, sub, split, upper, lower = + string.format, string.find, string.gmatch, string.match, + string.sub, string.split, string.upper, string.lower + +local PNG, styles, fs_elements = loadfile(modpath .. "/etc/styles.lua")() + +local ESC = core.formspec_escape +local S = core.get_translator "i3" + +local ES = function(...) + return ESC(S(...)) +end + +local function fmt(elem, ...) + if not fs_elements[elem] then + return sprintf(elem, ...) + end + + return sprintf(fs_elements[elem], ...) +end + +local function add_subtitle(fs, name, y, ctn_len, font_size, sep, label) + fs(fmt("style[%s;font=bold;font_size=%u]", name, font_size)) + fs("button", 0, y, ctn_len, 0.5, name, ESC(label)) + + if sep then + fs("image", 0, y + 0.55, ctn_len, 0.035, PNG.bar) + end +end + +-- **************************************************************************** +-- Function to get detail info aout texture +-- Forked from skinsdb mod +local function get_skin_info_formspec(skin, xoffset, yoffset) + local texture = skin:get_texture() + local m_name = skin:get_meta_string("name") + local m_author = skin:get_meta_string("author") + local m_license = skin:get_meta_string("license") + local m_format = skin:get_meta("format") + -- overview page + local raw_size = m_format == "1.8" and "2,2" or "2,1" + + --local lxoffs = 0.8 + xoffset + local cxoffs = 2 + xoffset + local rxoffs = 2 + xoffset + + local formspec = "" -- = "image["..lxoffs..","..0.6 + yoffset..";1,2;"..minetest.formspec_escape(skin:get_preview()).."]" + if texture then + formspec = formspec.."label["..rxoffs..","..2 + yoffset..";"..S("Raw texture")..":]" + .."image["..1 + rxoffs..","..2.5 + yoffset..";"..raw_size..";"..texture.."]" + end + if m_name ~= "" then + formspec = formspec.."label["..cxoffs..","..0.5 + yoffset..";"..S("Name")..": "..minetest.formspec_escape(m_name).."]" + end + if m_author ~= "" then + formspec = formspec.."label["..cxoffs..","..1 + yoffset..";"..S("Author")..": "..minetest.formspec_escape(m_author).."]" + end + if m_license ~= "" then + formspec = formspec.."label["..cxoffs..","..1.5 + yoffset..";"..S("License")..": "..minetest.formspec_escape(m_license).."]" + end + return formspec +end + +-- skin images and pages +local function get_skin_selection_formspec(player, context) + context.skins_list = skins.get_skinlist_for_player(player:get_player_name()) + context.total_pages = 1 + local xoffs = 0.8 + local yoffs = 6.5 + local xspc = 1.1 + local yspc = 2.1 + local skinwidth = 1 + local skinheight = 2 + local xscale = 1 + local btn_y = yoffs + 4.5 + local drop_y = yoffs + 4.5 + local btn_width = 0.35 + local btn_heigh = 0.35 + local droppos = xoffs + 1.3 + local droplen = 6.2 + local btn_left = droppos - btn_width + local btn_right = droppos + droplen + local maxdisp = 16 + + local ctrls_height = 0.4 + + for i, skin in ipairs(context.skins_list ) do + local page = math.floor((i-1) / maxdisp)+1 + skin:set_meta("inv_page", page) + skin:set_meta("inv_page_index", (i-1)%maxdisp+1) + context.total_pages = page + end + context.skins_page = context.skins_page or skins.get_player_skin(player):get_meta("inv_page") or 1 + context.dropdown_values = nil + + local page = context.skins_page + local formspec = "" + + for i = (page-1)*maxdisp+1, page*maxdisp do + local skin = context.skins_list[i] + if not skin then + break + end + + local index_p = skin:get_meta("inv_page_index") + local x = ((index_p-1) % 8) * xspc + xoffs + local y + if index_p > 8 then + y = yoffs + yspc + else + y = yoffs + end + formspec = formspec.. + string.format("image_button[%f,%f;%f,%f;%s;skins_set$%i;]", + x, y, skinwidth, skinheight, + minetest.formspec_escape(skin:get_preview()), i).. + "tooltip[skins_set$"..i..";"..minetest.formspec_escape(skin:get_meta_string("name")).."]" + end + + if context.total_pages > 1 then + local page_prev = page - 1 + local page_next = page + 1 + if page_prev < 1 then + page_prev = context.total_pages + end + if page_next > context.total_pages then + page_next = 1 + end + local page_list = "" + context.dropdown_values = {} + for pg=1, context.total_pages do + local pagename = S("Page").." "..pg.."/"..context.total_pages + context.dropdown_values[pagename] = pg + if pg > 1 then page_list = page_list.."," end + page_list = page_list..pagename + end + formspec = formspec.. + string.format("image_button[%f,%f;%f,%f;%s;skins_page$%i;]", + btn_left, btn_y, btn_width, btn_heigh, PNG.prev, page_prev).. + string.format("image_button[%f,%f;%f,%f;%s;skins_page$%i;]", + btn_right, btn_y, btn_width, btn_heigh, PNG.next, page_next).. + string.format("dropdown[%f,%f;%f,%f;skins_selpg;%s;%i]", + droppos, drop_y, droplen, ctrls_height, page_list, page) + end + return formspec +end + +-- **************************************************************************** +-- i3 Tab definition + +i3.new_tab { + name = "Skins", + description = "Skins", + image = "i3_skin.png", + + formspec = function(player, data, fs) + --fs("label[3,1;Test 1]") + local name = player:get_player_name() + + local ctn_len, ctn_hgt = 5.7, 6.3 + + local yextra = 1 + local yoffset = 0 + local xpos = 5 + + local _skins = skins.get_skinlist_for_player(name) + local skin_name = skins.get_player_skin(player).name + local sks, id = {}, 1 + + local props = player:get_properties() + + for i, skin in ipairs(_skins) do + if skin.name == skin_name then + id = i + end + + sks[#sks + 1] = skin.name + end + + sks = concat(sks, ","):gsub(";", "") + + add_subtitle(fs, "player_name", 0, ctn_len + 4.5, 22, true, ESC(name)) + + if props.mesh ~= "" then + local anim = player:get_local_animation() + local armor_skin = __3darmor or __skinsdb + local t = {} + + for _, v in ipairs(props.textures) do + t[#t + 1] = ESC(v):gsub(",", "!") + end + + local textures = concat(t, ","):gsub("!", ",") + local skinxoffset = 1.3 + + --fs("style[player_model;bgcolor=black]") + fs("model", skinxoffset, 0.2, armor_skin and 4 or 3.4, ctn_hgt, + "player_model", props.mesh, textures, "0,-150", "false", "false", + fmt("%u,%u%s", anim.x, anim.y, data.fs_version >= 5 and ";30" or "")) + else + local size = 2.5 + fs("image", 0.7, 0.2, size, size * props.visual_size.y, props.textures[1]) + end + + fs("label", xpos, yextra, fmt("%s:", ES"Select a skin")) + fs(fmt("dropdown[%f,%f;4,0.6;skins;%s;%u;true]", xpos, yextra + 0.2, sks, id)) + + local skin = skins.get_player_skin(player) + local formspec = get_skin_info_formspec(skin, 3, 2) + fs(formspec) + --core.log("fs skins: "..dump(formspec)) + + local context = skins.get_formspec_context(player) + local formspec = get_skin_selection_formspec(player, context) + --core.log("skins context: "..dump(context)) + --core.log("fs skins: "..dump(formspec)) + fs(formspec) + end, + + -- click button handlers - giving fields + fields = function(player, data, fields) + local name = player:get_player_name() + local sb_inv = fields.scrbar_inv + + --core.log("fields: "..dump(fields)) + + -- set skin with dropdown from original i3 inventory + if fields.skins then + local id = tonumber(fields.skins) + local _skins = skins.get_skinlist_for_player(name) + skins.set_player_skin(player, _skins[id]) + end + + -- set skin with skinddb image an page change + local context = skins.get_formspec_context(player) + local action = skins.on_skin_selection_receive_fields(player, context, fields) + + -- update formspec + return i3.set_fs(player) + end, +} + +-- **************************************************************************** diff --git a/mods/gui/i3/init.lua b/mods/gui/i3/init.lua index da63de7..f79b921 100644 --- a/mods/gui/i3/init.lua +++ b/mods/gui/i3/init.lua @@ -20,13 +20,17 @@ local PNG, styles, fs_elements = loadfile(modpath .. "/etc/styles.lua")() local compress_groups, compressed = loadfile(modpath .. "/etc/compress.lua")() local group_stereotypes, group_names = loadfile(modpath .. "/etc/groups.lua")() -local progressive_mode = core.settings:get_bool("i3_progressive_mode", false) +local progressive_mode = core.settings:get_bool("i3_progressive_mode", true) local item_compression = core.settings:get_bool("i3_item_compression", true) local damage_enabled = core.settings:get_bool "enable_damage" +-- new settings +local creative_trash_only = core.settings:get_bool("i3_no_trash_in_survival", true) +local use_skinsdb_tab = core.settings:get_bool("i3_skinsdb_tab", true) + +-- ************ + local __3darmor, __skinsdb, __awards -local __sfinv, old_sfinv_fn -local __unified_inventory, old_unified_inventory_fn local http = core.request_http_api() local singleplayer = core.is_singleplayer() @@ -107,11 +111,11 @@ local function get_formspec_version(info) end local function outdated(name) - local fs = sprintf("size[5.8,1.3]image[0,0;1,1;%s]label[1,0;%s]button_exit[2.4,0.8;1,1;;OK]", + local fs = sprintf("size[6.3,1.3]image[0,0;1,1;%s]label[1,0;%s]button_exit[2.6,0.8;1,1;;OK]", PNG.book, - "Your Minetest client is outdated.\nGet the latest version on minetest.net to use i3") + "Your Minetest client is outdated.\nGet the latest version on minetest.net to play the game.") - core.show_formspec(name, "i3", fs) + core.show_formspec(name, "i3_outdated", fs) end local old_is_creative_enabled = core.is_creative_enabled @@ -2011,7 +2015,6 @@ local function get_ctn_content(fs, data, player, yoffset, ctn_len, award_list, a -- fmt("list[detached:i3_trash;main;%f,%f;1,1;]", 4.45, yoffset + 3.75)) --fs("image", 4.45, yoffset + 3.75, 1, 1, PNG.trash) - local creative_trash_only = core.settings:get_bool("i3_no_trash_in_survival", true) if core.is_creative_enabled(name) or not creative_trash_only then fs(fmt("list[detached:i3_trash;main;%f,%f;1,1;]", 4.45, yoffset + 3.75)) fs("image", 4.45, yoffset + 3.75, 1, 1, PNG.trash) @@ -2020,6 +2023,8 @@ local function get_ctn_content(fs, data, player, yoffset, ctn_len, award_list, a local yextra = damage_enabled and 5.5 or 5 for i, title in ipairs(SUBCAT) do + if title ~= "skins" or not use_skinsdb_tab then + local btn_name = fmt("btn_%s", title) fs(fmt("style[btn_%s;fgimg=%s;fgimg_hovered=%s;content_offset=0]", title, @@ -2028,6 +2033,8 @@ local function get_ctn_content(fs, data, player, yoffset, ctn_len, award_list, a fs("image_button", 0.25 + ((i - 1) * 1.18), yextra - 0.2, 0.5, 0.5, "", btn_name, "") fs(fmt("tooltip[%s;%s]", btn_name, title:gsub("^%l", upper))) + + end end fs("box", 0, yextra + 0.45, ctn_len, 0.045, "#bababa50") @@ -2059,19 +2066,24 @@ local function get_ctn_content(fs, data, player, yoffset, ctn_len, award_list, a end elseif data.subcat == 3 then - if __skinsdb then + if __skinsdb and not use_skinsdb_tab then local _skins = skins.get_skinlist_for_player(name) - local sks = {} + local skin_name = skins.get_player_skin(player).name + local sks, id = {}, 1 + + for i, skin in ipairs(_skins) do + if skin.name == skin_name then + id = i + end - for _, skin in ipairs(_skins) do sks[#sks + 1] = skin.name end sks = concat(sks, ","):gsub(";", "") fs("label", 0, yextra + 0.85, fmt("%s:", ES"Select a skin")) - fs(fmt("dropdown[0,%f;4,0.6;skins;%s;%u;true]", yextra + 1.1, sks, data.skin_id or 1)) - else + fs(fmt("dropdown[0,%f;4,0.6;skins;%s;%u;true]", yextra + 1.1, sks, id)) + elseif not use_skinsdb_tab then not_installed("skinsdb") end @@ -2138,18 +2150,26 @@ local function get_tabs_fs(player, data, fs, full_height) end local function get_debug_grid(data, fs, full_height) - local spacing = 0.2 + fs("style_type[label;font_size=8;noclip=true]") + local spacing, i = 0.2, 1 for x = 0, data.inv_width + 8, spacing do fs("box", x, 0, 0.01, full_height, "#ff0") + fs("label", x, full_height + 0.1, tostring(i)) + i = i + 1 end + i = 61 + for y = 0, full_height, spacing do fs("box", 0, y, data.inv_width + 8, 0.01, "#ff0") + fs("label", -0.15, y, tostring(i)) + i = i - 1 end fs("box", data.inv_width / 2, 0, 0.01, full_height, "#f00") fs("box", 0, full_height / 2, data.inv_width, 0.01, "#f00") + fs("style_type[label;font_size=16]") end local function make_fs(player, data) @@ -2309,20 +2329,21 @@ local function init_data(player, info) end local function reset_data(data) - data.filter = "" - data.expand = "" - data.pagenum = 1 - data.rnum = 1 - data.unum = 1 - data.scrbar_rcp = 1 - data.scrbar_usg = 1 - data.query_item = nil - data.recipes = nil - data.usages = nil - data.export_rcp = nil - data.export_usg = nil - data.alt_items = nil - data.items = data.items_raw + data.filter = "" + data.expand = "" + data.pagenum = 1 + data.rnum = 1 + data.unum = 1 + data.scrbar_rcp = 1 + data.scrbar_usg = 1 + data.query_item = nil + data.recipes = nil + data.usages = nil + data.export_rcp = nil + data.export_usg = nil + data.alt_items = nil + data.confirm_trash = nil + data.items = data.items_raw if data.current_itab > 1 then sort_by_category(data) @@ -2524,12 +2545,12 @@ local function get_inventory_fs(player, data, fs) fs("scroll_container_end[]") local btn = { - --{"trash", ES"Trash all items"}, + --{"trash", ES"Clear inventory"}, {"sort_az", ES"Sort items (A-Z)"}, {"sort_za", ES"Sort items (Z-A)"}, {"compress", ES"Compress items"}, } - if core.is_creative_enabled(name) then table.insert(btn, 1, {"trash", ES"Trash all items"}) end + if core.is_creative_enabled(name) then table.insert(btn, 1, {"trash", ES"Clear inventory"}) end for i, v in ipairs(btn) do local btn_name, tooltip = unpack(v) @@ -2540,6 +2561,18 @@ local function get_inventory_fs(player, data, fs) fs("image_button", i + 3.447 - (i * 0.4), 11.43, 0.35, 0.35, "", btn_name, "") fs(fmt("tooltip[%s;%s]", btn_name, tooltip)) end + + if data.confirm_trash then + fs("style_type[box;colors=#999,#999,#808080,#808080]") + + for _ = 1, 3 do + fs("box", 2.97, 10.75, 4.3, 0.5, "") + end + + fs("label", 3.12, 11, "Confirm trash?") + fs("image_button", 5.17, 10.75, 1, 0.5, "", "confirm_trash_yes", "Yes") + fs("image_button", 6.27, 10.75, 1, 0.5, "", "confirm_trash_no", "No") + end end i3.new_tab { @@ -2551,10 +2584,10 @@ i3.new_tab { local name = player:get_player_name() local sb_inv = fields.scrbar_inv - if fields.skins and data.skin_id ~= tonum(fields.skins) then - data.skin_id = tonum(fields.skins) + if fields.skins then + local id = tonum(fields.skins) local _skins = skins.get_skinlist_for_player(name) - skins.set_player_skin(player, _skins[data.skin_id]) + skins.set_player_skin(player, _skins[id]) end for field in pairs(fields) do @@ -2611,9 +2644,16 @@ i3.new_tab { end if fields.trash then - local inv = player:get_inventory() - inv:set_list("main", {}) - inv:set_list("craft", {}) + data.confirm_trash = true + + elseif fields.confirm_trash_yes or fields.confirm_trash_no then + if fields.confirm_trash_yes then + local inv = player:get_inventory() + inv:set_list("main", {}) + inv:set_list("craft", {}) + end + + data.confirm_trash = nil elseif fields.compress then compress_items(player) @@ -2900,18 +2940,12 @@ end core.register_on_mods_loaded(function() get_init_items() - __sfinv = rawget(_G, "sfinv") - - if __sfinv then - old_sfinv_fn = sfinv.set_player_inventory_formspec + if rawget(_G, "sfinv") then function sfinv.set_player_inventory_formspec() return end sfinv.enabled = false end - __unified_inventory = rawget(_G, "unified_inventory") - - if __unified_inventory then - old_unified_inventory_fn = unified_inventory.set_inventory_formspec + if rawget(_G, "unified_inventory") then function unified_inventory.set_inventory_formspec() return end end end) @@ -2991,21 +3025,7 @@ core.register_on_joinplayer(function(player) local info = core.get_player_information and core.get_player_information(name) if not info or get_formspec_version(info) < MIN_FORMSPEC_VERSION then - if __sfinv then - sfinv.set_player_inventory_formspec = old_sfinv_fn - sfinv.enabled = true - end - - if __unified_inventory then - unified_inventory.set_inventory_formspec = old_unified_inventory_fn - - if __sfinv then - sfinv.enabled = false - end - end - pdata[name] = nil - return outdated(name) end @@ -3036,7 +3056,6 @@ end) local META_SAVES = { bag_size = true, waypoints = true, - skin_id = true, inv_items = true, known_recipes = true, } @@ -3074,11 +3093,14 @@ end after(SAVE_INTERVAL, routine) core.register_on_player_receive_fields(function(player, formname, fields) - if formname ~= "" then + local name = player:get_player_name() + + if formname == "i3_outdated" then + return false, core.kick_player(name, "Come back when your client is up-to-date.") + elseif formname ~= "" then return false end - local name = player:get_player_name() local data = pdata[name] if not data then return end @@ -3286,6 +3308,7 @@ if progressive_mode then local player = players[i] local name = player:get_player_name() local data = pdata[name] + if not data then return end local inv_items = get_inv_items(player) local diff = array_diff(inv_items, data.inv_items) @@ -3321,7 +3344,7 @@ if progressive_mode then local name = player:get_player_name() local data = pdata[name] - if data.show_hud ~= nil and singleplayer then + if data and data.show_hud ~= nil and singleplayer then show_hud_success(player, data) end end @@ -3362,6 +3385,7 @@ if progressive_mode then core.register_on_joinplayer(function(player) local name = player:get_player_name() local data = pdata[name] + if not data then return end -- remove recipe filter to suppress progressive_mode in creative -- on player join @@ -3413,3 +3437,7 @@ end --dofile(modpath .. "/tests/test_tabs.lua") --dofile(modpath .. "/tests/test_custom_recipes.lua") + +if __skinsdb and use_skinsdb_tab then + dofile(modpath .. "/i3_mods_tabs.lua") +end \ No newline at end of file diff --git a/mods/gui/i3/settingtypes.txt b/mods/gui/i3/settingtypes.txt index 8d4766d..5c8d388 100644 --- a/mods/gui/i3/settingtypes.txt +++ b/mods/gui/i3/settingtypes.txt @@ -1,8 +1,15 @@ # The progressive mode shows recipes you can craft from items you ever had in your inventory. -i3_progressive_mode (Learn crafting recipes progressively) bool false +i3_progressive_mode (Learn crafting recipes progressively) bool true # Regroup the items of the same type in the item list. i3_item_compression (Regroup items of the same type) bool true + # ------------ NEW SETTINGS ------------ +# not in original i3 available + +# Show trash only in creative mode, or with creative privileges i3_no_trash_in_survival (Trash is available only in creative) bool true + +# For skin choice use tab instead of inventory dropdown only +i3_skinsdb_tab (Use tab for skins) bool true diff --git a/mods/lib_api/modlib/License.txt b/mods/lib_api/modlib/License.txt new file mode 100644 index 0000000..8532a89 --- /dev/null +++ b/mods/lib_api/modlib/License.txt @@ -0,0 +1,7 @@ +Copyright 2019 - 2021 Lars Mueller alias LMD or appguru(eu) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/mods/lib_api/modlib/binary.lua b/mods/lib_api/modlib/binary.lua index 79d58cb..03bdd67 100644 --- a/mods/lib_api/modlib/binary.lua +++ b/mods/lib_api/modlib/binary.lua @@ -1,5 +1,6 @@ -- Localize globals -local assert, math = assert, math +local assert, math_huge, math_frexp, math_floor + = assert, math.huge, math.frexp, math.floor -- Set environment local _ENV = {} @@ -30,7 +31,7 @@ function read_float(read_byte, double) mantissa = (mantissa + byte_2) / 0x80 if exponent == 0xFF then if mantissa == 0 then - return sign * math.huge + return sign * math_huge end -- Differentiating quiet and signalling nan is not possible in Lua, hence we don't have to do it -- HACK ((0/0)^1) yields nan, 0/0 yields -nan @@ -67,7 +68,7 @@ end function write_uint(write_byte, uint, bytes) for _ = 1, bytes do write_byte(uint % 0x100) - uint = math.floor(uint / 0x100) + uint = math_floor(uint / 0x100) end assert(uint == 0) end @@ -80,16 +81,16 @@ function write_float(write_byte, number, on_write, double) number = -number sign = 0x80 end - local mantissa, exponent = math.frexp(number) + local mantissa, exponent = math_frexp(number) exponent = exponent + 127 if exponent > 1 then -- TODO ensure this deals properly with subnormal numbers mantissa = mantissa * 2 - 1 exponent = exponent - 1 end - local sign_byte = sign + math.floor(exponent / 2) + local sign_byte = sign + math_floor(exponent / 2) mantissa = mantissa * 0x80 - local exponent_byte = (exponent % 2) * 0x80 + math.floor(mantissa) + local exponent_byte = (exponent % 2) * 0x80 + math_floor(mantissa) mantissa = mantissa % 1 local mantissa_bytes = {} -- TODO ensure this check is proper @@ -102,7 +103,7 @@ function write_float(write_byte, number, on_write, double) local len = double and 6 or 2 for index = len, 1, -1 do mantissa = mantissa * 0x100 - mantissa_bytes[index] = math.floor(mantissa) + mantissa_bytes[index] = math_floor(mantissa) mantissa = mantissa % 1 end assert(mantissa == 0) diff --git a/mods/lib_api/modlib/bluon.lua b/mods/lib_api/modlib/bluon.lua index 20af1b6..72b4175 100644 --- a/mods/lib_api/modlib/bluon.lua +++ b/mods/lib_api/modlib/bluon.lua @@ -1,5 +1,6 @@ -- Localize globals -local assert, error, ipairs, math, modlib, next, pairs, setmetatable, string, table, type, unpack = assert, error, ipairs, math, modlib, next, pairs, setmetatable, string, table, type, unpack +local assert, error, ipairs, math_floor, math_huge, modlib, next, pairs, setmetatable, string, table_insert, type, unpack + = assert, error, ipairs, math.floor, math.huge, modlib, next, pairs, setmetatable, string, table.insert, type, unpack -- Set environment local _ENV = {} @@ -61,8 +62,8 @@ local constants = { [0] = "\2", -- not possible as table entry as Lua doesn't allow +/-nan as table key -- [0/0] = "\3", - [math.huge] = "\4", - [-math.huge] = "\5", + [math_huge] = "\4", + [-math_huge] = "\5", [""] = "\20" } @@ -297,18 +298,18 @@ function read(self, stream) end if type <= type_ranges.string then local string = stream_read(uint(type - type_ranges.number)) - table.insert(references, string) + table_insert(references, string) return string end if type <= type_ranges.table then type = type - type_ranges.string - 1 local tab = {} - table.insert(references, tab) + table_insert(references, tab) if type == 0 then return tab end local list_len = uint(type % 5) - local kv_len = uint(math.floor(type / 5)) + local kv_len = uint(math_floor(type / 5)) for index = 1, list_len do tab[index] = _read(stream_read(1)) end diff --git a/mods/lib_api/modlib/init.lua b/mods/lib_api/modlib/init.lua index fdf5e80..4679998 100644 --- a/mods/lib_api/modlib/init.lua +++ b/mods/lib_api/modlib/init.lua @@ -50,6 +50,7 @@ for _, file in pairs{ "ranked_set", "binary", "b3d", + "luon", "bluon", "persistence", "debug" diff --git a/mods/lib_api/modlib/luon.lua b/mods/lib_api/modlib/luon.lua new file mode 100644 index 0000000..3ae43f7 --- /dev/null +++ b/mods/lib_api/modlib/luon.lua @@ -0,0 +1,165 @@ +local assert, next, pairs, pcall, error, type, table_insert, table_concat, string_format, string_match, setfenv, math_huge, loadfile, loadstring + = assert, next, pairs, pcall, error, type, table.insert, table.concat, string.format, string.match, setfenv, math.huge, loadfile, loadstring +local count_values = modlib.table.count_values + +-- Build a table with the succeeding character from A-Z +local succ = {} +for char = ("A"):byte(), ("Z"):byte() - 1 do + succ[string.char(char)] = string.char(char + 1) +end + +local function quote(string) + return string_format("%q", string) +end + +local _ENV = {} +setfenv(1, _ENV) + +function write(object, write) + local reference = {"A"} + local function increment_reference(place) + if not reference[place] then + reference[place] = "B" + elseif reference[place] == "Z" then + reference[place] = "A" + return increment_reference(place + 1) + else + reference[place] = succ[reference[place]] + end + end + local references = {} + local to_fill = {} + for value, count in pairs(count_values(object)) do + local type_ = type(value) + if count >= 2 and ((type_ == "string" and #reference + 2 >= #value) or type_ == "table") then + local ref = table_concat(reference) + write(ref) + write"=" + write(type_ == "table" and "{}" or quote(value)) + write";" + references[value] = ref + if type_ == "table" then + to_fill[value] = true + end + increment_reference(1) + end + end + local function is_short_key(key) + return not references[key] and type(key) == "string" and string_match(key, "^[%a_][%a%d_]*$") + end + local function dump(value) + -- Primitive types + if value == nil then + return write"nil" + end + if value == true then + return write"true" + end + if value == false then + return write"false" + end + local type_ = type(value) + if type_ == "number" then + return write(string_format("%.17g", value)) + end + -- Reference types: table and string + local ref = references[value] + if ref then + -- Referenced + if not to_fill[value] then + return write(ref) + end + -- Fill table + to_fill[value] = false + for k, v in pairs(value) do + write(ref) + if is_short_key(k) then + write"." + write(k) + else + write"[" + dump(k) + write"]" + end + write"=" + dump(v) + write";" + end + elseif type_ == "string" then + return write(quote(value)) + elseif type_ == "table" then + local first = true + write"{" + local len = #value + for i = 1, len do + if not first then write";" end + dump(value[i]) + first = false + end + for k, v in next, value do + if type(k) ~= "number" or k % 1 ~= 0 or k < 1 or k > len then + if not first then write";" end + if is_short_key(k) then + write(k) + else + write"[" + dump(k) + write"]" + end + write"=" + dump(v) + first = false + end + end + write"}" + else + error("unsupported type: " .. type_) + end + end + local fill_root = to_fill[object] + if fill_root then + -- Root table is circular, must return by named reference + dump(object) + write"return " + write(references[object]) + else + -- Root table is not circular, can directly start writing + write"return " + dump(object) + end +end + +function write_file(object, file) + return write(object, function(text) + file:write(text) + end) +end + +function write_string(object) + local rope = {} + write(object, function(text) + table_insert(rope, text) + end) + return table_concat(rope) +end + +function read(...) + local read = assert(...) + -- math.huge is serialized to inf, 0/0 is serialized to -nan + setfenv(read, {inf = math_huge, nan = 0/0}) + local success, value_or_err = pcall(read) + if success then + return value_or_err + end + return nil, value_or_err +end + +function read_file(path) + return read(loadfile(path)) +end + +function read_string(string) + return read(loadstring(string)) +end + +return _ENV \ No newline at end of file diff --git a/mods/lib_api/modlib/minetest/colorspec.lua b/mods/lib_api/modlib/minetest/colorspec.lua index 0536167..3658b12 100644 --- a/mods/lib_api/modlib/minetest/colorspec.lua +++ b/mods/lib_api/modlib/minetest/colorspec.lua @@ -185,7 +185,7 @@ function colorspec.from_string(string) if number then return colorspec.from_number(number * 0x100 + alpha) end - local hex_text = string:match(hex) + local hex_text = string:match("^" .. hex .. "$") local len, num = hex_text:len(), tonumber(hex_text, 16) if len == 8 then return colorspec.from_number(num) diff --git a/mods/lib_api/modlib/persistence.lua b/mods/lib_api/modlib/persistence.lua index 525e469..1342a7b 100644 --- a/mods/lib_api/modlib/persistence.lua +++ b/mods/lib_api/modlib/persistence.lua @@ -106,43 +106,39 @@ function lua_log_file:_dump(value, is_key) end reference = self.reference_count + 1 local key = "R[" .. reference .."]" - local formatted local function create_reference() self.reference_count = reference self.references[value] = reference end if _type == "string" then local reference_strings = self.reference_strings - if is_key and ((not reference_strings) or value:len() <= key:len()) and value:match"[%a_][%a%d_]*" then + if is_key and ((not reference_strings) or value:len() <= key:len()) and value:match"^[%a_][%a%d_]*$" then -- Short key return value, true end - formatted = ("%q"):format(value) + local formatted = ("%q"):format(value) if (not reference_strings) or formatted:len() <= key:len() then -- Short string return formatted end -- Use reference create_reference() + self:log(key .. "=" .. formatted) elseif _type == "table" then -- Tables always need a reference before they are traversed to prevent infinite recursion create_reference() - local entries = {} - for _, value in ipairs(value) do - table.insert(entries, self:_dump(value)) - end + -- TODO traverse tables to determine whether this is actually needed + self:log(key .. "={}") local tablelen = #value - for key, value in pairs(value) do - if type(key) ~= "number" or key % 1 ~= 0 or key < 1 or key > tablelen then - local dumped, short = self:_dump(key, true) - table.insert(entries, (short and dumped or ("[" .. dumped .. "]")) .. "=" .. self:_dump(value)) + for k, v in pairs(value) do + if type(k) ~= "number" or k % 1 ~= 0 or k < 1 or k > tablelen then + local dumped, short = self:_dump(k, true) + self:log(key .. (short and ("." .. dumped) or ("[" .. dumped .. "]")) .. "=" .. self:_dump(v)) end end - formatted = "{" .. table.concat(entries, ";") .. "}" else error("unsupported type: " .. _type) end - self:log(key .. "=" .. formatted) return key end diff --git a/mods/lib_api/modlib/table.lua b/mods/lib_api/modlib/table.lua index b96f89b..3c3c200 100644 --- a/mods/lib_api/modlib/table.lua +++ b/mods/lib_api/modlib/table.lua @@ -337,6 +337,27 @@ function deep_foreach_any(table, func) visit(table) end +-- Recursively counts occurences of values in a table +-- Also counts primitive values like boolean and number +-- Does not count NaN, because that doesn't work as a table index +function count_values(value) + local counts = {} + local function count_values_(value) + -- Ignore NaN + if value ~= value then return end + local count = counts[value] + counts[value] = (count or 0) + 1 + if not count and type(value) == "table" then + for k, v in pairs(value) do + count_values_(k) + count_values_(v) + end + end + end + count_values_(value) + return counts +end + function foreach_value(table, func) for _, v in pairs(table) do func(v) diff --git a/mods/lib_api/modlib/test.lua b/mods/lib_api/modlib/test.lua index ffff5b5..b3d2c1f 100644 --- a/mods/lib_api/modlib/test.lua +++ b/mods/lib_api/modlib/test.lua @@ -166,10 +166,41 @@ for _ = 1, 1000 do assert(distance == min_distance) end +local function serializer_test(assert_preserves) + -- TODO nan + for _, constant in pairs{true, false, huge, -huge} do + assert_preserves(constant) + end + -- Strings + for i = 1, 1000 do + assert_preserves(_G.table.concat(table.repetition(_G.string.char(i % 256), i))) + end + -- Numbers + for _ = 1, 1000 do + local int = random(-2^50, 2^50) + assert(int % 1 == 0) + assert_preserves(int) + assert_preserves((random() - 0.5) * 2^random(-20, 20)) + end + -- Simple tables + assert_preserves{hello = "world", welt = "hallo"} + assert_preserves{"hello", "hello", "hello"} + local circular = {} + circular[circular] = circular + circular[1] = circular + assert_preserves(circular) + local mixed = {1, 2, 3} + mixed[mixed] = mixed + mixed.vec = {x = 1, y = 2, z = 3} + mixed.vec2 = modlib.table.copy(mixed.vec) + mixed.blah = "blah" + assert_preserves(mixed) +end + -- bluon do local bluon = bluon - local function assert_preserves(object) + serializer_test(function(object) local rope = table.rope{} local written, read, input local _, err = pcall(function() @@ -186,25 +217,17 @@ do written = written and text.hexdump(written), err = err }) - end - for _, constant in pairs{true, false, huge, -huge} do - assert_preserves(constant) - end - for i = 1, 1000 do - assert_preserves(_G.table.concat(table.repetition(_G.string.char(i % 256), i))) - end - for _ = 1, 1000 do - local int = random(-2^50, 2^50) - assert(int % 1 == 0) - assert_preserves(int) - assert_preserves((random() - 0.5) * 2^random(-20, 20)) - end - assert_preserves{hello = "world", welt = "hallo"} - assert_preserves{"hello", "hello", "hello"} - local a = {} - a[a] = a - a[1] = a - assert_preserves(a) + end) +end + +-- luon +do + serializer_test(function(object) + local serialized = luon.write_string(object) + assert(table.equals_references(object, luon.read_string(serialized)), serialized) + end) + local nan = luon.read_string(luon.write_string(0/0)) + assert(nan ~= nan) end if not _G.minetest then return end @@ -230,9 +253,16 @@ local function test_logfile(reference_strings) logfile.root = {a_longer_string = "test"} logfile:rewrite() logfile:set_root({a = 1}, {b = 2, c = 3, d = _G.math.huge, e = -_G.math.huge}) + local circular = {} + circular[circular] = circular + logfile:set_root(circular, circular) logfile:close() logfile:init() - assert(table.equals(logfile.root, {a_longer_string = "test", [{a = 1}] = {b = 2, c = 3, d = _G.math.huge, e = -_G.math.huge}})) + assert(table.equals_references(logfile.root, { + a_longer_string = "test", + [{a = 1}] = {b = 2, c = 3, d = _G.math.huge, e = -_G.math.huge}, + [circular] = circular, + })) if not reference_strings then for key in pairs(logfile.references) do assert(type(key) ~= "string")