diff --git a/.gitattributes b/.gitattributes index 06b76c6c8..ecd9a7a29 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,3 +3,5 @@ *.cpp diff=cpp *.h diff=cpp + +*.gltf binary diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 2d0c72907..a2ec10a0a 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -88,7 +88,7 @@ jobs: - name: Install deps run: | source ./util/ci/common.sh - install_linux_deps clang-7 llvm + install_linux_deps clang-7 llvm-7 - name: Build run: | @@ -102,6 +102,11 @@ jobs: run: | ./bin/minetest --run-unittests + # Do this here because we have ASan and error paths are sensitive to dangling pointers + - name: Test error cases + run: | + ./util/test_error_cases.sh + # Current clang version clang_18: runs-on: ubuntu-24.04 diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index e193c828d..731d7d719 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -29,8 +29,8 @@ on: jobs: build: - # use lowest possible macOS running on x86_64 to support more users - runs-on: macos-12 + # use lowest possible macOS running on x86_64 supported by brew to support more users + runs-on: macos-13 steps: - uses: actions/checkout@v4 - name: Install deps diff --git a/.gitignore b/.gitignore index 8ff758720..c7879380b 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ tags !tags/ gtags.files .idea +.qtcreator/ # Codelite *.project # Visual Studio Code & plugins @@ -109,6 +110,8 @@ src/cmake_config_githash.h *.layout *.o *.a +*.dump +*.dmp *.ninja .ninja* *.gch diff --git a/LICENSE.txt b/LICENSE.txt index de76c7a80..03ca35100 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -57,12 +57,10 @@ srifqi: textures/base/pack/minimap_btn.png Zughy: - textures/base/pack/cdb_add.png 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 textures/base/pack/settings_reset.png @@ -79,7 +77,6 @@ kilbith: textures/base/pack/progress_bar_bg.png SmallJoker: - textures/base/pack/cdb_clear.png textures/base/pack/server_favorite_delete.png (based on server_favorite.png) DS: diff --git a/builtin/common/item_s.lua b/builtin/common/item_s.lua index 72a722ed1..673c83877 100644 --- a/builtin/common/item_s.lua +++ b/builtin/common/item_s.lua @@ -166,20 +166,19 @@ function core.is_colored_paramtype(ptype) end function core.strip_param2_color(param2, paramtype2) - if not core.is_colored_paramtype(paramtype2) then + if paramtype2 == "color" then + return param2 + elseif paramtype2 == "colorfacedir" then + return math.floor(param2 / 32) * 32 + elseif paramtype2 == "color4dir" then + return math.floor(param2 / 4) * 4 + elseif paramtype2 == "colorwallmounted" then + return math.floor(param2 / 8) * 8 + elseif paramtype2 == "colordegrotate" then + return math.floor(param2 / 32) * 32 + else return nil end - if paramtype2 == "colorfacedir" then - param2 = math.floor(param2 / 32) * 32 - elseif paramtype2 == "color4dir" then - param2 = math.floor(param2 / 4) * 4 - elseif paramtype2 == "colorwallmounted" then - param2 = math.floor(param2 / 8) * 8 - elseif paramtype2 == "colordegrotate" then - param2 = math.floor(param2 / 32) * 32 - end - -- paramtype2 == "color" requires no modification. - return param2 end -- Content ID caching diff --git a/builtin/common/misc_helpers.lua b/builtin/common/misc_helpers.lua index 9c25b826f..2ad9b10af 100644 --- a/builtin/common/misc_helpers.lua +++ b/builtin/common/misc_helpers.lua @@ -235,6 +235,16 @@ function core.formspec_escape(text) end +local hypertext_escapes = { + ["\\"] = "\\\\", + ["<"] = "\\<", + [">"] = "\\>", +} +function core.hypertext_escape(text) + return text and text:gsub("[\\<>]", hypertext_escapes) +end + + function core.wrap_text(text, max_length, as_table) local result = {} local line = {} diff --git a/builtin/fstk/tabview.lua b/builtin/fstk/tabview.lua index 9f8889143..42fc9ac18 100644 --- a/builtin/fstk/tabview.lua +++ b/builtin/fstk/tabview.lua @@ -66,13 +66,13 @@ local function get_formspec(self) local content, prepend = tab.get_formspec(self, tab.name, tab.tabdata, tab.tabsize) - local ENABLE_TOUCH = core.settings:get_bool("enable_touch") + local TOUCH_GUI = core.settings:get_bool("touch_gui") local orig_tsize = tab.tabsize or { width = self.width, height = self.height } local tsize = { width = orig_tsize.width, height = orig_tsize.height } tsize.height = tsize.height + TABHEADER_H -- tabheader included in formspec size - + (ENABLE_TOUCH and GAMEBAR_OFFSET_TOUCH or GAMEBAR_OFFSET_DESKTOP) + + (TOUCH_GUI and GAMEBAR_OFFSET_TOUCH or GAMEBAR_OFFSET_DESKTOP) + GAMEBAR_H -- gamebar included in formspec size if self.parent == nil and not prepend then diff --git a/builtin/game/features.lua b/builtin/game/features.lua index 81b291e6c..10884497c 100644 --- a/builtin/game/features.lua +++ b/builtin/game/features.lua @@ -44,6 +44,7 @@ core.features = { override_item_remove_fields = true, hotbar_hud_element = true, bulk_lbms = true, + abm_without_neighbors = true, } function core.has_feature(arg) diff --git a/builtin/mainmenu/content/contentdb.lua b/builtin/mainmenu/content/contentdb.lua index e0479cb4c..5d6d6c482 100644 --- a/builtin/mainmenu/content/contentdb.lua +++ b/builtin/mainmenu/content/contentdb.lua @@ -182,6 +182,23 @@ function contentdb.get_package_by_id(id) end +function contentdb.calculate_package_id(type, author, name) + local id = author:lower() .. "/" + if (type == nil or type == "game") and #name > 5 and name:sub(#name - 4) == "_game" then + id = id .. name:sub(1, #name - 5) + else + id = id .. name + end + return id +end + + +function contentdb.get_package_by_info(author, name) + local id = contentdb.calculate_package_id(nil, author, name) + return contentdb.package_by_id[id] +end + + -- Create a coroutine from `fn` and provide results to `callback` when complete (dead). -- Returns a resumer function. local function make_callback_coroutine(fn, callback) @@ -415,15 +432,7 @@ local function fetch_pkgs(params) local aliases = {} for _, package in pairs(packages) do - local name_len = #package.name - -- This must match what contentdb.update_paths() does! - package.id = package.author:lower() .. "/" - if package.type == "game" and name_len > 5 and package.name:sub(name_len - 4) == "_game" then - package.id = package.id .. package.name:sub(1, name_len - 5) - else - package.id = package.id .. package.name - end - + package.id = params.calculate_package_id(package.type, package.author, package.name) package.url_part = core.urlencode(package.author) .. "/" .. core.urlencode(package.name) if package.aliases then @@ -443,7 +452,7 @@ end function contentdb.fetch_pkgs(callback) contentdb.loading = true - core.handle_async(fetch_pkgs, nil, function(result) + core.handle_async(fetch_pkgs, { calculate_package_id = contentdb.calculate_package_id }, function(result) if result then contentdb.load_ok = true contentdb.load_error = false @@ -581,3 +590,78 @@ function contentdb.filter_packages(query, by_type) end end end + + +function contentdb.get_full_package_info(package, callback) + assert(package) + if package.full_info then + callback(package.full_info) + return + end + + local function fetch(params) + local version = core.get_version() + local base_url = core.settings:get("contentdb_url") + + local languages + local current_language = core.get_language() + if current_language ~= "" then + languages = { current_language, "en;q=0.8" } + else + languages = { "en" } + end + + local url = base_url .. + "/api/packages/" .. params.package.url_part .. "/for-client/?" .. + "protocol_version=" .. core.urlencode(core.get_max_supp_proto()) .. + "&engine_version=" .. core.urlencode(version.string) .. + "&formspec_version=" .. core.urlencode(core.get_formspec_version()) .. + "&include_images=false" + local http = core.get_http_api() + local response = http.fetch_sync({ + url = url, + extra_headers = { + "Accept-Language: " .. table.concat(languages, ", ") + }, + }) + if not response.succeeded then + return nil + end + + return core.parse_json(response.data) + end + + local function my_callback(value) + package.full_info = value + callback(value) + end + + if not core.handle_async(fetch, { package = package }, my_callback) then + core.log("error", "ERROR: async event failed") + callback(nil) + end +end + + +function contentdb.get_formspec_padding() + -- Padding is increased on Android to account for notches + -- TODO: use Android API to determine size of cut outs + return { x = PLATFORM == "Android" and 1 or 0.5, y = PLATFORM == "Android" and 0.25 or 0.5 } +end + + +function contentdb.get_formspec_size() + local window = core.get_window_info() + local size = { x = window.max_formspec_size.x, y = window.max_formspec_size.y } + + -- Minimum formspec size + local min_x = 15.5 + local min_y = 10 + if size.x < min_x or size.y < min_y then + local scale = math.max(min_x / size.x, min_y / size.y) + size.x = size.x * scale + size.y = size.y * scale + end + + return size +end diff --git a/builtin/mainmenu/content/dlg_contentdb.lua b/builtin/mainmenu/content/dlg_contentdb.lua index bcc89f7cd..8f232e490 100644 --- a/builtin/mainmenu/content/dlg_contentdb.lua +++ b/builtin/mainmenu/content/dlg_contentdb.lua @@ -26,68 +26,20 @@ end -- Filter local search_string = "" local cur_page = 1 -local num_per_page = 5 -local filter_type = 1 -local filter_types_titles = { - fgettext("All packages"), - fgettext("Games"), - fgettext("Mods"), - fgettext("Texture packs"), -} +local filter_type -- Automatic package installation local auto_install_spec = nil -local filter_types_type = { - nil, - "game", - "mod", - "txp", + +local filter_type_names = { + { "type_all", nil }, + { "type_game", "game" }, + { "type_mod", "mod" }, + { "type_txp", "txp" }, } -local function install_or_update_package(this, package) - local install_parent - if package.type == "mod" then - install_parent = core.get_modpath() - elseif package.type == "game" then - install_parent = core.get_gamepath() - elseif package.type == "txp" then - install_parent = core.get_texturepath() - else - error("Unknown package type: " .. package.type) - end - - if package.queued or package.downloading then - return - end - - local function on_confirm() - local dlg = create_install_dialog(package) - dlg:set_parent(this) - this:hide() - dlg:show() - - dlg:load_deps() - end - - if package.type == "mod" and #pkgmgr.games == 0 then - local dlg = messagebox("install_game", - fgettext("You need to install a game before you can install a mod")) - dlg:set_parent(this) - this:hide() - dlg:show() - elseif not package.path and core.is_dir(install_parent .. DIR_DELIM .. package.name) then - local dlg = create_confirm_overwrite(package, on_confirm) - dlg:set_parent(this) - this:hide() - dlg:show() - else - on_confirm() - end -end - - -- Resolves the package specification stored in auto_install_spec into an actual package. -- May only be called after the package list has been loaded successfully. local function resolve_auto_install_spec() @@ -145,7 +97,7 @@ end local function sort_and_filter_pkgs() contentdb.update_paths() contentdb.sort_packages() - contentdb.filter_packages(search_string, filter_types_type[filter_type]) + contentdb.filter_packages(search_string, filter_type) local auto_install_pkg = resolve_auto_install_spec() if auto_install_pkg then @@ -176,72 +128,151 @@ local function load() end -local function get_info_formspec(text) - local H = 9.5 +local function get_info_formspec(size, padding, text) return table.concat({ "formspec_version[6]", - "size[15.75,9.5]", - core.settings:get_bool("touch_gui") and "padding[0.01,0.01]" or "position[0.5,0.55]", + "size[", size.x, ",", size.y, "]", + "padding[0,0]", + "bgcolor[;true]", - "label[4,4.35;", text, "]", - "container[0,", H - 0.8 - 0.375, "]", - "button[0.375,0;5,0.8;back;", fgettext("Back to Main Menu"), "]", + "label[", padding.x + 3.625, ",4.35;", text, "]", + "container[", padding.x, ",", size.y - 0.8 - padding.y, "]", + "button[0,0;2,0.8;back;", fgettext("Back"), "]", "container_end[]", }) end +-- Determines how to fit `num_per_page` into `size` space +local function fit_cells(num_per_page, size) + local cell_spacing = 0.5 + local columns = 1 + local cell_w, cell_h + -- Fit cells into the available height + while true do + cell_w = (size.x - (columns-1)*cell_spacing) / columns + cell_h = cell_w / 4 + + local required_height = math.ceil(num_per_page / columns) * (cell_h + cell_spacing) - cell_spacing + -- Add 0.1 to be more lenient + if required_height <= size.y + 0.1 then + break + end + + columns = columns + 1 + end + + return cell_spacing, columns, cell_w, cell_h +end + + +local function calculate_num_per_page() + local size = contentdb.get_formspec_size() + local padding = contentdb.get_formspec_padding() + local window = core.get_window_info() + + size.x = size.x - padding.x * 2 + size.y = size.y - padding.y * 2 - 1.425 - 0.25 - 0.8 + + local coordToPx = window.size.x / window.max_formspec_size.x / window.real_gui_scaling + + local num_per_page = 12 + while num_per_page > 2 do + local _, _, cell_w, _ = fit_cells(num_per_page, size) + if cell_w * coordToPx > 350 then + break + end + + num_per_page = num_per_page - 1 + end + return num_per_page +end + + local function get_formspec(dlgdata) + local window_padding = contentdb.get_formspec_padding() + local size = contentdb.get_formspec_size() + if contentdb.loading then - return get_info_formspec(fgettext("Loading...")) + return get_info_formspec(size, window_padding, fgettext("Loading...")) end if contentdb.load_error then - return get_info_formspec(fgettext("No packages could be retrieved")) + return get_info_formspec(size, window_padding, fgettext("No packages could be retrieved")) end assert(contentdb.load_ok) contentdb.update_paths() + local num_per_page = dlgdata.num_per_page dlgdata.pagemax = math.max(math.ceil(#contentdb.packages / num_per_page), 1) if cur_page > dlgdata.pagemax then cur_page = 1 end - local W = 15.75 - local H = 9.5 + local W = size.x - window_padding.x * 2 + local H = size.y - window_padding.y * 2 + + local category_x = 0 + local number_category_buttons = 4 + local max_button_w = (W - 0.375 - 0.25 - 7) / number_category_buttons + local category_button_w = math.min(max_button_w, 3) + local function make_category_button(name, label, selected) + category_x = category_x + 1 + local color = selected and mt_color_green or "" + return ("style[%s;bgcolor=%s]button[%f,0;%f,0.8;%s;%s]"):format(name, color, + (category_x - 1) * category_button_w, category_button_w, name, label) + end + + + local selected_type = filter_type + + local search_box_width = W - 0.375 - 0.25 - 2*0.8 + - number_category_buttons * category_button_w local formspec = { - "formspec_version[6]", - "size[15.75,9.5]", - core.settings:get_bool("touch_gui") and "padding[0.01,0.01]" or "position[0.5,0.55]", + "formspec_version[7]", + "size[", size.x, ",", size.y, "]", + "padding[0,0]", + "bgcolor[;true]", - "style[status,downloading,queued;border=false]", + "container[", window_padding.x, ",", window_padding.y, "]", - "container[0.375,0.375]", - "field[0,0;7.225,0.8;search_string;;", core.formspec_escape(search_string), "]", + -- Top-left: categories + make_category_button("type_all", fgettext("All"), selected_type == nil), + make_category_button("type_game", fgettext("Games"), selected_type == "game"), + make_category_button("type_mod", fgettext("Mods"), selected_type == "mod"), + make_category_button("type_txp", fgettext("Texture Packs"), selected_type == "txp"), + + -- Top-right: Search + "container[", W - search_box_width - 0.8*2, ",0]", + "field[0,0;", search_box_width, ",0.8;search_string;;", core.formspec_escape(search_string), "]", "field_enter_after_edit[search_string;true]", - "image_button[7.3,0;0.8,0.8;", core.formspec_escape(defaulttexturedir .. "search.png"), ";search;]", - "image_button[8.125,0;0.8,0.8;", core.formspec_escape(defaulttexturedir .. "clear.png"), ";clear;]", - "dropdown[9.175,0;2.7875,0.8;type;", table.concat(filter_types_titles, ","), ";", filter_type, "]", + "image_button[", search_box_width, ",0;0.8,0.8;", + core.formspec_escape(defaulttexturedir .. "search.png"), ";search;]", + "image_button[", search_box_width + 0.8, ",0;0.8,0.8;", + core.formspec_escape(defaulttexturedir .. "clear.png"), ";clear;]", "container_end[]", - -- Page nav buttons - "container[0,", H - 0.8 - 0.375, "]", - "button[0.375,0;5,0.8;back;", fgettext("Back to Main Menu"), "]", + -- Bottom strip start + "container[0,", H - 0.8, "]", + "button[0,0;2,0.8;back;", fgettext("Back"), "]", - "container[", W - 0.375 - 0.8*4 - 2, ",0]", - "image_button[0,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "start_icon.png;pstart;]", - "image_button[0.8,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "prev_icon.png;pback;]", + -- Bottom-center: Page nav buttons + "container[", (W - 1*4 - 2) / 2, ",0]", + "image_button[0,0;1,0.8;", core.formspec_escape(defaulttexturedir), "start_icon.png;pstart;]", + "image_button[1,0;1,0.8;", core.formspec_escape(defaulttexturedir), "prev_icon.png;pback;]", "style[pagenum;border=false]", - "button[1.6,0;2,0.8;pagenum;", tonumber(cur_page), " / ", tonumber(dlgdata.pagemax), "]", - "image_button[3.6,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "next_icon.png;pnext;]", - "image_button[4.4,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "end_icon.png;pend;]", - "container_end[]", + "button[2,0;2,0.8;pagenum;", tonumber(cur_page), " / ", tonumber(dlgdata.pagemax), "]", + "image_button[4,0;1,0.8;", core.formspec_escape(defaulttexturedir), "next_icon.png;pnext;]", + "image_button[5,0;1,0.8;", core.formspec_escape(defaulttexturedir), "end_icon.png;pend;]", + "container_end[]", -- page nav end - "container_end[]", + -- Bottom-right: updating + "container[", W - 3, ",0]", + "style[status,downloading,queued;border=false]", } if contentdb.number_downloading > 0 then - formspec[#formspec + 1] = "button[12.5875,0.375;2.7875,0.8;downloading;" + formspec[#formspec + 1] = "button[0,0;3,0.8;downloading;" if #contentdb.download_queue > 0 then formspec[#formspec + 1] = fgettext("$1 downloading,\n$2 queued", contentdb.number_downloading, #contentdb.download_queue) @@ -260,16 +291,19 @@ local function get_formspec(dlgdata) end if num_avail_updates == 0 then - formspec[#formspec + 1] = "button[12.5875,0.375;2.7875,0.8;status;" + formspec[#formspec + 1] = "button[0,0;3,0.8;status;" formspec[#formspec + 1] = fgettext("No updates") formspec[#formspec + 1] = "]" else - formspec[#formspec + 1] = "button[12.5875,0.375;2.7875,0.8;update_all;" + formspec[#formspec + 1] = "button[0,0;3,0.8;update_all;" formspec[#formspec + 1] = fgettext("Update All [$1]", num_avail_updates) formspec[#formspec + 1] = "]" end end + formspec[#formspec + 1] = "container_end[]" -- updating end + formspec[#formspec + 1] = "container_end[]" -- bottom strip end + if #contentdb.packages == 0 then formspec[#formspec + 1] = "label[4,4.75;" formspec[#formspec + 1] = fgettext("No results") @@ -281,81 +315,85 @@ local function get_formspec(dlgdata) formspec[#formspec + 1] = "tooltip[downloading;" .. fgettext("Downloading...") .. tooltip_colors formspec[#formspec + 1] = "tooltip[queued;" .. fgettext("Queued") .. tooltip_colors + formspec[#formspec + 1] = "container[0,1.425]" + + local cell_spacing, columns, cell_w, cell_h = fit_cells(num_per_page, { + x = W, + y = H - 1.425 - 0.25 - 0.8 + }) + local img_w = cell_h * 3 / 2 + local start_idx = (cur_page - 1) * num_per_page + 1 for i=start_idx, math.min(#contentdb.packages, start_idx+num_per_page-1) do local package = contentdb.packages[i] - local container_y = (i - start_idx) * 1.375 + (2*0.375 + 0.8) - formspec[#formspec + 1] = "container[0.375," - formspec[#formspec + 1] = container_y - formspec[#formspec + 1] = "]" - -- image - formspec[#formspec + 1] = "image[0,0;1.5,1;" - formspec[#formspec + 1] = core.formspec_escape(get_screenshot(package)) - formspec[#formspec + 1] = "]" + table.insert_all(formspec, { + "container[", + (cell_w + cell_spacing) * ((i - start_idx) % columns), + ",", + (cell_h + cell_spacing) * math.floor((i - start_idx) / columns), + "]", - -- title - formspec[#formspec + 1] = "label[1.875,0.1;" - formspec[#formspec + 1] = core.formspec_escape( - core.colorize(mt_color_green, package.title) .. - core.colorize("#BFBFBF", " by " .. package.author)) - formspec[#formspec + 1] = "]" + "box[0,0;", cell_w, ",", cell_h, ";#ffffff11]", - -- buttons - local description_width = W - 2.625 - 2 * 0.7 - 2 * 0.15 + -- image, + "image[0,0;", img_w, ",", cell_h, ";", + core.formspec_escape(get_screenshot(package, package.thumbnail, 2)), "]", - local second_base = "image_button[-1.55,0;0.7,0.7;" .. core.formspec_escape(defaulttexturedir) - local third_base = "image_button[-2.4,0;0.7,0.7;" .. core.formspec_escape(defaulttexturedir) - formspec[#formspec + 1] = "container[" - formspec[#formspec + 1] = W - 0.375*2 - formspec[#formspec + 1] = ",0.1]" + "label[", img_w + 0.25 + 0.05, ",0.5;", + core.formspec_escape( + core.colorize(mt_color_green, package.title) .. + core.colorize("#BFBFBF", " by " .. package.author)), "]", - if package.downloading then - formspec[#formspec + 1] = "animated_image[-1.7,-0.15;1,1;downloading;" - formspec[#formspec + 1] = core.formspec_escape(defaulttexturedir) - formspec[#formspec + 1] = "cdb_downloading.png;3;400;]" - elseif package.queued then - formspec[#formspec + 1] = second_base - formspec[#formspec + 1] = "cdb_queued.png;queued;]" - elseif not package.path then - local elem_name = "install_" .. i .. ";" - formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#71aa34]" - formspec[#formspec + 1] = second_base .. "cdb_add.png;" .. elem_name .. "]" - formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Install") .. tooltip_colors - else - if package.installed_release < package.release then - -- The install_ action also handles updating - local elem_name = "install_" .. i .. ";" - formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#28ccdf]" - formspec[#formspec + 1] = third_base .. "cdb_update.png;" .. elem_name .. "]" - formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Update") .. tooltip_colors + "textarea[", img_w + 0.25, ",0.75;", cell_w - img_w - 0.25, ",", cell_h - 0.75, ";;;", + core.formspec_escape(package.short_description), "]", - description_width = description_width - 0.7 - 0.15 - end + "style[view_", i, ";border=false]", + "style[view_", i, ":hovered;bgimg=", core.formspec_escape(defaulttexturedir .. "button_hover_semitrans.png"), "]", + "style[view_", i, ":pressed;bgimg=", core.formspec_escape(defaulttexturedir .. "button_press_semitrans.png"), "]", + "button[0,0;", cell_w, ",", cell_h, ";view_", i, ";]", + }) - local elem_name = "uninstall_" .. i .. ";" - formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#a93b3b]" - formspec[#formspec + 1] = second_base .. "cdb_clear.png;" .. elem_name .. "]" - formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Uninstall") .. tooltip_colors + if package.featured then + table.insert_all(formspec, { + "tooltip[0,0;0.8,0.8;", fgettext("Featured"), "]", + "image[0.2,0.2;0.4,0.4;", defaulttexturedir, "server_favorite.png]", + }) end - local web_elem_name = "view_" .. i .. ";" - formspec[#formspec + 1] = "image_button[-0.7,0;0.7,0.7;" .. - core.formspec_escape(defaulttexturedir) .. "cdb_viewonline.png;" .. web_elem_name .. "]" - formspec[#formspec + 1] = "tooltip[" .. web_elem_name .. - fgettext("View more information in a web browser") .. tooltip_colors - formspec[#formspec + 1] = "container_end[]" + table.insert_all(formspec, { + "container[", cell_w - 0.625,",", 0.25, "]", + }) - -- description - formspec[#formspec + 1] = "textarea[1.855,0.3;" - formspec[#formspec + 1] = tostring(description_width) - formspec[#formspec + 1] = ",0.8;;;" - formspec[#formspec + 1] = core.formspec_escape(package.short_description) - formspec[#formspec + 1] = "]" + if package.downloading then + table.insert_all(formspec, { + "animated_image[0,0;0.5,0.5;downloading;", defaulttexturedir, "cdb_downloading.png;3;400;;]", + }) + elseif package.queued then + table.insert_all(formspec, { + "image[0,0;0.5,0.5;", defaulttexturedir, "cdb_queued.png]", + }) + elseif package.path then + if package.installed_release < package.release then + table.insert_all(formspec, { + "image[0,0;0.5,0.5;", defaulttexturedir, "cdb_update.png]", + }) + else + table.insert_all(formspec, { + "image[0.1,0.1;0.3,0.3;", defaulttexturedir, "checkbox_64.png]", + }) + end + end - formspec[#formspec + 1] = "container_end[]" + table.insert_all(formspec, { + "container_end[]", + "container_end[]", + }) end + formspec[#formspec + 1] = "container_end[]" + formspec[#formspec + 1] = "container_end[]" + return table.concat(formspec) end @@ -364,14 +402,14 @@ local function handle_submit(this, fields) if fields.search or fields.key_enter_field == "search_string" then search_string = fields.search_string:trim() cur_page = 1 - contentdb.filter_packages(search_string, filter_types_type[filter_type]) + contentdb.filter_packages(search_string, filter_type) return true end if fields.clear then search_string = "" cur_page = 1 - contentdb.filter_packages("", filter_types_type[filter_type]) + contentdb.filter_packages("", filter_type) return true end @@ -407,12 +445,11 @@ local function handle_submit(this, fields) return true end - if fields.type then - local new_type = table.indexof(filter_types_titles, fields.type) - if new_type ~= filter_type then - filter_type = new_type + for _, pair in ipairs(filter_type_names) do + if fields[pair[1]] then + filter_type = pair[2] cur_page = 1 - contentdb.filter_packages(search_string, filter_types_type[filter_type]) + contentdb.filter_packages(search_string, filter_type) return true end end @@ -428,32 +465,20 @@ local function handle_submit(this, fields) return true end + local num_per_page = this.data.num_per_page local start_idx = (cur_page - 1) * num_per_page + 1 assert(start_idx ~= nil) for i=start_idx, math.min(#contentdb.packages, start_idx+num_per_page-1) do local package = contentdb.packages[i] assert(package) - if fields["install_" .. i] then - install_or_update_package(this, package) - return true - end - - if fields["uninstall_" .. i] then - local dlg = create_delete_content_dlg(package) + if fields["view_" .. i] or fields["title_" .. i] or fields["author_" .. i] then + local dlg = create_package_dialog(package) dlg:set_parent(this) this:hide() dlg:show() return true end - - if fields["view_" .. i] then - local url = ("%s/packages/%s?protocol_version=%d"):format( - core.settings:get("contentdb_url"), package.url_part, - core.get_max_supp_proto()) - core.open_url(url) - return true - end end return false @@ -462,8 +487,8 @@ end local function handle_events(event) if event == "DialogShow" then - -- On touchscreen, don't show the "MINETEST" header behind the dialog. - mm_game_theme.set_engine(core.settings:get_bool("touch_gui")) + -- Don't show the "MINETEST" header behind the dialog. + mm_game_theme.set_engine(true) -- If ContentDB is already loaded, auto-install packages here. do_auto_install() @@ -471,6 +496,11 @@ local function handle_events(event) return true end + if event == "WindowInfoChange" then + ui.update() + return true + end + return false end @@ -485,17 +515,7 @@ end function create_contentdb_dlg(type, install_spec) search_string = "" cur_page = 1 - if type then - -- table.indexof does not work on tables that contain `nil` - for i, v in pairs(filter_types_type) do - if v == type then - filter_type = i - break - end - end - else - filter_type = 1 - end + filter_type = type -- Keep the old auto_install_spec if the caller doesn't specify one. if install_spec then @@ -504,8 +524,10 @@ function create_contentdb_dlg(type, install_spec) load() - return dialog_create("contentdb", + local dlg = dialog_create("contentdb", get_formspec, handle_submit, handle_events) + dlg.data.num_per_page = calculate_num_per_page() + return dlg end diff --git a/builtin/mainmenu/content/dlg_install.lua b/builtin/mainmenu/content/dlg_install.lua index 89819be2a..3f43bd23c 100644 --- a/builtin/mainmenu/content/dlg_install.lua +++ b/builtin/mainmenu/content/dlg_install.lua @@ -244,3 +244,45 @@ function create_install_dialog(package) return dlg end + + +function install_or_update_package(parent, package) + local install_parent + if package.type == "mod" then + install_parent = core.get_modpath() + elseif package.type == "game" then + install_parent = core.get_gamepath() + elseif package.type == "txp" then + install_parent = core.get_texturepath() + else + error("Unknown package type: " .. package.type) + end + + if package.queued or package.downloading then + return + end + + local function on_confirm() + local dlg = create_install_dialog(package) + dlg:set_parent(parent) + parent:hide() + dlg:show() + + dlg:load_deps() + end + + if package.type == "mod" and #pkgmgr.games == 0 then + local dlg = messagebox("install_game", + fgettext("You need to install a game before you can install a mod")) + dlg:set_parent(parent) + parent:hide() + dlg:show() + elseif not package.path and core.is_dir(install_parent .. DIR_DELIM .. package.name) then + local dlg = create_confirm_overwrite(package, on_confirm) + dlg:set_parent(parent) + parent:hide() + dlg:show() + else + on_confirm() + end +end diff --git a/builtin/mainmenu/content/dlg_package.lua b/builtin/mainmenu/content/dlg_package.lua new file mode 100644 index 000000000..404e950c4 --- /dev/null +++ b/builtin/mainmenu/content/dlg_package.lua @@ -0,0 +1,333 @@ +--Minetest +--Copyright (C) 2018-24 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 function get_info_formspec(size, padding, text) + return table.concat({ + "formspec_version[6]", + "size[", size.x, ",", size.y, "]", + "padding[0,0]", + "bgcolor[;true]", + + "label[4,4.35;", text, "]", + "container[", padding.x, ",", size.y - 0.8 - padding.y, "]", + "button[0,0;2,0.8;back;", fgettext("Back"), "]", + "container_end[]", + }) +end + + +local function get_formspec(data) + local window_padding = contentdb.get_formspec_padding() + local size = contentdb.get_formspec_size() + size.x = math.min(size.x, 20) + local W = size.x - window_padding.x * 2 + local H = size.y - window_padding.y * 2 + + if not data.info then + if not data.loading and not data.loading_error then + data.loading = true + + contentdb.get_full_package_info(data.package, function(info) + data.loading = false + + if info == nil then + data.loading_error = true + ui.update() + return + end + + if info.forums then + info.forums = "https://forum.minetest.net/viewtopic.php?t=" .. info.forums + end + + assert(data.package.name == info.name) + data.info = info + ui.update() + end) + end + + -- get_full_package_info can return cached info immediately, so + -- check to see if that happened + if not data.info then + if data.loading_error then + return get_info_formspec(size, window_padding, fgettext("No packages could be retrieved")) + end + return get_info_formspec(size, window_padding, fgettext("Loading...")) + end + end + + -- Check installation status + contentdb.update_paths() + + local info = data.info + + local info_line = + fgettext("by $1 — $2 downloads — +$3 / $4 / -$5", + info.author, info.downloads, + info.reviews.positive, info.reviews.neutral, info.reviews.negative) + + local bottom_buttons_y = H - 0.8 + + local formspec = { + "formspec_version[7]", + "size[", size.x, ",", size.y, "]", + "padding[0,0]", + "bgcolor[;true]", + + "container[", window_padding.x, ",", window_padding.y, "]", + + "button[0,", bottom_buttons_y, ";2,0.8;back;", fgettext("Back"), "]", + "button[", W - 3, ",", bottom_buttons_y, ";3,0.8;open_contentdb;", fgettext("ContentDB page"), "]", + + "style_type[label;font_size=+24;font=bold]", + "label[0,0.4;", core.formspec_escape(info.title), "]", + "style_type[label;font_size=;font=]", + + "label[0,1.2;", core.formspec_escape(info_line), "]", + } + + table.insert_all(formspec, { + "container[", W - 6, ",0]" + }) + + local left_button_rect = "0,0;2.875,1" + local right_button_rect = "3.125,0;2.875,1" + if data.package.downloading then + formspec[#formspec + 1] = "animated_image[5,0;1,1;downloading;" + formspec[#formspec + 1] = core.formspec_escape(defaulttexturedir) + formspec[#formspec + 1] = "cdb_downloading.png;3;400;]" + elseif data.package.queued then + formspec[#formspec + 1] = "style[queued;border=false]" + formspec[#formspec + 1] = "image_button[5,0;1,1;" .. core.formspec_escape(defaulttexturedir) + formspec[#formspec + 1] = "cdb_queued.png;queued;]" + elseif not data.package.path then + formspec[#formspec + 1] = "style[install;bgcolor=green]" + formspec[#formspec + 1] = "button[" + formspec[#formspec + 1] = right_button_rect + formspec[#formspec + 1] =";install;" + formspec[#formspec + 1] = fgettext("Install [$1]", info.download_size) + formspec[#formspec + 1] = "]" + else + if data.package.installed_release < data.package.release then + -- The install_ action also handles updating + formspec[#formspec + 1] = "style[install;bgcolor=#28ccdf]" + formspec[#formspec + 1] = "button[" + formspec[#formspec + 1] = left_button_rect + formspec[#formspec + 1] = ";install;" + formspec[#formspec + 1] = fgettext("Update") + formspec[#formspec + 1] = "]" + end + + formspec[#formspec + 1] = "style[uninstall;bgcolor=#a93b3b]" + formspec[#formspec + 1] = "button[" + formspec[#formspec + 1] = right_button_rect + formspec[#formspec + 1] = ";uninstall;" + formspec[#formspec + 1] = fgettext("Uninstall") + formspec[#formspec + 1] = "]" + end + + local current_tab = data.current_tab or 1 + local tab_titles = { + fgettext("Description"), + fgettext("Information"), + } + + local tab_body_height = bottom_buttons_y - 2.8 + + table.insert_all(formspec, { + "container_end[]", + + "box[0,2.55;", W, ",", tab_body_height, ";#ffffff11]", + + "tabheader[0,2.55;", W, ",0.8;tabs;", + table.concat(tab_titles, ","), ";", current_tab, ";true;true]", + + "container[0,2.8]", + }) + + if current_tab == 1 then + -- Screenshots and description + local hypertext = "" .. core.hypertext_escape(info.short_description) .. "\n" + local winfo = core.get_window_info() + local fs_to_px = winfo.size.x / winfo.max_formspec_size.x + for i, ss in ipairs(info.screenshots) do + local path = get_screenshot(data.package, ss.url, 2) + hypertext = hypertext .. "" + if i ~= #info.screenshots then + hypertext = hypertext .. "" + end + end + hypertext = hypertext .. "\n" .. info.long_description.head + + local first = true + local function add_link_button(label, name) + if info[name] then + if not first then + hypertext = hypertext .. " | " + end + hypertext = hypertext .. "" .. core.hypertext_escape(label) .. "" + info.long_description.links["link_" .. name] = info[name] + first = false + end + end + + add_link_button(fgettext("Donate"), "donate_url") + add_link_button(fgettext("Website"), "website") + add_link_button(fgettext("Source"), "repo") + add_link_button(fgettext("Issue Tracker"), "issue_tracker") + add_link_button(fgettext("Translate"), "translation_url") + add_link_button(fgettext("Forum Topic"), "forums") + + hypertext = hypertext .. "\n\n" .. info.long_description.body + + hypertext = hypertext:gsub("= visible_l) - local max = total_l - visible_l - local thumb_size = (visible_l / total_l) * max - return ("scrollbaroptions[min=0;max=%f;thumbsize=%f]"):format(max / scroll_factor, thumb_size / scroll_factor) -end - - local formspec_show_hack = false @@ -507,8 +520,8 @@ local function get_formspec(dialogdata) "tooltip[search;", fgettext("Search"), "]", "tooltip[search_clear;", fgettext("Clear"), "]", "container_end[]", - "scroll_container[0.25,1.25;", tostring(left_pane_width), ",", - tostring(tabsize.height - 1.5), ";leftscroll;vertical;0.1]", + ("scroll_container[0.25,1.25;%f,%f;leftscroll;vertical;0.1;0]"):format( + left_pane_width, tabsize.height - 1.5), "style_type[button;border=false;bgcolor=#3333]", "style_type[button:hover;border=false;bgcolor=#6663]", } @@ -538,7 +551,6 @@ local function get_formspec(dialogdata) fs[#fs + 1] = "scroll_container_end[]" if y >= tabsize.height - 1.25 then - fs[#fs + 1] = make_scrollbaroptions_for_scroll_container(tabsize.height - 1.5, y, 0.1) fs[#fs + 1] = ("scrollbar[%f,1.25;%f,%f;vertical;leftscroll;%f]"):format( left_pane_width + 0.25, scrollbar_w, tabsize.height - 1.5, dialogdata.leftscroll or 0) end @@ -550,7 +562,7 @@ local function get_formspec(dialogdata) end local right_pane_width = tabsize.width - left_pane_width - 0.375 - 2*scrollbar_w - 0.25 - fs[#fs + 1] = ("scroll_container[%f,0;%f,%f;rightscroll;vertical;0.1]"):format( + fs[#fs + 1] = ("scroll_container[%f,0;%f,%f;rightscroll;vertical;0.1;0.25]"):format( tabsize.width - right_pane_width - scrollbar_w, right_pane_width, tabsize.height) y = 0.25 @@ -606,7 +618,6 @@ local function get_formspec(dialogdata) fs[#fs + 1] = "scroll_container_end[]" if y >= tabsize.height then - fs[#fs + 1] = make_scrollbaroptions_for_scroll_container(tabsize.height, y + 0.375, 0.1) fs[#fs + 1] = ("scrollbar[%f,0;%f,%f;vertical;rightscroll;%f]"):format( tabsize.width - scrollbar_w, scrollbar_w, tabsize.height, dialogdata.rightscroll or 0) end @@ -624,6 +635,18 @@ function write_settings_early() end end +local function regenerate_page_list(dialogdata) + local suggested_page_id = update_filtered_pages(dialogdata.query) + + dialogdata.components = nil + + if not filtered_page_by_id[dialogdata.page_id] then + dialogdata.leftscroll = 0 + dialogdata.rightscroll = 0 + + dialogdata.page_id = suggested_page_id + end +end local function buttonhandler(this, fields) local dialogdata = this.data @@ -648,27 +671,7 @@ local function buttonhandler(this, fields) local value = core.is_yes(fields.show_advanced) core.settings:set_bool("show_advanced", value) write_settings_early() - end - - -- touch_controls is a checkbox in a setting component. We handle this - -- setting differently so we can hide/show pages using the next if-statement - if fields.touch_controls ~= nil then - local value = core.is_yes(fields.touch_controls) - core.settings:set_bool("touch_controls", value) - write_settings_early() - end - - if fields.show_advanced ~= nil or fields.touch_controls ~= nil then - local suggested_page_id = update_filtered_pages(dialogdata.query) - - dialogdata.components = nil - - if not filtered_page_by_id[dialogdata.page_id] then - dialogdata.leftscroll = 0 - dialogdata.rightscroll = 0 - - dialogdata.page_id = suggested_page_id - end + regenerate_page_list(dialogdata) return true end @@ -701,20 +704,26 @@ local function buttonhandler(this, fields) end end - for i, comp in ipairs(dialogdata.components) do - if comp.on_submit and comp:on_submit(fields, this) then - write_settings_early() - + local function after_setting_change(comp) + write_settings_early() + if comp.setting.name == "touch_controls" then + -- Changing the "touch_controls" setting may result in a different + -- page list. + regenerate_page_list(dialogdata) + else -- Clear components so they regenerate dialogdata.components = nil + end + end + + for i, comp in ipairs(dialogdata.components) do + if comp.on_submit and comp:on_submit(fields, this) then + after_setting_change(comp) return true end if comp.setting and fields["reset_" .. i] then core.settings:remove(comp.setting.name) - write_settings_early() - - -- Clear components so they regenerate - dialogdata.components = nil + after_setting_change(comp) return true end end diff --git a/builtin/mainmenu/tab_about.lua b/builtin/mainmenu/tab_about.lua index 0394ea507..ab3edbddc 100644 --- a/builtin/mainmenu/tab_about.lua +++ b/builtin/mainmenu/tab_about.lua @@ -19,12 +19,7 @@ local function prepare_credits(dest, source) local string = table.concat(source, "\n") .. "\n" - local hypertext_escapes = { - ["\\"] = "\\\\", - ["<"] = "\\<", - [">"] = "\\>", - } - string = string:gsub("[\\<>]", hypertext_escapes) + string = core.hypertext_escape(string) string = string:gsub("%[.-%]", "%1") table.insert(dest, string) diff --git a/builtin/mainmenu/tab_local.lua b/builtin/mainmenu/tab_local.lua index f0a7255d7..8d807cc79 100644 --- a/builtin/mainmenu/tab_local.lua +++ b/builtin/mainmenu/tab_local.lua @@ -92,11 +92,11 @@ function singleplayer_refresh_gamebar() end end - local ENABLE_TOUCH = core.settings:get_bool("enable_touch") + local TOUCH_GUI = core.settings:get_bool("touch_gui") local gamebar_pos_y = MAIN_TAB_H + TABHEADER_H -- tabheader included in formspec size - + (ENABLE_TOUCH and GAMEBAR_OFFSET_TOUCH or GAMEBAR_OFFSET_DESKTOP) + + (TOUCH_GUI and GAMEBAR_OFFSET_TOUCH or GAMEBAR_OFFSET_DESKTOP) local btnbar = buttonbar_create( "game_button_bar", diff --git a/builtin/settingtypes.txt b/builtin/settingtypes.txt index da3a23324..02420eb05 100644 --- a/builtin/settingtypes.txt +++ b/builtin/settingtypes.txt @@ -61,7 +61,7 @@ # # # This is a comment # # -# # Requires: shaders, enable_dynamic_shadows, !touch_controls +# # Requires: shaders, enable_dynamic_shadows, !enable_waving_leaves # name (Readable name) type type_args # # A requirement can be the name of a boolean setting or an engine-defined value. @@ -72,6 +72,7 @@ # * shaders_support (a video driver that supports shaders, may not be enabled) # * shaders (both enable_shaders and shaders_support) # * desktop / android +# * touchscreen / keyboard_mouse # * opengl / gles # * You can negate any requirement by prepending with ! # @@ -91,7 +92,7 @@ camera_smoothing (Camera smoothing) float 0.0 0.0 0.99 # Smooths rotation of camera when in cinematic mode, 0 to disable. Enter cinematic mode by using the key set in Controls. # -# Requires: !touch_controls +# Requires: keyboard_mouse cinematic_camera_smoothing (Camera smoothing in cinematic mode) float 0.7 0.0 0.99 # If enabled, you can place nodes at the position (feet + eye level) where you stand. @@ -112,8 +113,8 @@ always_fly_fast (Always fly fast) bool true # The time in seconds it takes between repeated node placements when holding # the place button. # -# Requires: !touch_controls -repeat_place_time (Place repetition interval) float 0.25 0.16 2.0 +# Requires: keyboard_mouse +repeat_place_time (Place repetition interval) float 0.25 0.15 2.0 # The minimum time in seconds it takes between digging nodes when holding # the dig button. @@ -131,60 +132,62 @@ safe_dig_and_place (Safe digging and placing) bool false # Invert vertical mouse movement. # -# Requires: !touch_controls +# Requires: keyboard_mouse invert_mouse (Invert mouse) bool false # Mouse sensitivity multiplier. # -# Requires: !touch_controls +# Requires: keyboard_mouse mouse_sensitivity (Mouse sensitivity) float 0.2 0.001 10.0 # Enable mouse wheel (scroll) for item selection in hotbar. # -# Requires: !touch_controls +# Requires: keyboard_mouse enable_hotbar_mouse_wheel (Hotbar: Enable mouse wheel for selection) bool true # Invert mouse wheel (scroll) direction for item selection in hotbar. # -# Requires: !touch_controls +# Requires: keyboard_mouse invert_hotbar_mouse_wheel (Hotbar: Invert mouse wheel direction) bool false [*Touchscreen] # Enables the touchscreen controls, allowing you to play the game with a touchscreen. -touch_controls (Enable touchscreen controls) bool true +# "auto" means that the touchscreen controls will be enabled and disabled +# automatically depending on the last used input method. +touch_controls (Touchscreen controls) enum auto auto,true,false # Touchscreen sensitivity multiplier. # -# Requires: touch_controls +# Requires: touchscreen touchscreen_sensitivity (Touchscreen sensitivity) float 0.2 0.001 10.0 # The length in pixels after which a touch interaction is considered movement. # -# Requires: touch_controls +# Requires: touchscreen touchscreen_threshold (Movement threshold) int 20 0 100 # The delay in milliseconds after which a touch interaction is considered a long tap. # -# Requires: touch_controls +# Requires: touchscreen touch_long_tap_delay (Threshold for long taps) int 400 100 1000 # Use crosshair to select object instead of whole screen. # If enabled, a crosshair will be shown and will be used for selecting object. # -# Requires: touch_controls +# Requires: touchscreen touch_use_crosshair (Use crosshair for touch screen) bool false # Fixes the position of virtual joystick. # If disabled, virtual joystick will center to first-touch's position. # -# Requires: touch_controls +# Requires: touchscreen fixed_virtual_joystick (Fixed virtual joystick) bool false # Use virtual joystick to trigger "Aux1" button. # If enabled, virtual joystick will also tap "Aux1" button when out of main circle. # -# Requires: touch_controls +# Requires: touchscreen virtual_joystick_triggers_aux1 (Virtual joystick triggers Aux1 button) bool false # The gesture for punching players/entities. @@ -197,7 +200,7 @@ virtual_joystick_triggers_aux1 (Virtual joystick triggers Aux1 button) bool fals # Known from the classic Minetest mobile controls. # Combat is more or less impossible. # -# Requires: touch_controls +# Requires: touchscreen touch_punch_gesture (Punch gesture) enum short_tap short_tap,long_tap @@ -262,31 +265,6 @@ viewing_range (Viewing range) int 190 20 4000 # Higher values result in a less detailed image. undersampling (Undersampling) int 1 1 8 -[**Graphics Effects] - -# Allows liquids to be translucent. -translucent_liquids (Translucent liquids) bool true - -# Leaves style: -# - Fancy: all faces visible -# - Simple: only outer faces, if defined special_tiles are used -# - Opaque: disable transparency -leaves_style (Leaves style) enum fancy fancy,simple,opaque - -# Connects glass if supported by node. -connected_glass (Connect glass) bool false - -# Enable smooth lighting with simple ambient occlusion. -# Disable for speed or for different looks. -smooth_lighting (Smooth lighting) bool true - -# Enables tradeoffs that reduce CPU load or increase rendering performance -# at the expense of minor visual glitches that do not impact game playability. -performance_tradeoffs (Tradeoffs for performance) bool false - -# Adds particles when digging a node. -enable_particles (Digging particles) bool true - [**3D] # 3D support. @@ -466,13 +444,29 @@ enable_raytraced_culling (Enable Raytraced Culling) bool true -[*Shaders] +[*Effects] -# Shaders allow advanced visual effects and may increase performance on some video -# cards. -# -# Requires: shaders_support -enable_shaders (Shaders) bool true +# Allows liquids to be translucent. +translucent_liquids (Translucent liquids) bool true + +# Leaves style: +# - Fancy: all faces visible +# - Simple: only outer faces +# - Opaque: disable transparency +leaves_style (Leaves style) enum fancy fancy,simple,opaque + +# Connects glass if supported by node. +connected_glass (Connect glass) bool false + +# Enable smooth lighting with simple ambient occlusion. +smooth_lighting (Smooth lighting) bool true + +# Enables tradeoffs that reduce CPU load or increase rendering performance +# at the expense of minor visual glitches that do not impact game playability. +performance_tradeoffs (Tradeoffs for performance) bool false + +# Adds particles when digging a node. +enable_particles (Digging particles) bool true [**Waving Nodes] @@ -649,42 +643,12 @@ enable_vignette (Vignette) bool false # Requires: shaders, enable_post_processing debanding (Enable Debanding) bool true -[**Bloom] - # Set to true to enable bloom effect. # Bright colors will bleed over the neighboring objects. # # Requires: shaders, enable_post_processing enable_bloom (Enable Bloom) bool false -# Set to true to render debugging breakdown of the bloom effect. -# In debug mode, the screen is split into 4 quadrants: -# top-left - processed base image, top-right - final image -# bottom-left - raw base image, bottom-right - bloom texture. -# -# Requires: shaders, enable_post_processing, enable_bloom -enable_bloom_debug (Enable Bloom Debug) bool false - -# Defines how much bloom is applied to the rendered image -# Smaller values make bloom more subtle -# Range: from 0.01 to 1.0, default: 0.05 -# -# Requires: shaders, enable_post_processing, enable_bloom -bloom_intensity (Bloom Intensity) float 0.05 0.01 1.0 - -# Defines the magnitude of bloom overexposure. -# Range: from 0.1 to 10.0, default: 1.0 -# -# Requires: shaders, enable_post_processing, enable_bloom -bloom_strength_factor (Bloom Strength Factor) float 1.0 0.1 10.0 - -# Logical value that controls how far the bloom effect spreads -# from the bright objects. -# Range: from 0.1 to 8, default: 1 -# -# Requires: shaders, enable_post_processing, enable_bloom -bloom_radius (Bloom Radius) float 1 0.1 8 - # Set to true to enable volumetric lighting effect (a.k.a. "Godrays"). # # Requires: shaders, enable_post_processing, enable_bloom @@ -713,6 +677,11 @@ enable_translucent_foliage (Translucent foliage) bool false # Requires: shaders, enable_dynamic_shadows enable_node_specular (Node specular) bool false +# When enabled, liquid reflections are simulated. +# +# Requires: shaders, enable_waving_water, enable_dynamic_shadows +enable_water_reflections (Liquid reflections) bool false + [*Audio] # Volume of all sounds. @@ -950,8 +919,13 @@ default_privs (Default privileges) string interact, shout # Privileges that players with basic_privs can grant basic_privs (Basic privileges) string interact, shout -# If enabled, disable cheat prevention in multiplayer. -disable_anticheat (Disable anticheat) bool false +# Server anticheat configuration. +# Flags are positive. Uncheck the flag to disable corresponding anticheat module. +anticheat_flags (Anticheat flags) flags digging,interaction,movement digging,interaction,movement + +# Tolerance of movement cheat detector. +# Increase the value if players experience stuttery movement. +anticheat_movement_tolerance (Anticheat movement tolerance) float 1.0 1.0 # If enabled, actions are recorded for rollback. # This option is only read when server starts. @@ -1896,6 +1870,11 @@ ignore_world_load_errors (Ignore world errors) bool false [**Graphics] +# Shaders are a fundamental part of rendering and enable advanced visual effects. +# +# Requires: shaders_support +enable_shaders (Shaders) bool true + # Path to shader directory. If no path is defined, default location will be used. # # Requires: shaders @@ -1919,6 +1898,7 @@ cloud_radius (Cloud radius) int 12 1 62 desynchronize_mapblock_texture_animation (Desynchronize block animation) bool false # Enables caching of facedir rotated meshes. +# This is only effective with shaders disabled. enable_mesh_cache (Mesh cache) bool false # Delay between mesh updates on the client in ms. Increasing this will slow @@ -1970,6 +1950,14 @@ client_mesh_chunk (Client Mesh Chunksize) int 1 1 16 # Enables debug and error-checking in the OpenGL driver. opengl_debug (OpenGL debug) bool false +# Set to true to render debugging breakdown of the bloom effect. +# In debug mode, the screen is split into 4 quadrants: +# top-left - processed base image, top-right - final image +# bottom-left - raw base image, bottom-right - bloom texture. +# +# Requires: shaders, enable_post_processing, enable_bloom +enable_bloom_debug (Enable Bloom Debug) bool false + [**Sound] # Comma-separated list of AL and ALC extensions that should not be used. # Useful for testing. See al_extensions.[h,cpp] for details. diff --git a/client/shaders/cloud_shader/opengl_vertex.glsl b/client/shaders/cloud_shader/opengl_vertex.glsl index ebf4aae49..92f5de64b 100644 --- a/client/shaders/cloud_shader/opengl_vertex.glsl +++ b/client/shaders/cloud_shader/opengl_vertex.glsl @@ -8,11 +8,7 @@ void main(void) { gl_Position = mWorldViewProj * inVertexPosition; -#ifdef GL_ES - vec4 color = inVertexColor.bgra; -#else vec4 color = inVertexColor; -#endif color *= materialColor; varColor = color; diff --git a/client/shaders/default_shader/opengl_vertex.glsl b/client/shaders/default_shader/opengl_vertex.glsl index a908ac953..d95a3c2d3 100644 --- a/client/shaders/default_shader/opengl_vertex.glsl +++ b/client/shaders/default_shader/opengl_vertex.glsl @@ -3,9 +3,5 @@ varying lowp vec4 varColor; void main(void) { gl_Position = mWorldViewProj * inVertexPosition; -#ifdef GL_ES - varColor = inVertexColor.bgra; -#else varColor = inVertexColor; -#endif } diff --git a/client/shaders/minimap_shader/opengl_vertex.glsl b/client/shaders/minimap_shader/opengl_vertex.glsl index b23d27181..1a9491805 100644 --- a/client/shaders/minimap_shader/opengl_vertex.glsl +++ b/client/shaders/minimap_shader/opengl_vertex.glsl @@ -7,9 +7,5 @@ void main(void) { varTexCoord = inTexCoord0.st; gl_Position = mWorldViewProj * inVertexPosition; -#ifdef GL_ES - varColor = inVertexColor.bgra; -#else varColor = inVertexColor; -#endif } diff --git a/client/shaders/nodes_shader/opengl_vertex.glsl b/client/shaders/nodes_shader/opengl_vertex.glsl index 30205d3cd..62de02aef 100644 --- a/client/shaders/nodes_shader/opengl_vertex.glsl +++ b/client/shaders/nodes_shader/opengl_vertex.glsl @@ -211,15 +211,11 @@ void main(void) vNormal = inVertexNormal; // Calculate color. + vec4 color = inVertexColor; // Red, green and blue components are pre-multiplied with // the brightness, so now we have to multiply these // colors with the color of the incoming light. // The pre-baked colors are halved to prevent overflow. -#ifdef GL_ES - vec4 color = inVertexColor.bgra; -#else - vec4 color = inVertexColor; -#endif // The alpha gives the ratio of sunlight in the incoming light. nightRatio = 1.0 - color.a; color.rgb = color.rgb * (color.a * dayLight.rgb + diff --git a/client/shaders/object_shader/opengl_vertex.glsl b/client/shaders/object_shader/opengl_vertex.glsl index b29269281..375918904 100644 --- a/client/shaders/object_shader/opengl_vertex.glsl +++ b/client/shaders/object_shader/opengl_vertex.glsl @@ -124,11 +124,7 @@ void main(void) : directional_ambient(normalize(inVertexNormal)); #endif -#ifdef GL_ES - vec4 color = inVertexColor.bgra; -#else vec4 color = inVertexColor; -#endif color *= materialColor; diff --git a/client/shaders/selection_shader/opengl_vertex.glsl b/client/shaders/selection_shader/opengl_vertex.glsl index 39dde3056..9ca87a9cf 100644 --- a/client/shaders/selection_shader/opengl_vertex.glsl +++ b/client/shaders/selection_shader/opengl_vertex.glsl @@ -6,9 +6,5 @@ void main(void) varTexCoord = inTexCoord0.st; gl_Position = mWorldViewProj * inVertexPosition; -#ifdef GL_ES - varColor = inVertexColor.bgra; -#else varColor = inVertexColor; -#endif } diff --git a/doc/lua_api.md b/doc/lua_api.md index 66a83542e..f2f0a5ba3 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -274,7 +274,7 @@ Accepted formats are: images: .png, .jpg, .tga, (deprecated:) .bmp sounds: .ogg vorbis - models: .x, .b3d, .obj, .gltf (Minetest 5.10 or newer) + models: .x, .b3d, .obj, (since version 5.10:) .gltf, .glb Other formats won't be sent to the client (e.g. you can store .blend files in a folder for convenience, without the risk that such files are transferred) @@ -294,7 +294,7 @@ depends on by supplying a file with an equal name. Only a subset of model file format features is supported: Simple textured meshes (with multiple textures), optionally with normals. -The .x and .b3d formats additionally support skeletal animation. +The .x, .b3d and .gltf formats additionally support (a single) animation. #### glTF @@ -302,9 +302,15 @@ The glTF model file format for now only serves as a more modern alternative to the other static model file formats; it unlocks no special rendering features. +Binary glTF (`.glb`) files are supported and recommended over `.gltf` files +due to their space savings. + This means that many glTF features are not supported *yet*, including: -* Animation +* Animations + * Only a single animation is supported, + use frame ranges within this animation. + * Only integer frames are supported. * Cameras * Materials * Only base color textures are supported @@ -490,6 +496,11 @@ to let the client generate textures on-the-fly. The modifiers are applied directly in sRGB colorspace, i.e. without gamma-correction. +### Notes + + * `TEXMOD_UPSCALE`: The texture with the lower resolution will be automatically + upscaled to the higher resolution texture. + ### Texture overlaying Textures can be overlaid by putting a `^` between them. @@ -503,8 +514,9 @@ Example: default_dirt.png^default_grass_side.png `default_grass_side.png` is overlaid over `default_dirt.png`. -The texture with the lower resolution will be automatically upscaled to -the higher resolution texture. + +*See notes: `TEXMOD_UPSCALE`* + ### Texture grouping @@ -701,6 +713,8 @@ Apply a mask to the base image. The mask is applied using binary AND. +*See notes: `TEXMOD_UPSCALE`* + #### `[sheet:x:,` Retrieves a tile at position x, y (in tiles, 0-indexed) @@ -798,6 +812,8 @@ in GIMP. Overlay is the same as Hard light but with the role of the two textures swapped, see the `[hardlight` modifier description for more detail about these blend modes. +*See notes: `TEXMOD_UPSCALE`* + #### `[hardlight:` Applies a Hard light blend with the two textures, like the Hard light layer @@ -813,6 +829,8 @@ increase contrast without clipping. Hard light is the same as Overlay but with the roles of the two textures swapped, i.e. `A.png^[hardlight:B.png` is the same as `B.png^[overlay:A.png` +*See notes: `TEXMOD_UPSCALE`* + #### `[png:` Embed a base64 encoded PNG image in the texture string. @@ -831,6 +849,8 @@ In particular consider `minetest.dynamic_add_media` and test whether using other texture modifiers could result in a shorter string than embedding a whole image, this may vary by use case. +*See notes: `TEXMOD_UPSCALE`* + Hardware coloring ----------------- @@ -1394,16 +1414,19 @@ The function of `param2` is determined by `paramtype2` in node definition. The palette should have 256 pixels. * `paramtype2 = "colorfacedir"` * Same as `facedir`, but with colors. - * The first three bits of `param2` tells which color is picked from the + * The three most significant bits of `param2` tells which color is picked from the palette. The palette should have 8 pixels. + * The five least significant bits contain the `facedir` value. * `paramtype2 = "color4dir"` - * Same as `facedir`, but with colors. - * The first six bits of `param2` tells which color is picked from the + * Same as `4dir`, but with colors. + * The six most significant bits of `param2` tells which color is picked from the palette. The palette should have 64 pixels. + * The two least significant bits contain the `4dir` rotation. * `paramtype2 = "colorwallmounted"` * Same as `wallmounted`, but with colors. - * The first five bits of `param2` tells which color is picked from the + * The five most significant bits of `param2` tells which color is picked from the palette. The palette should have 32 pixels. + * The three least significant bits contain the `wallmounted` value. * `paramtype2 = "glasslikeliquidlevel"` * Only valid for "glasslike_framed" or "glasslike_framed_optional" drawtypes. "glasslike_framed_optional" nodes are only affected if the @@ -1417,9 +1440,9 @@ The function of `param2` is determined by `paramtype2` in node definition. * Liquid texture is defined using `special_tiles = {"modname_tilename.png"}` * `paramtype2 = "colordegrotate"` * Same as `degrotate`, but with colors. - * The first (most-significant) three bits of `param2` tells which color - is picked from the palette. The palette should have 8 pixels. - * Remaining 5 bits store rotation in range 0–23 (i.e. in 15° steps) + * The three most significant bits of `param2` tells which color is picked + from the palette. The palette should have 8 pixels. + * The five least significant bits store rotation in range 0–23 (i.e. in 15° steps) * `paramtype2 = "none"` * `param2` will not be used by the engine and can be used to store an arbitrary value @@ -1470,7 +1493,8 @@ Look for examples in `games/devtest` or `games/minetest_game`. 'Connected Glass'. * `allfaces` * Often used for partially-transparent nodes. - * External and internal sides of textures are visible. + * External sides of textures, and unlike other drawtypes, the external sides + of other blocks, are visible from the inside. * `allfaces_optional` * Often used for leaves nodes. * This switches between `normal`, `glasslike` and `allfaces` according to @@ -2729,6 +2753,8 @@ Version History * Formspec version 7 (5.8.0): * style[]: Add focused state for buttons * Add field_enter_after_edit[] (experimental) +* Formspec version 8 (5.10.0) + * scroll_container[]: content padding parameter Elements -------- @@ -2812,7 +2838,7 @@ Elements * End of a container, following elements are no longer relative to this container. -### `scroll_container[,;,;;;]` +### `scroll_container[,;,;;;;]` * Start of a scroll_container block. All contained elements will ... * take the scroll_container coordinate as position origin, @@ -2821,6 +2847,12 @@ Elements * be clipped to the rectangle defined by `X`, `Y`, `W` and `H`. * `orientation`: possible values are `vertical` and `horizontal`. * `scroll factor`: optional, defaults to `0.1`. +* `content padding`: (optional), in formspec coordinate units + * If specified, the scrollbar properties `max` and `thumbsize` are calculated automatically + based on the content size plus `content padding` at the end of the container. `min` is set to 0. + * Negative `scroll factor` is not supported. + * When active, `scrollbaroptions[]` has no effect on the affected properties. + * Defaults to empty value (= disabled). * Nesting is possible. * Some elements might work a little different if they are in a scroll_container. * Note: If you want the scroll_container to actually work, you also need to add a @@ -5524,6 +5556,8 @@ Utilities hotbar_hud_element = true, -- Bulk LBM support (5.10.0) bulk_lbms = true, + -- ABM supports field without_neighbors (5.10.0) + abm_without_neighbors = true, } ``` @@ -5847,8 +5881,13 @@ Call these functions only at load time! * `clicker`: ObjectRef - Object that acted upon `player`, may or may not be a player * `minetest.register_on_player_hpchange(function(player, hp_change, reason), modifier)` * Called when the player gets damaged or healed + * When `hp == 0`, damage doesn't trigger this callback. + * When `hp == hp_max`, healing does still trigger this callback. * `player`: ObjectRef of the player * `hp_change`: the amount of change. Negative when it is damage. + * Historically, the new HP value was clamped to [0, 65535] before + calculating the HP change. This clamping has been removed as of + Minetest 5.10.0 * `reason`: a PlayerHPChangeReason table. * The `type` field will have one of the following values: * `set_hp`: A mod or the engine called `set_hp` without @@ -6556,6 +6595,9 @@ Formspec * `minetest.formspec_escape(string)`: returns a string * escapes the characters "[", "]", "\", "," and ";", which cannot be used in formspecs. +* `minetest.hypertext_escape(string)`: returns a string + * escapes the characters "\", "<", and ">" to show text in a hypertext element. + * not safe for use with tag attributes. * `minetest.explode_table_event(string)`: returns a table * returns e.g. `{type="CHG", row=1, column=2}` * `type` is one of: @@ -6813,17 +6855,6 @@ This allows you easy interoperability for delegating work to jobs. * Register a path to a Lua file to be imported when an async environment is initialized. You can use this to preload code which you can then call later using `minetest.handle_async()`. -* `minetest.register_portable_metatable(name, mt)`: - * Register a metatable that should be preserved when data is transferred - between the main thread and the async environment. - * `name` is a string that identifies the metatable. It is recommended to - follow the `modname:name` convention for this identifier. - * `mt` is the metatable to register. - * Note that it is allowed to register the same metatable under multiple - names, but it is not allowed to register multiple metatables under the - same name. - * You must register the metatable in both the main environment - and the async environment for this mechanism to work. ### List of APIs available in an async environment @@ -6853,7 +6884,8 @@ Functions: * Standalone helpers such as logging, filesystem, encoding, hashing or compression APIs -* `minetest.register_portable_metatable` (see above) +* `minetest.register_portable_metatable` +* IPC Variables: @@ -6931,6 +6963,7 @@ Functions: * `minetest.get_node`, `set_node`, `find_node_near`, `find_nodes_in_area`, `spawn_tree` and similar * these only operate on the current chunk (if inside a callback) +* IPC Variables: @@ -7008,6 +7041,52 @@ Server this can make transfer of bigger files painless (if set up). Nevertheless it is advised not to use dynamic media for big media files. +IPC +--- + +The engine provides a generalized mechanism to enable sharing data between the +different Lua environments (main, mapgen and async). +It is essentially a shared in-memory key-value store. + +* `minetest.ipc_get(key)`: + * Read a value from the shared data area. + * `key`: string, should use the `"modname:thing"` convention to avoid conflicts. + * returns an arbitrary Lua value, or `nil` if this key does not exist +* `minetest.ipc_set(key, value)`: + * Write a value to the shared data area. + * `key`: as above + * `value`: an arbitrary Lua value, cannot be or contain userdata. + +Interacting with the shared data will perform an operation comparable to +(de)serialization on each access. +For that reason modifying references will not have any effect, as in this example: +```lua +minetest.ipc_set("test:foo", {}) +minetest.ipc_get("test:foo").subkey = "value" -- WRONG! +minetest.ipc_get("test:foo") -- returns an empty table +``` + +**Advanced**: + +* `minetest.ipc_cas(key, old_value, new_value)`: + * Write a value to the shared data area, but only if the previous value + equals what was given. + This operation is called Compare-and-Swap and can be used to implement + synchronization between threads. + * `key`: as above + * `old_value`: value compared to using `==` (`nil` compares equal for non-existing keys) + * `new_value`: value that will be set + * returns: true on success, false otherwise +* `minetest.ipc_poll(key, timeout)`: + * Do a blocking wait until a value (other than `nil`) is present at the key. + * **IMPORTANT**: You usually don't need this function. Use this as a last resort + if nothing else can satisfy your use case! None of the Lua environments the + engine has are safe to block for extended periods, especially on the main + thread any delays directly translate to lag felt by players. + * `key`: as above + * `timeout`: maximum wait time, in milliseconds (positive values only) + * returns: true on success, false on timeout + Bans ---- @@ -7407,6 +7486,17 @@ Misc. * `minetest.global_exists(name)` * Checks if a global variable has been set, without triggering a warning. +* `minetest.register_portable_metatable(name, mt)`: + * Register a metatable that should be preserved when Lua data is transferred + between environments (via IPC or `handle_async`). + * `name` is a string that identifies the metatable. It is recommended to + follow the `modname:name` convention for this identifier. + * `mt` is the metatable to register. + * Note that the same metatable can be registered under multiple names, + but multiple metatables must not be registered under the same name. + * You must register the metatable in both the main environment + and the async environment for this mechanism to work. + Global objects -------------- @@ -8017,8 +8107,7 @@ child will follow movement and rotation of that bone. * Animation interpolates towards the end frame but stops when it is reached * If looped, there is no interpolation back to the start frame * If looped, the model should look identical at start and end - * Only integer numbers are supported - * default: `{x=1, y=1}` + * default: `{x=1.0, y=1.0}` * `frame_speed`: How fast the animation plays, in frames per second (number) * default: `15.0` * `frame_blend`: number, default: `0.0` @@ -8241,12 +8330,18 @@ child will follow movement and rotation of that bone. bgcolor[], any non-style elements (eg: label) may result in weird behavior. * Only affects formspecs shown after this is called. * `get_formspec_prepend()`: returns a formspec string. -* `get_player_control()`: returns table with player pressed keys - * The table consists of fields with the following boolean values - representing the pressed keys: `up`, `down`, `left`, `right`, `jump`, - `aux1`, `sneak`, `dig`, `place`, `LMB`, `RMB`, and `zoom`. +* `get_player_control()`: returns table with player input + * The table contains the following boolean fields representing the pressed + keys: `up`, `down`, `left`, `right`, `jump`, `aux1`, `sneak`, `dig`, + `place`, `LMB`, `RMB` and `zoom`. * The fields `LMB` and `RMB` are equal to `dig` and `place` respectively, and exist only to preserve backwards compatibility. + * The table also contains the fields `movement_x` and `movement_y`. + * They represent the movement of the player. Values are numbers in the + range [-1.0,+1.0]. + * They take both keyboard and joystick input into account. + * You should prefer them over `up`, `down`, `left` and `right` to + support different input methods correctly. * Returns an empty table `{}` if the object is not a player. * `get_player_control_bits()`: returns integer with bit packed player pressed keys. @@ -8574,23 +8669,43 @@ child will follow movement and rotation of that bone. * values < 0 cause an effect similar to inversion, but keeping original luma and being symmetrical in terms of saturation (eg. -1 and 1 is the same saturation and luma, but different hues) + * This value has no effect on clients who have shaders or post-processing disabled. * `shadows` is a table that controls ambient shadows + * This has no effect on clients who have the "Dynamic Shadows" effect disabled. * `intensity` sets the intensity of the shadows from 0 (no shadows, default) to 1 (blackness) - * This value has no effect on clients who have the "Dynamic Shadows" shader disabled. * `tint` tints the shadows with the provided color, with RGB values ranging from 0 to 255. (default `{r=0, g=0, b=0}`) - * This value has no effect on clients who have the "Dynamic Shadows" shader disabled. * `exposure` is a table that controls automatic exposure. The basic exposure factor equation is `e = 2^exposure_correction / clamp(luminance, 2^luminance_min, 2^luminance_max)` + * This has no effect on clients who have the "Automatic Exposure" effect disabled. * `luminance_min` set the lower luminance boundary to use in the calculation (default: `-3.0`) * `luminance_max` set the upper luminance boundary to use in the calculation (default: `-3.0`) * `exposure_correction` correct observed exposure by the given EV value (default: `0.0`) * `speed_dark_bright` set the speed of adapting to bright light (default: `1000.0`) * `speed_bright_dark` set the speed of adapting to dark scene (default: `1000.0`) * `center_weight_power` set the power factor for center-weighted luminance measurement (default: `1.0`) + * `bloom` is a table that controls bloom. + * This has no effect on clients with protocol version < 46 or clients who + have the "Bloom" effect disabled. + * `intensity` defines much bloom is applied to the rendered image. + * Recommended range: from 0.0 to 1.0, default: 0.05 + * If set to 0, bloom is disabled. + * The default value is to be changed from 0.05 to 0 in the future. + If you wish to keep the current default value, you should set it + explicitly. + * `strength_factor` defines the magnitude of bloom overexposure. + * Recommended range: from 0.1 to 10.0, default: 1.0 + * `radius` is a logical value that controls how far the bloom effect + spreads from the bright objects. + * Recommended range: from 0.1 to 8.0, default: 1.0 + * The behavior of values outside the recommended range is unspecified. * `volumetric_light`: is a table that controls volumetric light (a.k.a. "godrays") - * `strength`: sets the strength of the volumetric light effect from 0 (off, default) to 1 (strongest) - * This value has no effect on clients who have the "Volumetric Lighting" or "Bloom" shaders disabled. + * This has no effect on clients who have the "Volumetric Lighting" or "Bloom" effects disabled. + * `strength`: sets the strength of the volumetric light effect from 0 (off, default) to 1 (strongest). + * `0.2` is a reasonable standard value. + * Currently, bloom `intensity` and `strength_factor` affect volumetric + lighting `strength` and vice versa. This behavior is to be changed + in the future, do not rely on it. * `get_lighting()`: returns the current state of lighting for the player. * Result is a table with the same fields as `light_definition` in `set_lighting`. @@ -9106,6 +9221,11 @@ Used by `minetest.register_abm`. -- If left out or empty, any neighbor will do. -- `group:groupname` can also be used here. + without_neighbors = {"default:lava_source", "default:lava_flowing"}, + -- Only apply `action` to nodes that have no one of these neighbors. + -- If left out or empty, it has no effect. + -- `group:groupname` can also be used here. + interval = 10.0, -- Operation interval in seconds @@ -9515,12 +9635,18 @@ Used by `minetest.register_node`. use_texture_alpha = ..., -- Specifies how the texture's alpha channel will be used for rendering. - -- possible values: - -- * "opaque": Node is rendered opaque regardless of alpha channel - -- * "clip": A given pixel is either fully see-through or opaque - -- depending on the alpha channel being below/above 50% in value - -- * "blend": The alpha channel specifies how transparent a given pixel - -- of the rendered node is + -- Possible values: + -- * "opaque": + -- Node is rendered opaque regardless of alpha channel. + -- * "clip": + -- A given pixel is either fully see-through or opaque + -- depending on the alpha channel being below/above 50% in value. + -- Use this for nodes with fully transparent and fully opaque areas. + -- * "blend": + -- The alpha channel specifies how transparent a given pixel + -- of the rendered node is. This comes at a performance cost. + -- Only use this when correct rendering + -- among semitransparent nodes is necessary. -- The default is "opaque" for drawtypes normal, liquid and flowingliquid, -- mesh and nodebox or "clip" otherwise. -- If set to a boolean value (deprecated): true either sets it to blend diff --git a/doc/menu_lua_api.md b/doc/menu_lua_api.md index be63af904..c03c0501e 100644 --- a/doc/menu_lua_api.md +++ b/doc/menu_lua_api.md @@ -57,7 +57,10 @@ Functions * 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:// + * `url` must begin with http:// or https:// +* `core.open_url_dialog(url)` + * shows a dialog to allow the user to choose whether to open a URL. + * `url` must begin with http:// or https:// * `core.open_dir(path)` * opens the path in the system file browser/explorer, returns false on failure. * Must be an existing directory. @@ -65,6 +68,8 @@ Functions * Android only. Shares file using the share popup * `core.get_version()` (possible in async calls) * returns current core version +* `core.get_formspec_version()` + * returns maximum supported formspec version diff --git a/games/devtest/mods/gltf/LICENSE.md b/games/devtest/mods/gltf/LICENSE.md index b0ae5fef5..6c3828a4a 100644 --- a/games/devtest/mods/gltf/LICENSE.md +++ b/games/devtest/mods/gltf/LICENSE.md @@ -1,4 +1,4 @@ -glTF test model (and corresponding texture) licenses: +The glTF test models (and corresponding textures) in this mod are all licensed freely: * Spider (`gltf_spider.gltf`, `gltf_spider.png`): * By [archfan7411](https://github.com/archfan7411) diff --git a/games/devtest/mods/gltf/init.lua b/games/devtest/mods/gltf/init.lua index b5c2032bc..252fd017d 100644 --- a/games/devtest/mods/gltf/init.lua +++ b/games/devtest/mods/gltf/init.lua @@ -18,13 +18,57 @@ do register_entity("blender_cube", cube_textures) register_entity("blender_cube_scaled", cube_textures) register_entity("blender_cube_matrix_transform", cube_textures) + minetest.register_entity("gltf:blender_cube_glb", { + initial_properties = { + visual = "mesh", + mesh = "gltf_blender_cube.glb", + textures = cube_textures, + backface_culling = true, + }, + }) end + register_entity("snow_man", {"gltf_snow_man.png"}) register_entity("spider", {"gltf_spider.png"}) --- Note: Model has an animation, but we can use it as a static test nevertheless + +minetest.register_entity("gltf:spider_animated", { + initial_properties = { + visual = "mesh", + mesh = "gltf_spider_animated.gltf", + textures = {"gltf_spider.png"}, + }, + on_activate = function(self) + self.object:set_animation({x = 0, y = 140}, 1) + end +}) + +minetest.register_entity("gltf:simple_skin", { + initial_properties = { + visual = "mesh", + visual_size = vector.new(5, 5, 5), + mesh = "gltf_simple_skin.gltf", + textures = {}, + backface_culling = false + }, + on_activate = function(self) + self.object:set_animation({x = 0, y = 5.5}, 1) + end +}) + -- The claws rendering incorrectly from one side is expected behavior: -- They use an unsupported double-sided material. -register_entity("frog", {"gltf_frog.png"}, false) +minetest.register_entity("gltf:frog", { + initial_properties = { + visual = "mesh", + mesh = "gltf_frog.gltf", + textures = {"gltf_frog.png"}, + backface_culling = false + }, + on_activate = function(self) + self.object:set_animation({x = 0, y = 0.75}, 1) + end +}) + minetest.register_node("gltf:frog", { description = "glTF frog, but it's a node", diff --git a/games/devtest/mods/gltf/models/gltf_blender_cube.glb b/games/devtest/mods/gltf/models/gltf_blender_cube.glb new file mode 100644 index 000000000..b1894fc4f Binary files /dev/null and b/games/devtest/mods/gltf/models/gltf_blender_cube.glb differ diff --git a/games/devtest/mods/gltf/models/gltf_simple_skin.gltf b/games/devtest/mods/gltf/models/gltf_simple_skin.gltf new file mode 100644 index 000000000..3d6c24a6c --- /dev/null +++ b/games/devtest/mods/gltf/models/gltf_simple_skin.gltf @@ -0,0 +1 @@ +{"scene":0,"scenes":[{"nodes":[0,1]}],"nodes":[{"skin":0,"mesh":0},{"children":[2]},{"translation":[0.0,1.0,0.0],"rotation":[0.0,0.0,0.0,1.0]}],"meshes":[{"primitives":[{"attributes":{"POSITION":1,"JOINTS_0":2,"WEIGHTS_0":3},"indices":0}]}],"skins":[{"inverseBindMatrices":4,"joints":[1,2]}],"animations":[{"channels":[{"sampler":0,"target":{"node":2,"path":"rotation"}}],"samplers":[{"input":5,"interpolation":"LINEAR","output":6}]}],"buffers":[{"uri":"data:application/gltf-buffer;base64,AAABAAMAAAADAAIAAgADAAUAAgAFAAQABAAFAAcABAAHAAYABgAHAAkABgAJAAgAAAAAvwAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAvwAAAD8AAAAAAAAAPwAAAD8AAAAAAAAAvwAAgD8AAAAAAAAAPwAAgD8AAAAAAAAAvwAAwD8AAAAAAAAAPwAAwD8AAAAAAAAAvwAAAEAAAAAAAAAAPwAAAEAAAAAA","byteLength":168},{"uri":"data:application/gltf-buffer;base64,AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAABAPwAAgD4AAAAAAAAAAAAAQD8AAIA+AAAAAAAAAAAAAAA/AAAAPwAAAAAAAAAAAAAAPwAAAD8AAAAAAAAAAAAAgD4AAEA/AAAAAAAAAAAAAIA+AABAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAA=","byteLength":320},{"uri":"data:application/gltf-buffer;base64,AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAgD8=","byteLength":128},{"uri":"data:application/gltf-buffer;base64,AAAAAAAAAD8AAIA/AADAPwAAAEAAACBAAABAQAAAYEAAAIBAAACQQAAAoEAAALBAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAkxjEPkSLbD8AAAAAAAAAAPT9ND/0/TQ/AAAAAAAAAAD0/TQ/9P00PwAAAAAAAAAAkxjEPkSLbD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAkxjEvkSLbD8AAAAAAAAAAPT9NL/0/TQ/AAAAAAAAAAD0/TS/9P00PwAAAAAAAAAAkxjEvkSLbD8AAAAAAAAAAAAAAAAAAIA/","byteLength":240}],"bufferViews":[{"buffer":0,"byteLength":48,"target":34963},{"buffer":0,"byteOffset":48,"byteLength":120,"target":34962},{"buffer":1,"byteLength":320,"byteStride":16},{"buffer":2,"byteLength":128},{"buffer":3,"byteLength":240}],"accessors":[{"bufferView":0,"componentType":5123,"count":24,"type":"SCALAR"},{"bufferView":1,"componentType":5126,"count":10,"type":"VEC3","max":[0.5,2.0,0.0],"min":[-0.5,0.0,0.0]},{"bufferView":2,"componentType":5123,"count":10,"type":"VEC4"},{"bufferView":2,"byteOffset":160,"componentType":5126,"count":10,"type":"VEC4"},{"bufferView":3,"componentType":5126,"count":2,"type":"MAT4"},{"bufferView":4,"componentType":5126,"count":12,"type":"SCALAR","max":[5.5],"min":[0.0]},{"bufferView":4,"byteOffset":48,"componentType":5126,"count":12,"type":"VEC4","max":[0.0,0.0,0.707,1.0],"min":[0.0,0.0,-0.707,0.707]}],"asset":{"version":"2.0"}} diff --git a/games/devtest/mods/gltf/models/gltf_spider_animated.gltf b/games/devtest/mods/gltf/models/gltf_spider_animated.gltf new file mode 100644 index 000000000..79221b0c7 --- /dev/null +++ b/games/devtest/mods/gltf/models/gltf_spider_animated.gltf @@ -0,0 +1 @@ +{"asset":{"generator":"Khronos glTF Blender I/O v1.7.33","version":"2.0"},"scene":0,"scenes":[{"name":"Scene","nodes":[58]}],"nodes":[{"name":"Pincer.L","rotation":[0.03853772580623627,0.09671717882156372,0.5138389468193054,0.8515457510948181],"translation":[-2.2351741790771484e-08,0.2836739718914032,-2.2351741790771484e-08]},{"children":[0],"name":"JawBase.L","rotation":[-0.23922589421272278,-9.208349638356594e-08,-0.38811206817626953,0.8900224566459656],"scale":[1,1,0.9999999403953552],"translation":[8.097286041675034e-08,0.7702280879020691,-1.169656727029178e-07]},{"name":"Pincer.R","rotation":[0.038537755608558655,-0.09671713411808014,-0.5138388872146606,0.8515458106994629],"scale":[0.9999997615814209,0.9999999403953552,1],"translation":[2.9802322387695312e-08,0.2836737036705017,-2.9802322387695312e-08]},{"children":[2],"name":"JawBase.R","rotation":[-0.2392251342535019,1.9714243535418063e-06,0.3881126046180725,0.8900225758552551],"translation":[1.3833086898173974e-09,0.7702280282974243,-6.620245329713725e-08]},{"children":[1,3],"name":"Head","rotation":[0.4052415192127228,-3.4197712478652165e-13,-8.695541282577324e-07,0.9142096638679504],"translation":[-2.0781445141115906e-16,0.6883190274238586,-1.4901161193847656e-08]},{"children":[4],"name":"NeckBase","rotation":[-0.778048574924469,7.488795716881214e-08,1.7622618315726868e-06,0.6282041072845459],"translation":[-3.399441372928941e-14,0.3915250301361084,-2.3283064365386963e-09]},{"name":"Body.002","rotation":[0.17414046823978424,0,-4.151832513343834e-07,0.9847208261489868],"translation":[5.897654597759178e-14,1.0079095363616943,-1.2134763416327132e-08]},{"children":[6],"name":"Body.001","rotation":[0.6673352122306824,-7.632472941426077e-14,-1.5910509318928234e-06,0.7447575330734253],"scale":[1,0.9999999403953552,0.9999999403953552],"translation":[-3.962037268363458e-15,0.3915250301361084,-3.1428597502269895e-09]},{"name":"Leg4Fore.L","rotation":[-0.021953541785478592,0.030033688992261887,-0.4378480017185211,0.8982790112495422],"scale":[1,0.9999998211860657,1.0000001192092896],"translation":[1.9202238377147296e-07,0.8228543996810913,-1.749940707895803e-07]},{"children":[8],"name":"Leg4Lower.L","rotation":[-0.11090508848428726,0.11991499364376068,-0.48737218976020813,0.8577813506126404],"scale":[0.9999995231628418,0.9999997615814209,1.000000238418579],"translation":[1.9631791303709178e-07,0.8208085298538208,-4.769351491518137e-08]},{"children":[9],"name":"Leg4Mid.L","rotation":[-0.21032677590847015,0.09273893386125565,-0.42330121994018555,0.8763437271118164],"scale":[1,0.9999996423721313,0.999999463558197],"translation":[-1.720833893159579e-07,1.5146127939224243,1.4611718768264836e-07]},{"children":[10],"name":"Leg4Upper.L","rotation":[0.581340491771698,-0.03387186676263809,0.4926694631576538,0.6466628909111023],"scale":[1.0000003576278687,1.000000238418579,1.0000003576278687],"translation":[-2.6137989550534257e-09,0.8996680974960327,-2.8558396536482178e-08]},{"children":[11],"name":"Leg4Base.L","rotation":[0.4988132119178772,0.67340087890625,0.0026363276410847902,0.5456278324127197],"scale":[0.9999998807907104,0.9999998807907104,0.9999999403953552],"translation":[6.932457941033476e-10,0.3915250301361084,-7.783789612858527e-09]},{"name":"Leg3Fore.L","rotation":[-0.040254246443510056,0.0051941643469035625,-0.3734953701496124,0.9267436861991882],"scale":[1.0000001192092896,1,1.0000003576278687],"translation":[-7.186849302343035e-07,0.761729896068573,1.4940267689667053e-08]},{"children":[13],"name":"Leg3Lower.L","rotation":[-0.02293548174202442,0.03108014352619648,-0.5376279950141907,0.8422969579696655],"scale":[1,0.9999998807907104,1],"translation":[2.6879865799855907e-07,0.890315592288971,-2.3254589365251377e-08]},{"children":[14],"name":"Leg3Mid.L","rotation":[-0.10393687337636948,0.026799378916621208,-0.47722530364990234,0.8722012042999268],"scale":[1,1.0000001192092896,1],"translation":[-4.5783519908582093e-07,1.5058820247650146,6.428529530921878e-08]},{"children":[15],"name":"Leg3Upper.L","rotation":[0.22388437390327454,0.00046301534166559577,0.7424523234367371,0.6313795447349548],"scale":[0.9999996423721313,0.9999999403953552,0.9999995231628418],"translation":[8.173065424443848e-08,0.8363674879074097,-3.891337030381692e-09]},{"children":[16],"name":"Leg3Base.L","rotation":[0.48101410269737244,0.8565962910652161,-0.006458853371441364,0.18661867082118988],"scale":[0.9999999403953552,0.9999998807907104,0.9999999403953552],"translation":[1.1383551878907383e-08,0.3915250301361084,2.5693926986036786e-09]},{"name":"Leg2Fore.L","rotation":[0.04987334460020065,-0.01207074522972107,-0.4028705060482025,0.9138174653053284],"scale":[0.999999463558197,1.0000004768371582,1.0000001192092896],"translation":[2.9161077463868423e-07,0.7777483463287354,1.7455030842938868e-07]},{"children":[18],"name":"Leg2Lower.L","rotation":[0.08352424204349518,-0.04269447177648544,-0.518085777759552,0.8501694202423096],"scale":[1.0000001192092896,1.0000004768371582,1.0000004768371582],"translation":[-1.5067358560827415e-07,0.9397417306900024,-4.163759115272114e-08]},{"children":[19],"name":"Leg2Mid.L","rotation":[0.14706559479236603,-0.028868243098258972,-0.47296836972236633,0.868239164352417],"scale":[1.0000004768371582,0.9999999403953552,0.9999999403953552],"translation":[-2.2217867012841452e-07,1.5058820247650146,-6.989571943449846e-08]},{"children":[20],"name":"Leg2Upper.L","rotation":[-0.42424774169921875,-0.0005238422891125083,0.6472405791282654,0.6333192586898804],"scale":[1.000000238418579,1.0000003576278687,1.0000005960464478],"translation":[9.311328597050306e-08,0.881853461265564,-1.8038990745594674e-08]},{"children":[21],"name":"Leg2Base.L","rotation":[-0.4972459375858307,-0.7882749438285828,0.006057440303266048,0.362398236989975],"scale":[0.9999997615814209,0.9999998807907104,0.9999999403953552],"translation":[7.375939858889069e-10,0.3915250301361084,4.028271050060539e-09]},{"name":"Leg1Fore.L","rotation":[-0.01934647001326084,-0.04218549281358719,-0.4403696358203888,0.8966162800788879],"scale":[1,1,0.9999997615814209],"translation":[2.0805721590022586e-07,0.815664529800415,4.0515438115562574e-08]},{"children":[23],"name":"Leg1Lower.L","rotation":[0.15678077936172485,-0.1661715805530548,-0.47995010018348694,0.8470270037651062],"scale":[0.999999463558197,1,0.9999999403953552],"translation":[3.670676562705921e-08,0.8788074851036072,8.29251618483795e-08]},{"children":[24],"name":"Leg1Mid.L","rotation":[0.26206591725349426,-0.11672191321849823,-0.4046621024608612,0.8683006763458252],"scale":[1.0000001192092896,0.9999999403953552,0.9999995231628418],"translation":[3.601947184961318e-08,1.5125981569290161,-1.6144279868512967e-07]},{"children":[25],"name":"Leg1Upper.L","rotation":[-0.62815922498703,0.04343283176422119,0.39305803179740906,0.6701006889343262],"translation":[-1.0171092412747385e-07,1.043814778327942,1.114601104745816e-07]},{"children":[26],"name":"Leg1Base.L","rotation":[-0.536352813243866,-0.596045732498169,-0.006935927551239729,0.5975006818771362],"scale":[0.9999998211860657,0.9999998211860657,1],"translation":[7.451212979958655e-09,0.3915250301361084,-5.977072614626877e-09]},{"name":"Leg4Fore.R","rotation":[-0.0219536405056715,-0.030033595860004425,0.43784812092781067,0.8982789516448975],"scale":[1.000000238418579,0.9999998807907104,1.0000001192092896],"translation":[4.575199454848189e-07,0.82285475730896,1.3987688873839943e-07]},{"children":[28],"name":"Leg4Lower.R","rotation":[-0.11090517044067383,-0.11991491913795471,0.48737218976020813,0.8577813506126404],"scale":[1.0000001192092896,0.9999999403953552,1.0000001192092896],"translation":[5.0247152216797986e-08,0.8208085894584656,1.2523592829438712e-07]},{"children":[29],"name":"Leg4Mid.R","rotation":[-0.21032673120498657,-0.09273889660835266,0.42330119013786316,0.876343846321106],"scale":[0.9999998211860657,0.9999995231628418,1.0000001192092896],"translation":[-1.2884336797469587e-07,1.514613151550293,6.563716681284859e-08]},{"children":[30],"name":"Leg4Upper.R","rotation":[0.5813404321670532,0.03387187048792839,-0.4926694333553314,0.6466629505157471],"scale":[1,1.000000238418579,0.9999997019767761],"translation":[-3.940737158814045e-08,0.8996680974960327,1.9567494291550247e-09]},{"children":[31],"name":"Leg4Base.R","rotation":[0.4988132119178772,-0.6733996272087097,-0.0026374668814241886,0.5456294417381287],"scale":[1,1.0000001192092896,1],"translation":[-1.1682686817948706e-08,0.3915250301361084,-1.3812247345867945e-08]},{"name":"Leg3Fore.R","rotation":[-0.04025428742170334,-0.005194155499339104,0.3734953999519348,0.9267436861991882],"scale":[0.9999998211860657,1.0000001192092896,1.0000001192092896],"translation":[-7.285660217348777e-07,0.7617300748825073,-4.0205627271916455e-08]},{"children":[33],"name":"Leg3Lower.R","rotation":[-0.02293553575873375,-0.03108006715774536,0.5376282930374146,0.8422967791557312],"scale":[1.0000001192092896,0.9999996423721313,0.9999999403953552],"translation":[7.143101754536474e-08,0.8903149366378784,6.888667769544554e-08]},{"children":[34],"name":"Leg3Mid.R","rotation":[-0.10393673926591873,-0.026799339801073074,0.47722548246383667,0.872201144695282],"scale":[1.0000003576278687,0.9999998807907104,0.9999998807907104],"translation":[1.4287303429227904e-07,1.5058823823928833,9.578651827268914e-08]},{"children":[35],"name":"Leg3Upper.R","rotation":[0.2238844484090805,-0.00046323961578309536,-0.7424524426460266,0.6313793659210205],"scale":[1.0000001192092896,1.0000005960464478,0.9999997615814209],"translation":[-2.9145089897042453e-08,0.8363675475120544,-1.3412945421009681e-08]},{"children":[36],"name":"Leg3Base.R","rotation":[0.48101410269737244,-0.8565958738327026,0.006457682233303785,0.18662074208259583],"scale":[0.9999999403953552,1.0000001192092896,1],"translation":[1.187698939197901e-09,0.3915250301361084,1.396204218906405e-08]},{"name":"Leg2Fore.R","rotation":[0.04987342655658722,0.012070796452462673,0.40287071466445923,0.9138173460960388],"scale":[0.9999997615814209,0.9999998807907104,0.9999997019767761],"translation":[4.900767294202524e-07,0.7777489423751831,1.3496240569565998e-07]},{"children":[38],"name":"Leg2Lower.R","rotation":[0.08352430164813995,0.04269447922706604,0.518085777759552,0.8501694202423096],"scale":[1.000000238418579,1.0000003576278687,0.9999999403953552],"translation":[1.2208448652017978e-07,0.9397414326667786,-3.409446946989192e-08]},{"children":[39],"name":"Leg2Mid.R","rotation":[0.1470656394958496,0.028868237510323524,0.4729681611061096,0.8682392835617065],"scale":[1.0000001192092896,1.0000003576278687,1.0000001192092896],"translation":[4.8437236443987786e-08,1.5058820247650146,-2.5024842642551448e-08]},{"children":[40],"name":"Leg2Upper.R","rotation":[-0.4242475926876068,0.0005238187150098383,-0.6472404599189758,0.6333194971084595],"scale":[0.9999997019767761,1,0.9999998211860657],"translation":[3.550610472302651e-09,0.8818532824516296,4.425183419698442e-08]},{"children":[41],"name":"Leg2Base.R","rotation":[-0.4972459375858307,0.7882757782936096,-0.006056289654225111,0.36239632964134216],"scale":[0.9999998211860657,1,0.9999999403953552],"translation":[-7.2600920830723226e-09,0.3915250301361084,-5.773719280455225e-09]},{"name":"Leg1Fore.R","rotation":[-0.015208502300083637,0.04422945901751518,0.4362727701663971,0.8985980749130249],"scale":[1.000000238418579,0.9999995827674866,0.9999997615814209],"translation":[-6.20622927272052e-07,0.8156638741493225,-1.6136721114889951e-07]},{"children":[43],"name":"Leg1Lower.R","rotation":[0.15885458886623383,0.17276015877723694,0.4745163321495056,0.848382830619812],"scale":[1.000000238418579,1.0000001192092896,1.0000004768371582],"translation":[-2.3015780925561558e-07,0.8788077235221863,2.258973452740065e-08]},{"children":[44],"name":"Leg1Mid.R","rotation":[0.2600231170654297,0.12465617805719376,0.4028773903846741,0.8686419725418091],"scale":[1,0.9999997019767761,0.9999999403953552],"translation":[-2.3629894485566183e-08,1.512597680091858,-5.442473494099431e-08]},{"children":[45],"name":"Leg1Upper.R","rotation":[-0.6237055063247681,-0.03962605446577072,-0.3963613212108612,0.6725466251373291],"scale":[1,1,0.9999995827674866],"translation":[4.151442212219081e-08,1.0438144207000732,6.221015524943141e-08]},{"children":[46],"name":"Leg1Base.R","rotation":[-0.5363527536392212,0.5960471630096436,0.0069372160360217094,0.5974993705749512],"scale":[1,1.0000001192092896,1.0000001192092896],"translation":[7.877114072130098e-09,0.3915250301361084,-5.523408841412447e-09]},{"children":[5,7,12,17,22,27,32,37,42,47],"name":"Body","rotation":[-0.9999927282333374,-4.546671483751652e-09,1.1920842553081457e-06,0.003814017167314887],"translation":[-2.1589291564903364e-17,0.5146726369857788,0.22900062799453735]},{"name":"Leg4IK.L","rotation":[-2.6692541510442425e-08,-2.6692541510442425e-08,-0.7071068286895752,0.7071068286895752],"translation":[2.2291481494903564,-0.5599625110626221,-0.7613579630851746]},{"name":"Leg3IK.L","rotation":[-2.6692541510442425e-08,-2.6692541510442425e-08,-0.7071068286895752,0.7071068286895752],"translation":[2.3687760829925537,-0.5599625110626221,-0.033313095569610596]},{"name":"Leg2IK.L","rotation":[-2.6692541510442425e-08,-2.6692541510442425e-08,-0.7071068286895752,0.7071068286895752],"translation":[2.3687760829925537,-0.5599625110626221,0.6964529752731323]},{"name":"Leg1IK.L","rotation":[-2.6692541510442425e-08,-2.6692541510442425e-08,-0.7071068286895752,0.7071068286895752],"translation":[2.2556710243225098,-0.5599625110626221,1.4977319240570068]},{"name":"Leg4IK.R","rotation":[-2.6692541510442425e-08,2.6692541510442425e-08,0.7071068286895752,0.7071068286895752],"translation":[-2.2291481494903564,-0.5599625110626221,-0.7613579630851746]},{"name":"Leg3IK.R","rotation":[-2.6692541510442425e-08,2.6692541510442425e-08,0.7071068286895752,0.7071068286895752],"translation":[-2.3687760829925537,-0.5599625110626221,-0.033313095569610596]},{"name":"Leg2IK.R","rotation":[-2.6692541510442425e-08,2.6692541510442425e-08,0.7071068286895752,0.7071068286895752],"translation":[-2.3687760829925537,-0.5599625110626221,0.6964529752731323]},{"name":"Leg1IK.R","rotation":[-2.6692541510442425e-08,2.6692541510442425e-08,0.7071068286895752,0.7071068286895752],"translation":[-2.2556710243225098,-0.5599625110626221,1.5977319478988647]},{"mesh":0,"name":"Spider","skin":0},{"children":[57,48,49,50,51,52,53,54,55,56],"name":"Armature"}],"animations":[{"channels":[{"sampler":0,"target":{"node":48,"path":"translation"}},{"sampler":1,"target":{"node":48,"path":"rotation"}},{"sampler":2,"target":{"node":48,"path":"scale"}},{"sampler":3,"target":{"node":4,"path":"translation"}},{"sampler":4,"target":{"node":4,"path":"rotation"}},{"sampler":5,"target":{"node":4,"path":"scale"}},{"sampler":6,"target":{"node":0,"path":"translation"}},{"sampler":7,"target":{"node":0,"path":"rotation"}},{"sampler":8,"target":{"node":0,"path":"scale"}},{"sampler":9,"target":{"node":2,"path":"translation"}},{"sampler":10,"target":{"node":2,"path":"rotation"}},{"sampler":11,"target":{"node":2,"path":"scale"}},{"sampler":12,"target":{"node":6,"path":"translation"}},{"sampler":13,"target":{"node":6,"path":"rotation"}},{"sampler":14,"target":{"node":6,"path":"scale"}},{"sampler":15,"target":{"node":11,"path":"rotation"}},{"sampler":16,"target":{"node":10,"path":"rotation"}},{"sampler":17,"target":{"node":9,"path":"rotation"}},{"sampler":18,"target":{"node":8,"path":"rotation"}},{"sampler":19,"target":{"node":16,"path":"rotation"}},{"sampler":20,"target":{"node":15,"path":"rotation"}},{"sampler":21,"target":{"node":14,"path":"rotation"}},{"sampler":22,"target":{"node":13,"path":"rotation"}},{"sampler":23,"target":{"node":21,"path":"rotation"}},{"sampler":24,"target":{"node":20,"path":"rotation"}},{"sampler":25,"target":{"node":19,"path":"rotation"}},{"sampler":26,"target":{"node":18,"path":"rotation"}},{"sampler":27,"target":{"node":26,"path":"rotation"}},{"sampler":28,"target":{"node":25,"path":"rotation"}},{"sampler":29,"target":{"node":24,"path":"rotation"}},{"sampler":30,"target":{"node":23,"path":"translation"}},{"sampler":31,"target":{"node":23,"path":"rotation"}},{"sampler":32,"target":{"node":23,"path":"scale"}},{"sampler":33,"target":{"node":31,"path":"rotation"}},{"sampler":34,"target":{"node":30,"path":"rotation"}},{"sampler":35,"target":{"node":29,"path":"rotation"}},{"sampler":36,"target":{"node":28,"path":"rotation"}},{"sampler":37,"target":{"node":36,"path":"rotation"}},{"sampler":38,"target":{"node":35,"path":"rotation"}},{"sampler":39,"target":{"node":34,"path":"rotation"}},{"sampler":40,"target":{"node":33,"path":"rotation"}},{"sampler":41,"target":{"node":41,"path":"rotation"}},{"sampler":42,"target":{"node":40,"path":"rotation"}},{"sampler":43,"target":{"node":39,"path":"rotation"}},{"sampler":44,"target":{"node":38,"path":"rotation"}},{"sampler":45,"target":{"node":46,"path":"rotation"}},{"sampler":46,"target":{"node":45,"path":"rotation"}},{"sampler":47,"target":{"node":44,"path":"rotation"}},{"sampler":48,"target":{"node":43,"path":"rotation"}},{"sampler":49,"target":{"node":49,"path":"translation"}},{"sampler":50,"target":{"node":49,"path":"rotation"}},{"sampler":51,"target":{"node":49,"path":"scale"}},{"sampler":52,"target":{"node":50,"path":"translation"}},{"sampler":53,"target":{"node":50,"path":"rotation"}},{"sampler":54,"target":{"node":50,"path":"scale"}},{"sampler":55,"target":{"node":51,"path":"translation"}},{"sampler":56,"target":{"node":51,"path":"rotation"}},{"sampler":57,"target":{"node":51,"path":"scale"}},{"sampler":58,"target":{"node":52,"path":"translation"}},{"sampler":59,"target":{"node":52,"path":"rotation"}},{"sampler":60,"target":{"node":52,"path":"scale"}},{"sampler":61,"target":{"node":53,"path":"translation"}},{"sampler":62,"target":{"node":53,"path":"rotation"}},{"sampler":63,"target":{"node":53,"path":"scale"}},{"sampler":64,"target":{"node":54,"path":"translation"}},{"sampler":65,"target":{"node":54,"path":"rotation"}},{"sampler":66,"target":{"node":54,"path":"scale"}},{"sampler":67,"target":{"node":55,"path":"translation"}},{"sampler":68,"target":{"node":55,"path":"rotation"}},{"sampler":69,"target":{"node":55,"path":"scale"}},{"sampler":70,"target":{"node":56,"path":"translation"}},{"sampler":71,"target":{"node":56,"path":"rotation"}},{"sampler":72,"target":{"node":56,"path":"scale"}}],"name":"ArmatureAction","samplers":[{"input":7,"interpolation":"LINEAR","output":8},{"input":7,"interpolation":"LINEAR","output":9},{"input":10,"interpolation":"LINEAR","output":11},{"input":10,"interpolation":"LINEAR","output":12},{"input":7,"interpolation":"LINEAR","output":13},{"input":10,"interpolation":"LINEAR","output":14},{"input":10,"interpolation":"LINEAR","output":15},{"input":7,"interpolation":"LINEAR","output":16},{"input":10,"interpolation":"LINEAR","output":17},{"input":10,"interpolation":"LINEAR","output":18},{"input":7,"interpolation":"LINEAR","output":19},{"input":10,"interpolation":"LINEAR","output":20},{"input":10,"interpolation":"LINEAR","output":21},{"input":7,"interpolation":"LINEAR","output":22},{"input":10,"interpolation":"LINEAR","output":23},{"input":7,"interpolation":"LINEAR","output":24},{"input":7,"interpolation":"LINEAR","output":25},{"input":7,"interpolation":"LINEAR","output":26},{"input":7,"interpolation":"LINEAR","output":27},{"input":7,"interpolation":"LINEAR","output":28},{"input":7,"interpolation":"LINEAR","output":29},{"input":7,"interpolation":"LINEAR","output":30},{"input":7,"interpolation":"LINEAR","output":31},{"input":7,"interpolation":"LINEAR","output":32},{"input":7,"interpolation":"LINEAR","output":33},{"input":7,"interpolation":"LINEAR","output":34},{"input":7,"interpolation":"LINEAR","output":35},{"input":7,"interpolation":"LINEAR","output":36},{"input":7,"interpolation":"LINEAR","output":37},{"input":7,"interpolation":"LINEAR","output":38},{"input":10,"interpolation":"LINEAR","output":39},{"input":7,"interpolation":"LINEAR","output":40},{"input":10,"interpolation":"LINEAR","output":41},{"input":7,"interpolation":"LINEAR","output":42},{"input":7,"interpolation":"LINEAR","output":43},{"input":7,"interpolation":"LINEAR","output":44},{"input":7,"interpolation":"LINEAR","output":45},{"input":7,"interpolation":"LINEAR","output":46},{"input":7,"interpolation":"LINEAR","output":47},{"input":7,"interpolation":"LINEAR","output":48},{"input":7,"interpolation":"LINEAR","output":49},{"input":7,"interpolation":"LINEAR","output":50},{"input":7,"interpolation":"LINEAR","output":51},{"input":7,"interpolation":"LINEAR","output":52},{"input":7,"interpolation":"LINEAR","output":53},{"input":7,"interpolation":"LINEAR","output":54},{"input":7,"interpolation":"LINEAR","output":55},{"input":7,"interpolation":"LINEAR","output":56},{"input":7,"interpolation":"LINEAR","output":57},{"input":7,"interpolation":"LINEAR","output":58},{"input":10,"interpolation":"LINEAR","output":59},{"input":7,"interpolation":"LINEAR","output":60},{"input":7,"interpolation":"LINEAR","output":61},{"input":10,"interpolation":"LINEAR","output":62},{"input":7,"interpolation":"LINEAR","output":63},{"input":7,"interpolation":"LINEAR","output":64},{"input":10,"interpolation":"LINEAR","output":65},{"input":7,"interpolation":"LINEAR","output":66},{"input":7,"interpolation":"LINEAR","output":67},{"input":10,"interpolation":"LINEAR","output":68},{"input":7,"interpolation":"LINEAR","output":69},{"input":7,"interpolation":"LINEAR","output":70},{"input":10,"interpolation":"LINEAR","output":71},{"input":7,"interpolation":"LINEAR","output":72},{"input":7,"interpolation":"LINEAR","output":73},{"input":10,"interpolation":"LINEAR","output":74},{"input":7,"interpolation":"LINEAR","output":75},{"input":7,"interpolation":"LINEAR","output":76},{"input":10,"interpolation":"LINEAR","output":77},{"input":7,"interpolation":"LINEAR","output":78},{"input":7,"interpolation":"LINEAR","output":79},{"input":10,"interpolation":"LINEAR","output":80},{"input":7,"interpolation":"LINEAR","output":81}]}],"materials":[{"doubleSided":true,"name":"Material.001","pbrMetallicRoughness":{}}],"meshes":[{"name":"Cube","primitives":[{"attributes":{"POSITION":0,"NORMAL":1,"TEXCOORD_0":2,"JOINTS_0":3,"WEIGHTS_0":4},"indices":5,"material":0}]}],"skins":[{"inverseBindMatrices":6,"joints":[48,5,4,1,0,3,2,7,6,12,11,10,9,8,17,16,15,14,13,22,21,20,19,18,27,26,25,24,23,32,31,30,29,28,37,36,35,34,33,42,41,40,39,38,47,46,45,44,43,49,50,51,52,53,54,55,56],"name":"Armature"}],"accessors":[{"bufferView":0,"componentType":5126,"count":1000,"max":[2.742279291152954,1.4045029878616333,2.0192716121673584],"min":[-2.742279291152954,-0.6434623599052429,-3.534085512161255],"type":"VEC3"},{"bufferView":1,"componentType":5126,"count":1000,"type":"VEC3"},{"bufferView":2,"componentType":5126,"count":1000,"type":"VEC2"},{"bufferView":3,"componentType":5121,"count":1000,"type":"VEC4"},{"bufferView":4,"componentType":5126,"count":1000,"type":"VEC4"},{"bufferView":5,"componentType":5123,"count":1500,"type":"SCALAR"},{"bufferView":6,"componentType":5126,"count":57,"type":"MAT4"},{"bufferView":7,"componentType":5126,"count":120,"max":[5],"min":[0.041666666666666664],"type":"SCALAR"},{"bufferView":8,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":9,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":10,"componentType":5126,"count":2,"max":[5],"min":[0.041666666666666664],"type":"SCALAR"},{"bufferView":11,"componentType":5126,"count":2,"type":"VEC3"},{"bufferView":12,"componentType":5126,"count":2,"type":"VEC3"},{"bufferView":13,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":14,"componentType":5126,"count":2,"type":"VEC3"},{"bufferView":15,"componentType":5126,"count":2,"type":"VEC3"},{"bufferView":16,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":17,"componentType":5126,"count":2,"type":"VEC3"},{"bufferView":18,"componentType":5126,"count":2,"type":"VEC3"},{"bufferView":19,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":20,"componentType":5126,"count":2,"type":"VEC3"},{"bufferView":21,"componentType":5126,"count":2,"type":"VEC3"},{"bufferView":22,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":23,"componentType":5126,"count":2,"type":"VEC3"},{"bufferView":24,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":25,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":26,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":27,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":28,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":29,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":30,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":31,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":32,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":33,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":34,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":35,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":36,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":37,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":38,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":39,"componentType":5126,"count":2,"type":"VEC3"},{"bufferView":40,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":41,"componentType":5126,"count":2,"type":"VEC3"},{"bufferView":42,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":43,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":44,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":45,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":46,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":47,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":48,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":49,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":50,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":51,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":52,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":53,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":54,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":55,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":56,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":57,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":58,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":59,"componentType":5126,"count":2,"type":"VEC4"},{"bufferView":60,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":61,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":62,"componentType":5126,"count":2,"type":"VEC4"},{"bufferView":63,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":64,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":65,"componentType":5126,"count":2,"type":"VEC4"},{"bufferView":66,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":67,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":68,"componentType":5126,"count":2,"type":"VEC4"},{"bufferView":69,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":70,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":71,"componentType":5126,"count":2,"type":"VEC4"},{"bufferView":72,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":73,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":74,"componentType":5126,"count":2,"type":"VEC4"},{"bufferView":75,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":76,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":77,"componentType":5126,"count":2,"type":"VEC4"},{"bufferView":78,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":79,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":80,"componentType":5126,"count":2,"type":"VEC4"},{"bufferView":81,"componentType":5126,"count":120,"type":"VEC3"}],"bufferViews":[{"buffer":0,"byteLength":12000,"byteOffset":0},{"buffer":0,"byteLength":12000,"byteOffset":12000},{"buffer":0,"byteLength":8000,"byteOffset":24000},{"buffer":0,"byteLength":4000,"byteOffset":32000},{"buffer":0,"byteLength":16000,"byteOffset":36000},{"buffer":0,"byteLength":3000,"byteOffset":52000},{"buffer":0,"byteLength":3648,"byteOffset":55000},{"buffer":0,"byteLength":480,"byteOffset":58648},{"buffer":0,"byteLength":1440,"byteOffset":59128},{"buffer":0,"byteLength":1920,"byteOffset":60568},{"buffer":0,"byteLength":8,"byteOffset":62488},{"buffer":0,"byteLength":24,"byteOffset":62496},{"buffer":0,"byteLength":24,"byteOffset":62520},{"buffer":0,"byteLength":1920,"byteOffset":62544},{"buffer":0,"byteLength":24,"byteOffset":64464},{"buffer":0,"byteLength":24,"byteOffset":64488},{"buffer":0,"byteLength":1920,"byteOffset":64512},{"buffer":0,"byteLength":24,"byteOffset":66432},{"buffer":0,"byteLength":24,"byteOffset":66456},{"buffer":0,"byteLength":1920,"byteOffset":66480},{"buffer":0,"byteLength":24,"byteOffset":68400},{"buffer":0,"byteLength":24,"byteOffset":68424},{"buffer":0,"byteLength":1920,"byteOffset":68448},{"buffer":0,"byteLength":24,"byteOffset":70368},{"buffer":0,"byteLength":1920,"byteOffset":70392},{"buffer":0,"byteLength":1920,"byteOffset":72312},{"buffer":0,"byteLength":1920,"byteOffset":74232},{"buffer":0,"byteLength":1920,"byteOffset":76152},{"buffer":0,"byteLength":1920,"byteOffset":78072},{"buffer":0,"byteLength":1920,"byteOffset":79992},{"buffer":0,"byteLength":1920,"byteOffset":81912},{"buffer":0,"byteLength":1920,"byteOffset":83832},{"buffer":0,"byteLength":1920,"byteOffset":85752},{"buffer":0,"byteLength":1920,"byteOffset":87672},{"buffer":0,"byteLength":1920,"byteOffset":89592},{"buffer":0,"byteLength":1920,"byteOffset":91512},{"buffer":0,"byteLength":1920,"byteOffset":93432},{"buffer":0,"byteLength":1920,"byteOffset":95352},{"buffer":0,"byteLength":1920,"byteOffset":97272},{"buffer":0,"byteLength":24,"byteOffset":99192},{"buffer":0,"byteLength":1920,"byteOffset":99216},{"buffer":0,"byteLength":24,"byteOffset":101136},{"buffer":0,"byteLength":1920,"byteOffset":101160},{"buffer":0,"byteLength":1920,"byteOffset":103080},{"buffer":0,"byteLength":1920,"byteOffset":105000},{"buffer":0,"byteLength":1920,"byteOffset":106920},{"buffer":0,"byteLength":1920,"byteOffset":108840},{"buffer":0,"byteLength":1920,"byteOffset":110760},{"buffer":0,"byteLength":1920,"byteOffset":112680},{"buffer":0,"byteLength":1920,"byteOffset":114600},{"buffer":0,"byteLength":1920,"byteOffset":116520},{"buffer":0,"byteLength":1920,"byteOffset":118440},{"buffer":0,"byteLength":1920,"byteOffset":120360},{"buffer":0,"byteLength":1920,"byteOffset":122280},{"buffer":0,"byteLength":1920,"byteOffset":124200},{"buffer":0,"byteLength":1920,"byteOffset":126120},{"buffer":0,"byteLength":1920,"byteOffset":128040},{"buffer":0,"byteLength":1920,"byteOffset":129960},{"buffer":0,"byteLength":1440,"byteOffset":131880},{"buffer":0,"byteLength":32,"byteOffset":133320},{"buffer":0,"byteLength":1440,"byteOffset":133352},{"buffer":0,"byteLength":1440,"byteOffset":134792},{"buffer":0,"byteLength":32,"byteOffset":136232},{"buffer":0,"byteLength":1440,"byteOffset":136264},{"buffer":0,"byteLength":1440,"byteOffset":137704},{"buffer":0,"byteLength":32,"byteOffset":139144},{"buffer":0,"byteLength":1440,"byteOffset":139176},{"buffer":0,"byteLength":1440,"byteOffset":140616},{"buffer":0,"byteLength":32,"byteOffset":142056},{"buffer":0,"byteLength":1440,"byteOffset":142088},{"buffer":0,"byteLength":1440,"byteOffset":143528},{"buffer":0,"byteLength":32,"byteOffset":144968},{"buffer":0,"byteLength":1440,"byteOffset":145000},{"buffer":0,"byteLength":1440,"byteOffset":146440},{"buffer":0,"byteLength":32,"byteOffset":147880},{"buffer":0,"byteLength":1440,"byteOffset":147912},{"buffer":0,"byteLength":1440,"byteOffset":149352},{"buffer":0,"byteLength":32,"byteOffset":150792},{"buffer":0,"byteLength":1440,"byteOffset":150824},{"buffer":0,"byteLength":1440,"byteOffset":152264},{"buffer":0,"byteLength":32,"byteOffset":153704},{"buffer":0,"byteLength":1440,"byteOffset":153736}],"buffers":[{"byteLength":155176,"uri":"data:application/octet-stream;base64,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BgAAAAYAAAAGAAAABgAAAAYAAAAGAAAABgAAAAYAAAAGAAAABgAAAAYAAAAGAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAALQAAAC0AAAAtAAAALQAAAC0AAAAtAAAALQAAAC0AAAAtAAAALQAAAC0AAAAtAAAALQAAAC0AAAAtAAAALQAAAC0AAAAtAAAALQAAAC0AAAAtAAAALQAAAC0AAAAtAAAALgAAAC4AAAAuAAAALgAAAC4AAAAuAAAALgAAAC4AAAAuAAAALgAAAC4AAAAuAAAALgAAAC4AAAAuAAAALgAAAC4AAAAuAAAALgAAAC4AAAAuAAAALgAAAC4AAAAuAAAALwAAAC8AAAAvAAAALwAAAC8AAAAvAAAALwAAAC8AAAAvAAAALwAAAC8AAAAvAAAALwAAAC8AAAAvAAAALwAAAC8AAAAvAAAALwAAAC8AAAAvAAAALwAAAC8AAAAvAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKQAAACkAAAApAAAAKQAAACkAAAApAAAAKQAAACkAAAApAAAAKQAAACkAAAApAAAAKQAAACkAAAApAAAAKQAAACkAAAApAAAAKQAAACkAAAApAAAAKQAAACkAAAApAAAAKgAAACoAAAAqAAAAKgAAACoAAAAqAAAAKgAAACoAAAAqAAAAKgAAACoAAAAqAAAAKgAAACoAAAAqAAAAKgAAACoAAAAqAAAAKgAAACoAAAAqAAAAKgAAACoAAAAqAAAAKwAAACsAAAArAAAAKwAAACsAAAArAAAAKwAAACsAAAArAAAAKwAAACsAAAArAAAAKwAAACsAAAArAAAAKwAAACsAAAArAAAAKwAAACsAAAArAAAAKwAAACsAAAArAAAAIwAAACMAAAAjAAAAIwAAACMAAAAjAAAAIwAAACMAAAAjAAAAIwAAACMAAAAjAAAAIwAAACMAAAAjAAAAIwAAACMAAAAjAAAAIwAAACMAAAAjAAAAIwAAACMAAAAjAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJQAAACUAAAAlAAAAJQAAACUAAAAlAAAAJQAAACUAAAAlAAAAJQAAACUAAAAlAAAAJQAAACUAAAAlAAAAJQAAACUAAAAlAAAAJQAAACUAAAAlAAAAJQAAACUAAAAlAAAAJgAAACYAAAAmAAAAJgAAACYAAAAmAAAAJgAAACYAAAAmAAAAJgAAACYAAAAmAAAAJgAAACYAAAAmAAAAJgAAACYAAAAmAAAAJgAAACYAAAAmAAAAJgAAACYAAAAmAAAAHgAAAB4AAAAeAAAAHgAAAB4AAAAeAAAAHgAAAB4AAAAeAAAAHgAAAB4AAAAeAAAAHgAAAB4AAAAeAAAAHgAAAB4AAAAeAAAAHgAAAB4AAAAeAAAAHgAAAB4AAAAeAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIQAAACEAAAAhAAAAIQAAACEAAAAhAAAAIQAAACEAAAAhAAAAIQAAACEAAAAhAAAAIQAAACEAAAAhAAAAIQAAACEAAAAhAAAAIQAAACEAAAAhAAAAIQAAACEAAAAhAAAAGQAAABkAAAAZAAAAGQAAABkAAAAZAAAAGQAAABkAAAAZAAAAGQAAABkAAAAZAAAAGQAAABkAAAAZAAAAGQAAABkAAAAZAAAAGQAAABkAAAAZAAAAGQAAABkAAAAZAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGwAAABsAAAAbAAAAGwAAABsAAAAbAAAAGwAAABsAAAAbAAAAGwAAABsAAAAbAAAAGwAAABsAAAAbAAAAGwAAABsAAAAbAAAAGwAAABsAAAAbAAAAGwAAABsAAAAbAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAFAAAABQAAAAUAAAAFAAAABQAAAAUAAAAFAAAABQAAAAUAAAAFAAAABQAAAAUAAAAFAAAABQAAAAUAAAAFAAAABQAAAAUAAAAFAAAABQAAAAUAAAAFAAAABQAAAAUAAAAFQAAABUAAAAVAAAAFQAAABUAAAAVAAAAFQAAABUAAAAVAAAAFQAAABUAAAAVAAAAFQAAABUAAAAVAAAAFQAAABUAAAAVAAAAFQAAABUAAAAVAAAAFQAAABUAAAAVAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEgAAABIAAAASAAAAEgAAABIAAAASAAAAEgAAABIAAAASAAAAEgAAABIAAAASAAAAEgAAABIAAAASAAAAEgAAABIAAAASAAAAEgAAABIAAAASAAAAEgAAABIAAAASAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACwAAAAsAAAALAAAACwAAAAsAAAALAAAACwAAAAsAAAALAAAACwAAAAsAAAALAAAACwAAAAsAAAALAAAACwAAAAsAAAALAAAACwAAAAsAAAALAAAACwAAAAsAAAALAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADQAAAA0AAAANAAAADQAAAA0AAAANAAAADQAAAA0AAAANAAAADQAAAA0AAAANAAAADQAAAA0AAAANAAAADQAAAA0AAAANAAAADQAAAA0AAAANAAAADQAAAA0AAAANAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAEADgAUAAEAFAAHAAoABgATAAoAEwAXABUAEgAMABUADAAPABAAAwAJABAACQAWAAUAAgAIAAUACAALABEADQAAABEAAAAEAEwAUgAsAEwALAAfACIAHgArACIAKwAvAEgAUAAkAEgAJAAnAEsAVQAhAEsAIQAuAFYATgAgAFYAIAAjACkAJQAYACkAGAAcADcAQgA8ADcAPAAxAD0APwA0AD0ANAAyAEYAOwA1AEYANQBAADAAMwA5ADAAOQA2AEQARwBBAEQAQQA+ADgAOgBFADgARQBDAB0AGgBPAB0ATwBXACgAGwBUACgAVABKAC0AKgBRAC0AUQBJABkAJgBTABkAUwBNAFgAWwBhAFgAYQBeAF8AYgBtAF8AbQBqAGwAbwBpAGwAaQBmAGUAaABdAGUAXQBaAGAAawBkAGAAZABZAG4AYwBcAG4AXABnAHAAcwB5AHAAeQB2AHgAegCFAHgAhQCDAIQAhwCBAIQAgQB+AH0AfwB0AH0AdAByAHcAggB8AHcAfABxAIYAewB1AIYAdQCAAIgAiwCRAIgAkQCOAJAAkgCdAJAAnQCbAJwAnwCZAJwAmQCWAJUAlwCMAJUAjACKAI8AmgCUAI8AlACJAJ4AkwCNAJ4AjQCYAKIAqACrAKIAqwClAKcAtAC3AKcAtwCqALIArACvALIArwC1AK0AoACjAK0AowCwAKYAoQCuAKYArgCzALYAsQCkALYApACpALoAwADDALoAwwC9AL8AzADOAL8AzgDBAMoAxADHAMoAxwDNAMYAuQC7AMYAuwDIAL4AuADFAL4AxQDLAM8AyQC8AM8AvADCANIA2ADbANIA2wDVANcA5ADmANcA5gDZAOIA3ADfAOIA3wDlAN4A0QDTAN4A0wDgANYA0ADdANYA3QDjAOcA4QDUAOcA1ADaAOkA6wDxAOkA8QDvAPAA8gD9APAA/QD7APwA/gD4APwA+AD2APUA9wDsAPUA7ADqAO4A+gD0AO4A9ADoAP8A8wDtAP8A7QD5AAABAwEJAQABCQEGAQcBCgEVAQcBFQESARQBFwERARQBEQEOAQ0BEAEFAQ0BBQECAQgBEwEMAQgBDAEBARYBCwEEARYBBAEPASkBHQEjASkBIwEvASgBJgEZASgBGQEbAS0BKwElAS0BJQEnASEBHwEsASEBLAEuARgBJAEqARgBKgEeARwBGgEgARwBIAEiATEBMwE6ATEBOgE4ATcBOQFFATcBRQFDAUQBRgE/AUQBPwE9AT4BQAE0AT4BNAEyATYBQgE8ATYBPAEwAUcBOwE1AUcBNQFBAUkBSwFRAUkBUQFPAVABUgFdAVABXQFbAVwBXgFYAVwBWAFWAVUBVwFMAVUBTAFKAU4BWgFUAU4BVAFIAV8BUwFNAV8BTQFZAWABYwFpAWABaQFmAWgBagF1AWgBdQFzAXQBdwFxAXQBcQFuAW0BbwFkAW0BZAFiAWcBcgFsAWcBbAFhAXYBawFlAXYBZQFwAYkBfQGDAYkBgwGPAYgBhgF5AYgBeQF7AY0BiwGFAY0BhQGHAYEBfwGMAYEBjAGOAXgBhAGKAXgBigF+AXwBegGAAXwBgAGCAZEBkwGZAZEBmQGXAZgBmgGlAZgBpQGjAaQBpgGgAaQBoAGeAZ0BnwGUAZ0BlAGSAZYBogGcAZYBnAGQAacBmwGVAacBlQGhAakBqwGxAakBsQGvAbABsgG9AbABvQG7AbwBvgG4AbwBuAG2AbUBtwGsAbUBrAGqAa4BugG0Aa4BtAGoAb8BswGtAb8BrQG5AcABwwHJAcAByQHGAcgBygHVAcgB1QHTAdQB1wHRAdQB0QHOAc0BzwHEAc0BxAHCAccB0gHMAccBzAHBAdYBywHFAdYBxQHQAekB3QHjAekB4wHvAegB5gHZAegB2QHbAe0B6wHlAe0B5QHnAeEB3wHsAeEB7AHuAdgB5AHqAdgB6gHeAdwB2gHgAdwB4AHiAfEB8wH5AfEB+QH3AfgB+gEFAvgBBQIDAgQCBgIAAgQCAAL+Af0B/wH0Af0B9AHyAfYBAgL8AfYB/AHwAQcC+wH1AQcC9QEBAgkCCwIRAgkCEQIPAhACEgIdAhACHQIbAhwCHgIYAhwCGAIWAhUCFwIMAhUCDAIKAg4CGgIUAg4CFAIIAh8CEwINAh8CDQIZAiACIwIpAiACKQImAigCKwI2AigCNgIzAjQCNwIxAjQCMQIuAiwCLwIkAiwCJAIhAicCMgItAicCLQIiAjUCKgIlAjUCJQIwAkkCPQJDAkkCQwJPAkgCRgI5AkgCOQI7Ak0CSwJFAk0CRQJHAkECPwJMAkECTAJOAjgCRAJKAjgCSgI+AjwCOgJAAjwCQAJCAlICVAJZAlICWQJXAlgCWgJmAlgCZgJkAmMCZQJgAmMCYAJeAl0CXwJTAl0CUwJRAlYCYgJcAlYCXAJQAmcCWwJVAmcCVQJhAmkCbwJzAmkCcwJtAm4CewJ/Am4CfwJyAnoCdAJ4AnoCeAJ+AnUCaAJsAnUCbAJ5AnACagJ2AnACdgJ8An0CdwJrAn0CawJxAoICiAKLAoICiwKFAocClAKXAocClwKKApICjAKPApICjwKVAo0CgAKDAo0CgwKQAoYCgQKOAoYCjgKTApYCkQKEApYChAKJAqcCrQKhAqcCoQKbAqgCnQKZAqgCmQKkAq8CqQKlAq8CpQKrAqMCrgKqAqMCqgKfApoCoAKsApoCrAKmApwCogKeApwCngKYArECtgK6ArECugK1ArcCwwLHArcCxwK7AsICvQLBAsICwQLGArwCsAK0ArwCtALAArgCsgK+ArgCvgLEAsUCvwKzAsUCswK5AskCzwLTAskC0wLNAs4C2wLfAs4C3wLSAtoC1ALYAtoC2ALeAtUCyALMAtUCzALZAtACygLWAtAC1gLcAt0C1wLLAt0CywLRAuIC6ALrAuIC6wLlAuYC8wL3AuYC9wLqAvIC7ALvAvIC7wL1Au0C4ALkAu0C5ALxAucC4QLuAucC7gL0AvYC8ALjAvYC4wLpAgcDDQMBAwcDAQP7AggD/QL5AggD+QIEAw8DCQMFAw8DBQMLAwMDDgMKAwMDCgP/AvoCAAMMA/oCDAMGA/wCAgP+AvwC/gL4AhEDFwMbAxEDGwMVAxYDIwMnAxYDJwMaAyIDHAMgAyIDIAMmAx0DEAMUAx0DFAMhAxgDEgMeAxgDHgMkAyUDHwMTAyUDEwMZAykDLwMzAykDMwMtAy4DOwM/Ay4DPwMyAzoDNAM4AzoDOAM+AzUDKAMsAzUDLAM5AzADKgM2AzADNgM8Az0DNwMrAz0DKwMxA0IDSANLA0IDSwNFA0YDUwNXA0YDVwNKA1IDTANPA1IDTwNVA00DQANEA00DRANRA0cDQQNOA0cDTgNUA1YDUANDA1YDQwNJA2cDbQNhA2cDYQNbA2gDXQNZA2gDWQNkA28DaQNlA28DZQNrA2MDbgNqA2MDagNfA1oDYANsA1oDbANmA1wDYgNeA1wDXgNYA3EDdwN7A3EDewN1A3YDgwOHA3YDhwN6A4IDfAOAA4IDgAOGA30DcAN0A30DdAOBA3gDcgN+A3gDfgOEA4UDfwNzA4UDcwN5A4kDjwOTA4kDkwONA44DmwOfA44DnwOSA5oDlAOYA5oDmAOeA5UDiAOMA5UDjAOZA5ADigOWA5ADlgOcA50DlwOLA50DiwORA6IDqAOrA6IDqwOlA6YDswO2A6YDtgOpA7IDrAOvA7IDrwO1A64DoQOkA64DpAOxA6cDoAOtA6cDrQO0A7cDsAOjA7cDowOqA8cDzQPBA8cDwQO7A8gDvQO5A8gDuQPEA88DyQPFA88DxQPLA8MDzgPKA8MD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TmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+cw9Rrv9jabWlCWo/0XvPPt5waq/JcWm1pQlqP9F7zz54BNKqTmtptaUJaj/Re88+jT7qr0Z4abWlCWo/0XvPPhnLKDCae2m1pwlqP9F7zz7t5U+vDU5ptaUJaj/Se88+goncLxOCabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4MKI+vempptaUJaj/Re88+WtPpL1ZeabWlCWo/0XvPPgDKKDCYe2m1pQlqP9F7zz5ssc+vzDBptaUJaj/Re88+NwGPsPxnabWlCWo/0XvPPuaqzi7ueWm1pQlqP9F7zz5TyIEve2hptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1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4G1iP+fAELztE0K9GXDnvvwEZD/Ubm27I9lMvcau3r5aKWY/BMwvuVYXVL21b9a+XhZoPyUpjjptxVa98bnSvvXsaD+J6Z652tVTvVoT1L5EoWg/nweCu5kqTL01Zde+DeRnP8KSFrxBaEG9XIfbvjjyZj9jLXm8ASo1vSVf376GCmY/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT94fJ68sMosvR944b6liGU/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT94fJ68sMosvR944b6liGU/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT94fJ68sMosvR944b6liGU/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT94fJ68sMosvR944b6liGU/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT94fJ68sMosvR944b6liGU/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT94fJ68sMosvR944b6liGU/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT94fJ68sMosvR944b6liGU/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT94fJ68sMosvR944b6liGU/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT94fJ68sMosvR944b6liGU/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT94fJ68sMosvR944b6liGU/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT94fJ68sMosvR944b6liGU/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT94fJ68sMosvR944b6liGU/fRmbvAekLb0AjOC+bMJlP2ROkryp1y+9SxjevoVaZj/IVIa8Gdcyvf2L2r7UMmc/zpJyvNAdNr0XVta+0i1oP13HWbzWQjm9ePDRvm0taT9Ir0S8M/07vd3jzb4ME2o/FLM0vFQhPr37xMq+N8BqP2DQK7zTXz+9SjDJvpwWaz+PoyK8iLNAvd5Oyr5r2Go/nfUQvDs+Q72B+M2+7gpqP/l78Lvg/ka9nPTRvpklaT8LXL27jU5LvQiK077sxmg/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4gzaP7VZD7+1Com/8SHGP7VZD7+1Com/SEyzP7VZD7+1Com/wkSiP7VZD7+1Com/OsSTP7VZD7+1Com/joOIP7VZD7+1Com/mDuBP7VZD7+1Com/Ykp9P7VZD7+1Com/Ykp9P7VZD7+1Com/Ykp9P7VZD7+1Com/hUnlsoVJ5bL0BDW/9AQ1P4VJ5bKFSeWy9AQ1v/QENT8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/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AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAP5IMgT+SDIE/AACAP5QDhD+UA4Q/AACAP/SeiD/0nog/AACAP6OYjj+jmI4/AACAP5KqlT+SqpU/AACAP7GOnT+xjp0/AACAP/D+pT/w/qU/AACAPz61rj8+ta4/AACAP4xrtz+Ma7c/AACAP8zbvz/M278/AACAP+q/xz/qv8c/AACAP9rRzj/a0c4/AACAP4rL1D+Ky9Q/AACAP+pm2T/qZtk/AACAP+ld3D/pXdw/AACAP3xq3T98at0/AACAP8BZ2z98at0/AACAP+iJ1T98at0/AACAP4SOzD98at0/AACAPyH7wD98at0/AACAP05jsz98at0/AACAP5hapD98at0/AACAP4x0lD98at0/AACAP7pEhD98at0/AACAP1+9aD98at0/AACAP/KrSj98at0/AACAP018Lz98at0/AACAP4ZVGD98at0/AACAP7xeBj98at0/AACAPyB+9T58at0/AACAPy077T58at0/AACAPy077T58at0/AACAPy077T58at0/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utbk/6lwQQLRZD797gqw/6lwQQLRZD79IT58/6lwQQLRZD79IT5k/6lwQQGaz/r6utZ0/6lwQQABNuL5IT6k/6lwQQDjNY76utbk/6lwQQDXNI757gsw/6lwQQDfNY75IT98/6lwQQAFNuL6tte8/6lwQQGez/r5HT/s/6lwQQLRZD7+utf8/6lwQQLNZD79HT/s/6lwQQLRZD7+ste8/6lwQQLRZD79GT98/6lwQQLRZD796gsw/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/x1UQQLRZD78EhME/0yMQQLRZD7/oRsY/PJwPQLRZD78pAs0/MpQOQLRZD7+ZudQ/4+AMQLRZD78Jcdw/fVcKQLRZD79KLOM/MM0GQLRZD78t7+c/KhcCQLRZD7+Dvek/uef0P7RZD790D+c/YGTeP7RZD7+//t8/DoDDP7RZD7+cAdY/ghanP7RZD79Djso/ggOMP7RZD7/qGr8/mEVqP7VZD7/IHbU/TKBKP7VZD78SDa4/pM4+P7RZD78DX6s/lpVQP7RZD78DX6s/8AZ9P7RZD78DX6s/wGabP7RZD78DX6s/CEq4P7RZD78DX6s/toLOP7RZD78DX6s/LmbXP7RZD78DX6s/VszRP7RZD78DX6s/osnCP7RZD78DX6s/UQ6tP7RZD78DX6s/okqTP7RZD78EX6s/qF1wP7RZD78EX6s/StY8P7RZD78EX6s/qF8RP7RZD78EX6s/hLTmPrRZD78EX6s/IE3QPrRZD78EX6s/IE3QPrRZD78EX6s/IE3QPrRZD78EX6s/hUnlsoVJ5bL0BDW/9AQ1P4VJ5bKFSeWy9AQ1v/QENT8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/ViaAP1YmgD8AAIA/V5eAP1eXgD8AAIA/AlCBPwJQgT8AAIA/VU2CP1VNgj8AAIA/T4yDP0+Mgz8AAIA/7wmFP+8JhT8AAIA/NMOGPzTDhj8AAIA/G7WIPxu1iD8AAIA/pdyKP6Xcij8AAIA/zzaNP882jT8AAIA/mcCPP5nAjz8AAIA/AHeSPwB3kj8AAIA/A1eVPwNXlT8AAIA/o12YP6NdmD8AAIA/3IebP9yHmz8AAIA/rtKeP67Snj8AAIA/FjuiPxY7oj8AAIA/Fr6lPxa+pT8AAIA/qlipP6pYqT8AAIA/0QetP9EHrT8AAIA/i8iwP4vIsD8AAIA/1Ze0P9WXtD8AAIA/rnK4P65yuD8AAIA/F1a8PxdWvD8AAIA/Cz/APws/wD8AAIA/iyrEP4sqxD8AAIA/lhXIP5YVyD8AAIA/Kv3LPyr9yz8AAIA/Rd7PP0Xezz8AAIA/57XTP+e10z8AAIA/DYHXPw2B1z8AAIA/tzzbP7c82z8AAIA/4+XeP+Pl3j8AAIA/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tFkPvxnErL4gwuK9tFkPvxnErL6ACWC9tFkPvxnErL5AFw69tFkPvxnErL5AFw69tFkPvxnErL5AFw69tFkPvxnErL6FSeWyhUnlMvQENT/0BDU/hUnlsoVJ5TL0BDU/9AQ1PwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD9vNoA/bzaAPwAAgD851oA/OdaAPwAAgD8Z2oE/GdqBPwAAgD/KPIM/yjyDPwAAgD8I+YQ/CPmEPwAAgD+QCYc/kAmHPwAAgD8caYk/HGmJPwAAgD9nEow/ZxKMPwAAgD8tAI8/LQCPPwAAgD8pLZI/KS2SPwAAgD8XlJU/F5SVPwAAgD+zL5k/sy+ZPwAAgD+3+pw/t/qcPwAAgD/g76A/4O+gPwAAgD/oCaU/6AmlPwAAgD+MQ6k/jEOpPwAAgD+Gl60/hpetPwAAgD+TALI/kwCyPwAAgD9tebY/bXm2PwAAgD/R/Lo/0fy6PwAAgD95hb8/eYW/PwAAgD8hDsQ/IQ7EPwAAgD+Ekcg/hJHIPwAAgD9eCs0/XgrNPwAAgD9qc9E/anPRPwAAgD9mx9U/ZsfVPwAAgD8KAdo/CgHaPwAAgD8QG94/EBvePwAAgD86EOI/OhDiPwAAgD8/2+U/P9vlPwAAgD/Zduk/2XbpPwAAgD/I3ew/yN3sPwAAgD/FCvA/xQrwPwAAgD+L+PI/i/jyPwAAgD/VofU/1aH1PwAAgD9hAfg/YQH4PwAAgD/pEfo/6RH6PwAAgD8mzvs/Js77PwAAgD/YMP0/2DD9PwAAgD+4NP4/uDT+PwAAgD+D1P4/g9T+PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD+Wsfo/8Qr/PwAAgD/YxO4/8Qr/PwAAgD+y89w/8Qr/PwAAgD8c7cY/8Qr/PwAAgD8OYK4/8Qr/PwAAgD+F+5Q/8Qr/PwAAgD/v3Hg/8Qr/PwAAgD/Az0w/8Qr/PwAAgD9wLSk/8Qr/PwAAgD/2UxE/8Qr/PwAAgD9CoQg/8Qr/PwAAgD9CoQg/8Qr/PwAAgD9CoQg/8Qr/PweaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8A2zSO+UHMIvQeaF8A4zWO+FTNePAeaF8ACTbi+kvnuPQeaF8Bns/6+XhZhPgeaF8C0WQ+/MIuIPgeaF8C0WQ+/kkl6PgeaF8C0WQ+/+K86PgeaF8C0WQ+/jPmyPQeaF8C0WQ+/UHMIvQeaF8C0WQ+/brYtvgeaF8C0WQ+/aA6ZvgeaF8CzWQ+/znTKvgeaF8C0WQ+/N9vdvgeaF8Bms/6+Ntu9vgeaF8AATbi+oeluvgeaF8A2zWO+qznEvQeaF8A2zSO+UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvSVLF8C0WQ+/gVX5vDFiFsC0WQ+/Xbu2vLDkFMC0WQ+/HHwevCvYEsC0WQ+/MPLbOypCEMC0WQ+/mM7VPDMoDcC0WQ+/HolDPc+PCcC0WQ+/ZiiRPYZ+BcC0WQ+/tBXCPd35AMC0WQ+/AAPzPb0O+L+0WQ+/bDMRPiBZ7b+0WQ+/4lsnPvTd4b+0WQ+/JDY7Pkmo1b+0WQ+/dv1LPizDyL+0WQ+/Hu1YPq85u7+0WQ+/ZEBhPt8Wrb+0WQ+/iDJkPpRsnr+0WQ+/iDJkPs9nj7+0WQ+/iDJkPlk8gL+0WQ+/iDJkPvI7Yr+0WQ+/iDJkPvCARL+0WQ+/iDJkPkCvJ7+0WQ+/iDJkPnYuDL+0WQ+/iDJkPjjM5L60WQ+/iDJkPpB7tb60WQ+/iDJkPig6i760WQ+/iDJkPvCtTb60WQ+/iDJkPpBCEr60WQ+/iDJkPuCgy720WQ+/iDJkPmDrk720WQ+/iDJkPqChgL20WQ+/iDJkPqChgL20WQ+/iDJkPqChgL20WQ+/iDJkPoVJ5bKFSeUy9AQ1P/QENT+FSeWyhUnlMvQENT/0BDU/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAP5IMgT+SDIE/AACAP5QDhD+UA4Q/AACAP/SeiD/0nog/AACAP6OYjj+jmI4/AACAP5KqlT+SqpU/AACAP7GOnT+xjp0/AACAP/D+pT/w/qU/AACAPz61rj8+ta4/AACAP4xrtz+Ma7c/AACAP8zbvz/M278/AACAP+q/xz/qv8c/AACAP9rRzj/a0c4/AACAP4rL1D+Ky9Q/AACAP+pm2T/qZtk/AACAP+ld3D/pXdw/AACAP3xq3T98at0/AACAP8BZ2z98at0/AACAP+iJ1T98at0/AACAP4SOzD98at0/AACAPyH7wD98at0/AACAP05jsz98at0/AACAP5hapD98at0/AACAP4x0lD98at0/AACAP7pEhD98at0/AACAP1+9aD98at0/AACAP/KrSj98at0/AACAP018Lz98at0/AACAP4ZVGD98at0/AACAP7xeBj98at0/AACAPyB+9T58at0/AACAPy077T58at0/AACAPy077T58at0/AACAPy077T58at0/B5oXwLRZD7++SjI/B5oXwLRZD7++SjI/B5oXwLRZD7++SjI/B5oXwLRZD7++SjI/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AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/ViaAP1YmgD8AAIA/V5eAP1eXgD8AAIA/AlCBPwJQgT8AAIA/VU2CP1VNgj8AAIA/T4yDP0+Mgz8AAIA/7wmFP+8JhT8AAIA/NMOGPzTDhj8AAIA/G7WIPxu1iD8AAIA/pdyKP6Xcij8AAIA/zzaNP882jT8AAIA/mcCPP5nAjz8AAIA/AHeSPwB3kj8AAIA/A1eVPwNXlT8AAIA/o12YP6NdmD8AAIA/3IebP9yHmz8AAIA/rtKeP67Snj8AAIA/FjuiPxY7oj8AAIA/Fr6lPxa+pT8AAIA/qlipP6pYqT8AAIA/0QetP9EHrT8AAIA/i8iwP4vIsD8AAIA/1Ze0P9WXtD8AAIA/rnK4P65yuD8AAIA/F1a8PxdWvD8AAIA/Cz/APws/wD8AAIA/iyrEP4sqxD8AAIA/lhXIP5YVyD8AAIA/Kv3LPyr9yz8AAIA/Rd7PP0Xezz8AAIA/57XTP+e10z8AAIA/DYHXPw2B1z8AAIA/tzzbP7c82z8AAIA/4+XeP+Pl3j8AAIA/kXniP5F54j8AAIA/vvTlP7705T8AAIA/aFTpP2hU6T8AAIA/lJXsP5SV7D8AAIA/OLXvPzi17z8AAIA/VrDyP1aw8j8AAIA/7oP1P+6D9T8AAIA//yz4P/8s+D8AAIA/hqj6P4ao+j8AAIA/gvP8P4Lz/D8AAIA/8Qr/P/EK/z8AAIA/+nUAQPp1AEAAAIA/E0oBQBNKAUAAAIA/pAACQKQAAkAAAIA/jJgCQIyYAkAAAIA/qBADQKgQA0AAAIA/2GcDQNhnA0AAAIA//ZwDQP2cA0AAAIA/9a4DQPWuA0AAAIA/9a4DQPWuA0AAAIA/9a4DQPWuA0AAAIA/9a4DQPWuA0AAAIA/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AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD+SDIE/kgyBPwAAgD+UA4Q/lAOEPwAAgD/0nog/9J6IPwAAgD+jmI4/o5iOPwAAgD+SqpU/kqqVPwAAgD+xjp0/sY6dPwAAgD/w/qU/8P6lPwAAgD8+ta4/PrWuPwAAgD+Ma7c/jGu3PwAAgD/M278/zNu/PwAAgD/qv8c/6r/HPwAAgD/a0c4/2tHOPwAAgD+Ky9Q/isvUPwAAgD/qZtk/6mbZPwAAgD/pXdw/6V3cPwAAgD98at0/fGrdPwAAgD/AWds/fGrdPwAAgD/oidU/fGrdPwAAgD+Ejsw/fGrdPwAAgD8h+8A/fGrdPwAAgD9OY7M/fGrdPwAAgD+YWqQ/fGrdPwAAgD+MdJQ/fGrdPwAAgD+6RIQ/fGrdPwAAgD9fvWg/fGrdPwAAgD/yq0o/fGrdPwAAgD9NfC8/fGrdPwAAgD+GVRg/fGrdPwAAgD+8XgY/fGrdPwAAgD8gfvU+fGrdPwAAgD8tO+0+fGrdPwAAgD8tO+0+fGrdPwAAgD8tO+0+fGrdPw=="}]} diff --git a/games/devtest/mods/lighting/init.lua b/games/devtest/mods/lighting/init.lua index 7b4392fb8..20448d925 100644 --- a/games/devtest/mods/lighting/init.lua +++ b/games/devtest/mods/lighting/init.lua @@ -14,7 +14,21 @@ local lighting_sections = { {n = "speed_bright_dark", d = "Dark scene adaptation speed", min = -10, max = 10, type="log2"}, {n = "center_weight_power", d = "Power factor for center-weighting", min = 0.1, max = 10}, } - } + }, + { + n = "bloom", d = "Bloom", + entries = { + {n = "intensity", d = "Intensity", min = 0, max = 1}, + {n = "strength_factor", d = "Strength Factor", min = 0.1, max = 10}, + {n = "radius", d = "Radius", min = 0.1, max = 8}, + }, + }, + { + n = "volumetric_light", d = "Volumetric Lighting", + entries = { + {n = "strength", d = "Strength", min = 0, max = 1}, + }, + }, } local function dump_lighting(lighting) @@ -59,38 +73,40 @@ minetest.register_chatcommand("set_lighting", { local lighting = player:get_lighting() local exposure = lighting.exposure or {} - local form = { - "formspec_version[2]", - "size[15,30]", - "position[0.99,0.15]", - "anchor[1,0]", - "padding[0.05,0.1]", - "no_prepend[]" - }; - + local content = {} local line = 1 for _,section in ipairs(lighting_sections) do local parameters = section.entries or {} local state = lighting[section.n] or {} - table.insert(form, "label[1,"..line..";"..section.d.."]") + table.insert(content, "label[1,"..line..";"..section.d.."]") line = line + 1 for _,v in ipairs(parameters) do - table.insert(form, "label[2,"..line..";"..v.d.."]") - table.insert(form, "scrollbaroptions[min=0;max=1000;smallstep=10;largestep=100;thumbsize=10]") + table.insert(content, "label[2,"..line..";"..v.d.."]") + table.insert(content, "scrollbaroptions[min=0;max=1000;smallstep=10;largestep=100;thumbsize=10]") local value = state[v.n] if v.type == "log2" then value = math.log(value or 1) / math.log(2) end local sb_scale = math.floor(1000 * (math.max(v.min, value or 0) - v.min) / (v.max - v.min)) - table.insert(form, "scrollbar[2,"..(line+0.7)..";12,1;horizontal;"..section.n.."."..v.n..";"..sb_scale.."]") + table.insert(content, "scrollbar[2,"..(line+0.7)..";12,1;horizontal;"..section.n.."."..v.n..";"..sb_scale.."]") line = line + 2.7 end line = line + 1 end + local form = { + "formspec_version[2]", + "size[15,", line, "]", + "position[0.99,0.15]", + "anchor[1,0]", + "padding[0.05,0.1]", + "no_prepend[]", + } + table.insert_all(form, content) + minetest.show_formspec(player_name, "lighting", table.concat(form)) local debug_value = dump_lighting(lighting) local debug_ui = player:hud_add({type="text", position={x=0.1, y=0.3}, scale={x=1,y=1}, alignment = {x=1, y=1}, text=debug_value, number=0xFFFFFF}) diff --git a/games/devtest/mods/testabms/README.md b/games/devtest/mods/testabms/README.md new file mode 100644 index 000000000..60fa6d656 --- /dev/null +++ b/games/devtest/mods/testabms/README.md @@ -0,0 +1,6 @@ +# Test ABMs + +This mod contains a nodes and related ABM actions. +By placing these nodes, you can test basic ABM behaviours. + +There are separate tests for ABM `chance`, `interval`, `min_y`, `max_y`, `neighbor` and `without_neighbor` fields. diff --git a/games/devtest/mods/testabms/after_node.lua b/games/devtest/mods/testabms/after_node.lua new file mode 100644 index 000000000..64cdfb484 --- /dev/null +++ b/games/devtest/mods/testabms/after_node.lua @@ -0,0 +1,12 @@ + +local S = minetest.get_translator("testnodes") + +-- After ABM node +minetest.register_node("testabms:after_abm", { + description = S("After ABM processed node."), + drawtype = "normal", + tiles = { "testabms_after_node.png" }, + + groups = { dig_immediate = 3 }, +}) + diff --git a/games/devtest/mods/testabms/chances.lua b/games/devtest/mods/testabms/chances.lua new file mode 100644 index 000000000..95f416b45 --- /dev/null +++ b/games/devtest/mods/testabms/chances.lua @@ -0,0 +1,56 @@ +-- test ABMs with different chances + +local S = minetest.get_translator("testnodes") + +-- ABM chance 5 node +minetest.register_node("testabms:chance_5", { + description = S("Node for test ABM chance_5"), + drawtype = "normal", + tiles = { "testabms_wait_node.png" }, + + groups = { dig_immediate = 3 }, + + on_construct = function (pos) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "Waiting for ABM testabms:chance_5") + end, +}) + +minetest.register_abm({ + label = "testabms:chance_5", + nodenames = "testabms:chance_5", + interval = 10, + chance = 5, + action = function (pos) + minetest.swap_node(pos, {name="testabms:after_abm"}) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "ABM testabsm:chance_5 changed this node.") + end +}) + +-- ABM chance 20 node +minetest.register_node("testabms:chance_20", { + description = S("Node for test ABM chance_20"), + drawtype = "normal", + tiles = { "testabms_wait_node.png" }, + + groups = { dig_immediate = 3 }, + + on_construct = function (pos) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "Waiting for ABM testabms:chance_20") + end, +}) + +minetest.register_abm({ + label = "testabms:chance_20", + nodenames = "testabms:chance_20", + interval = 10, + chance = 20, + action = function (pos) + minetest.swap_node(pos, {name="testabms:after_abm"}) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "ABM testabsm:chance_20 changed this node.") + end +}) + diff --git a/games/devtest/mods/testabms/init.lua b/games/devtest/mods/testabms/init.lua new file mode 100644 index 000000000..7830d8436 --- /dev/null +++ b/games/devtest/mods/testabms/init.lua @@ -0,0 +1,7 @@ +local path = minetest.get_modpath(minetest.get_current_modname()) + +dofile(path.."/after_node.lua") +dofile(path.."/chances.lua") +dofile(path.."/intervals.lua") +dofile(path.."/min_max.lua") +dofile(path.."/neighbors.lua") diff --git a/games/devtest/mods/testabms/intervals.lua b/games/devtest/mods/testabms/intervals.lua new file mode 100644 index 000000000..57b58faa5 --- /dev/null +++ b/games/devtest/mods/testabms/intervals.lua @@ -0,0 +1,56 @@ +-- test ABMs with different interval + +local S = minetest.get_translator("testnodes") + +-- ABM inteval 1 node +minetest.register_node("testabms:interval_1", { + description = S("Node for test ABM interval_1"), + drawtype = "normal", + tiles = { "testabms_wait_node.png" }, + + groups = { dig_immediate = 3 }, + + on_construct = function (pos) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "Waiting for ABM testabms:interval_1") + end, +}) + +minetest.register_abm({ + label = "testabms:interval_1", + nodenames = "testabms:interval_1", + interval = 1, + chance = 1, + action = function (pos) + minetest.swap_node(pos, {name="testabms:after_abm"}) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "ABM testabsm:interval_1 changed this node.") + end +}) + +-- ABM interval 60 node +minetest.register_node("testabms:interval_60", { + description = S("Node for test ABM interval_60"), + drawtype = "normal", + tiles = { "testabms_wait_node.png" }, + + groups = { dig_immediate = 3 }, + + on_construct = function (pos) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "Waiting for ABM testabms:interval_60") + end, +}) + +minetest.register_abm({ + label = "testabms:interval_60", + nodenames = "testabms:interval_60", + interval = 60, + chance = 1, + action = function (pos) + minetest.swap_node(pos, {name="testabms:after_abm"}) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "ABM testabsm:interval_60 changed this node.") + end +}) + diff --git a/games/devtest/mods/testabms/min_max.lua b/games/devtest/mods/testabms/min_max.lua new file mode 100644 index 000000000..62f1ccd53 --- /dev/null +++ b/games/devtest/mods/testabms/min_max.lua @@ -0,0 +1,58 @@ +-- test ABMs with min_y and max_y + +local S = minetest.get_translator("testnodes") + +-- ABM min_y node +minetest.register_node("testabms:min_y", { + description = S("Node for test ABM min_y."), + drawtype = "normal", + tiles = { "testabms_wait_node.png" }, + + groups = { dig_immediate = 3 }, + + on_construct = function (pos) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "Waiting for ABM testabms:min_y at y "..pos.y.." with min_y = 0") + end, +}) + +minetest.register_abm({ + label = "testabms:min_y", + nodenames = "testabms:min_y", + interval = 10, + chance = 1, + min_y = 0, + action = function (pos) + minetest.swap_node(pos, {name="testabms:after_abm"}) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "ABM testabsm:min_y changed this node.") + end +}) + +-- ABM max_y node +minetest.register_node("testabms:max_y", { + description = S("Node for test ABM max_y."), + drawtype = "normal", + tiles = { "testabms_wait_node.png" }, + + groups = { dig_immediate = 3 }, + + on_construct = function (pos) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "Waiting for ABM testabms:max_y at y "..pos.y.." with max_y = 0") + end, +}) + +minetest.register_abm({ + label = "testabms:max_y", + nodenames = "testabms:max_y", + interval = 10, + chance = 1, + max_y = 0, + action = function (pos) + minetest.swap_node(pos, {name="testabms:after_abm"}) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "ABM testabsm:max_y changed this node.") + end +}) + diff --git a/games/devtest/mods/testabms/mod.conf b/games/devtest/mods/testabms/mod.conf new file mode 100644 index 000000000..ad74cd2ed --- /dev/null +++ b/games/devtest/mods/testabms/mod.conf @@ -0,0 +1,2 @@ +name = testabms +description = Contains some nodes for test ABMs. diff --git a/games/devtest/mods/testabms/neighbors.lua b/games/devtest/mods/testabms/neighbors.lua new file mode 100644 index 000000000..42ce47dff --- /dev/null +++ b/games/devtest/mods/testabms/neighbors.lua @@ -0,0 +1,99 @@ +-- test ABMs with neighbor and without_neighbor + +local S = minetest.get_translator("testnodes") + +-- ABM required neighbor +minetest.register_node("testabms:required_neighbor", { + description = S("Node for test ABM required_neighbor.") .. "\n" + .. S("Sensitive neighbor node is testnodes:normal."), + drawtype = "normal", + tiles = { "testabms_wait_node.png" }, + + groups = { dig_immediate = 3 }, + + on_construct = function (pos) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", + "Waiting for ABM testabms:required_neighbor " + .. "(normal drawtype testnode sensitive)") + end, +}) + +minetest.register_abm({ + label = "testabms:required_neighbor", + nodenames = "testabms:required_neighbor", + neighbors = {"testnodes:normal"}, + interval = 1, + chance = 1, + action = function (pos) + minetest.swap_node(pos, {name="testabms:after_abm"}) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", + "ABM testabsm:required_neighbor changed this node.") + end +}) + +-- ABM missing neighbor node +minetest.register_node("testabms:missing_neighbor", { + description = S("Node for test ABM missing_neighbor.") .. "\n" + .. S("Sensitive neighbor node is testnodes:normal."), + drawtype = "normal", + tiles = { "testabms_wait_node.png" }, + + groups = { dig_immediate = 3 }, + + on_construct = function (pos) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", + "Waiting for ABM testabms:missing_neighbor" + .. " (normal drawtype testnode sensitive)") + end, +}) + +minetest.register_abm({ + label = "testabms:missing_neighbor", + nodenames = "testabms:missing_neighbor", + without_neighbors = {"testnodes:normal"}, + interval = 1, + chance = 1, + action = function (pos) + minetest.swap_node(pos, {name="testabms:after_abm"}) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", + "ABM testabsm:missing_neighbor changed this node.") + end +}) + +-- ABM required and missing neighbor node +minetest.register_node("testabms:required_missing_neighbor", { + description = S("Node for test ABM required_missing_neighbor.") .. "\n" + .. S("Sensitive neighbor nodes are testnodes:normal and testnodes:glasslike."), + drawtype = "normal", + tiles = { "testabms_wait_node.png" }, + + groups = { dig_immediate = 3 }, + + on_construct = function (pos) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", + "Waiting for ABM testabms:required_missing_neighbor" + .. " (wint normal drawtype testnode and no glasslike" + .. " drawtype testnode sensitive)") + end, +}) + +minetest.register_abm({ + label = "testabms:required_missing_neighbor", + nodenames = "testabms:required_missing_neighbor", + neighbors = {"testnodes:normal"}, + without_neighbors = {"testnodes:glasslike"}, + interval = 1, + chance = 1, + action = function (pos) + minetest.swap_node(pos, {name="testabms:after_abm"}) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", + "ABM testabsm:required_missing_neighbor changed this node.") + end +}) + diff --git a/games/devtest/mods/testabms/textures/testabms_after_node.png b/games/devtest/mods/testabms/textures/testabms_after_node.png new file mode 100644 index 000000000..dab87594b Binary files /dev/null and b/games/devtest/mods/testabms/textures/testabms_after_node.png differ diff --git a/games/devtest/mods/testabms/textures/testabms_wait_node.png b/games/devtest/mods/testabms/textures/testabms_wait_node.png new file mode 100644 index 000000000..a9bd9a36f Binary files /dev/null and b/games/devtest/mods/testabms/textures/testabms_wait_node.png differ diff --git a/games/devtest/mods/testformspec/formspec.lua b/games/devtest/mods/testformspec/formspec.lua index 8d0b759f5..f8f17798b 100644 --- a/games/devtest/mods/testformspec/formspec.lua +++ b/games/devtest/mods/testformspec/formspec.lua @@ -299,7 +299,18 @@ local scroll_fs = "scrollbaroptions[max=170]".. -- lowest seen pos is: 0.1*170+6=23 (factor*max+height) "scrollbar[7.5,0;0.3,4;vertical;scrbar;0]".. "scrollbar[8,0;0.3,4;vertical;scrbarhmmm;0]".. - "dropdown[0,6;2;hmdrpdwnnn;Outside,of,container;1]" + "dropdown[0,6;2;hmdrpdwnnn;Outside,of,container;1]".. + "scroll_container[0,8;10,4;scrbar420;vertical;0.1;2]".. + "button[0.5,0.5;10,1;;Container with padding=2]".. + "list[current_player;main;0,5;8,4;]".. + "scroll_container_end[]".. + "scrollbar[10.1,8;0.5,4;vertical;scrbar420;0]".. + -- Buttons for scale comparison + "button[11,8;1,1;;0]".. + "button[11,9;1,1;;1]".. + "button[11,10;1,1;;2]".. + "button[11,11;1,1;;3]".. + "button[11,12;1,1;;4]" --style_type[label;textcolor=green] --label[0,0;Green] @@ -462,7 +473,7 @@ mouse control = true] ]], -- Scroll containers - "formspec_version[3]size[12,13]" .. + "formspec_version[7]size[12,13]" .. scroll_fs, -- Sound diff --git a/games/devtest/mods/testnodes/drawtypes.lua b/games/devtest/mods/testnodes/drawtypes.lua index 087d09eff..4a657b739 100644 --- a/games/devtest/mods/testnodes/drawtypes.lua +++ b/games/devtest/mods/testnodes/drawtypes.lua @@ -98,6 +98,23 @@ minetest.register_node("testnodes:allfaces", { groups = { dig_immediate = 3 }, }) +minetest.register_node("testnodes:allfaces_6", { + description = S("\"allfaces 6 Textures\" Drawtype Test Node").."\n".. + S("Transparent node with visible internal backfaces"), + drawtype = "allfaces", + paramtype = "light", + tiles = { + "testnodes_allfaces.png^[colorize:red", + "testnodes_allfaces.png^[colorize:orange", + "testnodes_allfaces.png^[colorize:yellow", + "testnodes_allfaces.png^[colorize:green", + "testnodes_allfaces.png^[colorize:blue", + "testnodes_allfaces.png^[colorize:purple" + }, + + groups = { dig_immediate = 3 }, +}) + local allfaces_optional_tooltip = "".. S("Rendering depends on 'leaves_style' setting:").."\n".. S("* 'fancy': transparent with visible internal backfaces").."\n".. diff --git a/games/devtest/mods/testnodes/textures.lua b/games/devtest/mods/testnodes/textures.lua index 96f291d6a..b95fbd62e 100644 --- a/games/devtest/mods/testnodes/textures.lua +++ b/games/devtest/mods/testnodes/textures.lua @@ -52,6 +52,12 @@ minetest.register_node("testnodes:fill_positioning_reference", { groups = {dig_immediate = 3}, }) +minetest.register_node("testnodes:modifier_mask", { + description = S("[mask Modifier Test Node"), + tiles = {"testnodes_128x128_rgb.png^[mask:testnodes_mask_WRGBKW.png"}, + groups = {dig_immediate = 3}, +}) + -- Node texture transparency test local alphas = { 64, 128, 191 } diff --git a/games/devtest/mods/testnodes/textures/testnodes_128x128_rgb.png b/games/devtest/mods/testnodes/textures/testnodes_128x128_rgb.png new file mode 100644 index 000000000..060d8e67a Binary files /dev/null and b/games/devtest/mods/testnodes/textures/testnodes_128x128_rgb.png differ diff --git a/games/devtest/mods/testnodes/textures/testnodes_mask_WRGBKW.png b/games/devtest/mods/testnodes/textures/testnodes_mask_WRGBKW.png new file mode 100644 index 000000000..03ab71e3f Binary files /dev/null and b/games/devtest/mods/testnodes/textures/testnodes_mask_WRGBKW.png differ diff --git a/games/devtest/mods/unittests/inside_mapgen_env.lua b/games/devtest/mods/unittests/inside_mapgen_env.lua index a8df004de..f6f8513ce 100644 --- a/games/devtest/mods/unittests/inside_mapgen_env.lua +++ b/games/devtest/mods/unittests/inside_mapgen_env.lua @@ -22,9 +22,11 @@ local function do_tests() assert(core.registered_items["unittests:description_test"].on_place == true) end --- there's no (usable) communcation path between mapgen and the regular env --- so we just run the test unconditionally -do_tests() +-- first thread to get here runs the tests +if core.ipc_cas("unittests:mg_once", nil, true) then + -- this is checked from the main env + core.ipc_set("unittests:mg", { pcall(do_tests) }) +end core.register_on_generated(function(vm, pos1, pos2, blockseed) local n = tonumber(core.get_mapgen_setting("chunksize")) * 16 - 1 diff --git a/games/devtest/mods/unittests/misc.lua b/games/devtest/mods/unittests/misc.lua index 6ff5c7e84..6a2a33fa7 100644 --- a/games/devtest/mods/unittests/misc.lua +++ b/games/devtest/mods/unittests/misc.lua @@ -254,3 +254,43 @@ local function test_gennotify_api() assert(#custom == 0, "custom ids not empty") end unittests.register("test_gennotify_api", test_gennotify_api) + +-- <=> inside_mapgen_env.lua +local function test_mapgen_env(cb) + -- emerge threads start delayed so this can take a second + local res = core.ipc_get("unittests:mg") + if res == nil then + return core.after(0, test_mapgen_env, cb) + end + -- handle error status + if res[1] then + cb() + else + cb(res[2]) + end +end +unittests.register("test_mapgen_env", test_mapgen_env, {async=true}) + +local function test_ipc_vector_preserve(cb) + -- the IPC also uses register_portable_metatable + core.ipc_set("unittests:v", vector.new(4, 0, 4)) + local v = core.ipc_get("unittests:v") + assert(type(v) == "table") + assert(vector.check(v)) +end +unittests.register("test_ipc_vector_preserve", test_ipc_vector_preserve) + +local function test_ipc_poll(cb) + core.ipc_set("unittests:flag", nil) + assert(core.ipc_poll("unittests:flag", 1) == false) + + -- Note that unlike the async result callback - which has to wait for the + -- next server step - the IPC is instant + local t0 = core.get_us_time() + core.handle_async(function() + core.ipc_set("unittests:flag", true) + end, function() end) + assert(core.ipc_poll("unittests:flag", 1000) == true, "Wait failed (or slow machine?)") + print("delta: " .. (core.get_us_time() - t0) .. "us") +end +unittests.register("test_ipc_poll", test_ipc_poll) diff --git a/games/devtest/mods/unittests/player.lua b/games/devtest/mods/unittests/player.lua index 7650d5f57..f8945f320 100644 --- a/games/devtest/mods/unittests/player.lua +++ b/games/devtest/mods/unittests/player.lua @@ -42,41 +42,97 @@ unittests.register("test_hpchangereason", run_hpchangereason_tests, {player=true -- local expected_diff = nil +local hpchange_counter = 0 +local die_counter = 0 core.register_on_player_hpchange(function(player, hp_change, reason) if expected_diff then assert(hp_change == expected_diff) + hpchange_counter = hpchange_counter + 1 end end) +core.register_on_dieplayer(function() + die_counter = die_counter + 1 +end) + +local function hp_diference_test(player, hp_max) + assert(hp_max >= 22) -local function run_hp_difference_tests(player) local old_hp = player:get_hp() local old_hp_max = player:get_properties().hp_max - expected_diff = nil - player:set_properties({hp_max = 30}) - player:set_hp(22) + hpchange_counter = 0 + die_counter = 0 - -- final HP value is clamped to >= 0 before difference calculation - expected_diff = -22 + expected_diff = nil + player:set_properties({hp_max = hp_max}) + player:set_hp(22) + assert(player:get_hp() == 22) + assert(hpchange_counter == 0) + assert(die_counter == 0) + + -- HP difference is not clamped + expected_diff = -25 player:set_hp(-3) - -- and actual final HP value is clamped to >= 0 too + -- actual final HP value is clamped to >= 0 assert(player:get_hp() == 0) + assert(hpchange_counter == 1) + assert(die_counter == 1) expected_diff = 22 player:set_hp(22) assert(player:get_hp() == 22) + assert(hpchange_counter == 2) + assert(die_counter == 1) - -- final HP value is clamped to <= U16_MAX before difference calculation - expected_diff = 65535 - 22 + -- Integer overflow is prevented + -- so result is S32_MIN, not S32_MIN - 22 + expected_diff = -2147483648 + player:set_hp(-2147483648) + -- actual final HP value is clamped to >= 0 + assert(player:get_hp() == 0) + assert(hpchange_counter == 3) + assert(die_counter == 2) + + -- Damage is ignored if player is already dead (hp == 0) + expected_diff = "never equal" + player:set_hp(-11) + assert(player:get_hp() == 0) + -- no on_player_hpchange or on_dieplayer call expected + assert(hpchange_counter == 3) + assert(die_counter == 2) + + expected_diff = 11 + player:set_hp(11) + assert(player:get_hp() == 11) + assert(hpchange_counter == 4) + assert(die_counter == 2) + + -- HP difference is not clamped + expected_diff = 1000000 - 11 player:set_hp(1000000) - -- and actual final HP value is clamped to <= hp_max - assert(player:get_hp() == 30) + -- actual final HP value is clamped to <= hp_max + assert(player:get_hp() == hp_max) + assert(hpchange_counter == 5) + assert(die_counter == 2) + + -- "Healing" is not ignored when hp == hp_max + expected_diff = 80000 - hp_max + player:set_hp(80000) + assert(player:get_hp() == hp_max) + -- on_player_hpchange_call expected + assert(hpchange_counter == 6) + assert(die_counter == 2) expected_diff = nil player:set_properties({hp_max = old_hp_max}) player:set_hp(old_hp) core.close_formspec(player:get_player_name(), "") -- hide death screen end +local function run_hp_difference_tests(player) + hp_diference_test(player, 22) + hp_diference_test(player, 30) + hp_diference_test(player, 65535) -- U16_MAX +end unittests.register("test_hp_difference", run_hp_difference_tests, {player=true}) -- diff --git a/irr/include/IAnimatedMesh.h b/irr/include/IAnimatedMesh.h index 80b3bc3ca..2a1c1f4b1 100644 --- a/irr/include/IAnimatedMesh.h +++ b/irr/include/IAnimatedMesh.h @@ -19,11 +19,8 @@ irr::scene::SMeshBuffer etc. */ class IAnimatedMesh : public IMesh { public: - //! Gets the frame count of the animated mesh. - /** Note that the play-time is usually getFrameCount()-1 as it stops as soon as the last frame-key is reached. - \return The amount of frames. If the amount is 1, - it is a static, non animated mesh. */ - virtual u32 getFrameCount() const = 0; + //! Gets the maximum frame number, 0 if the mesh is static. + virtual f32 getMaxFrameNumber() const = 0; //! Gets the animation speed of the animated mesh. /** \return The number of frames per second to play the @@ -39,19 +36,10 @@ public: virtual void setAnimationSpeed(f32 fps) = 0; //! Returns the IMesh interface for a frame. - /** \param frame: Frame number as zero based index. The maximum - frame number is getFrameCount() - 1; - \param detailLevel: Level of detail. 0 is the lowest, 255 the - highest level of detail. Most meshes will ignore the detail level. - \param startFrameLoop: Because some animated meshes (.MD2) are - blended between 2 static frames, and maybe animated in a loop, - the startFrameLoop and the endFrameLoop have to be defined, to - prevent the animation to be blended between frames which are - outside of this loop. - If startFrameLoop and endFrameLoop are both -1, they are ignored. - \param endFrameLoop: see startFrameLoop. - \return Returns the animated mesh based on a detail level. */ - virtual IMesh *getMesh(s32 frame, s32 detailLevel = 255, s32 startFrameLoop = -1, s32 endFrameLoop = -1) = 0; + /** \param frame: Frame number, >= 0, <= getMaxFrameNumber() + Linear interpolation is used if this is between two frames. + \return Returns the animated mesh for the given frame */ + virtual IMesh *getMesh(f32 frame) = 0; //! Returns the type of the animated mesh. /** In most cases it is not necessary to use this method. diff --git a/irr/include/IAnimatedMeshSceneNode.h b/irr/include/IAnimatedMeshSceneNode.h index 65fdaaadf..8f9f6d661 100644 --- a/irr/include/IAnimatedMeshSceneNode.h +++ b/irr/include/IAnimatedMeshSceneNode.h @@ -63,7 +63,7 @@ public: virtual void setCurrentFrame(f32 frame) = 0; //! Sets the frame numbers between the animation is looped. - /** The default is 0 to getFrameCount()-1 of the mesh. + /** The default is 0 to getMaxFrameNumber() of the mesh. Number of played frames is end-start. It interpolates toward the last frame but stops when it is reached. It does not interpolate back to start even when looping. @@ -71,7 +71,7 @@ public: \param begin: Start frame number of the loop. \param end: End frame number of the loop. \return True if successful, false if not. */ - virtual bool setFrameLoop(s32 begin, s32 end) = 0; + virtual bool setFrameLoop(f32 begin, f32 end) = 0; //! Sets the speed with which the animation is played. /** \param framesPerSecond: Frames per second played. */ @@ -108,9 +108,9 @@ public: //! Returns the currently displayed frame number. virtual f32 getFrameNr() const = 0; //! Returns the current start frame number. - virtual s32 getStartFrame() const = 0; + virtual f32 getStartFrame() const = 0; //! Returns the current end frame number. - virtual s32 getEndFrame() const = 0; + virtual f32 getEndFrame() const = 0; //! Sets looping mode which is on by default. /** If set to false, animations will not be played looped. */ diff --git a/irr/include/IEventReceiver.h b/irr/include/IEventReceiver.h index a484bfb84..7fb9e5f4e 100644 --- a/irr/include/IEventReceiver.h +++ b/irr/include/IEventReceiver.h @@ -347,6 +347,9 @@ struct SEvent //! Type of mouse event EMOUSE_INPUT_EVENT Event; + + //! Is this a simulated mouse event generated by Minetest itself? + bool Simulated; }; //! Any kind of keyboard event. @@ -538,6 +541,11 @@ struct SEvent struct SUserEvent UserEvent; struct SApplicationEvent ApplicationEvent; }; + + SEvent() { + // would be left uninitialized in many places otherwise + MouseInput.Simulated = false; + } }; //! Interface of an object which can receive events. diff --git a/irr/include/ISkinnedMesh.h b/irr/include/ISkinnedMesh.h index bb611bba2..869327bcd 100644 --- a/irr/include/ISkinnedMesh.h +++ b/irr/include/ISkinnedMesh.h @@ -159,15 +159,17 @@ public: core::array Weights; //! Unnecessary for loaders, will be overwritten on finalize - core::matrix4 GlobalMatrix; + core::matrix4 GlobalMatrix; // loaders may still choose to set this (temporarily) to calculate absolute vertex data. core::matrix4 GlobalAnimatedMatrix; core::matrix4 LocalAnimatedMatrix; + + //! These should be set by loaders. core::vector3df Animatedposition; core::vector3df Animatedscale; core::quaternion Animatedrotation; - core::matrix4 GlobalInversedMatrix; // the x format pre-calculates this - + // The .x and .gltf formats pre-calculate this + std::optional GlobalInversedMatrix; private: //! Internal members used by CSkinnedMesh friend class CSkinnedMesh; diff --git a/irr/src/KHR/khrplatform.h b/irr/include/KHR/khrplatform.h similarity index 100% rename from irr/src/KHR/khrplatform.h rename to irr/include/KHR/khrplatform.h diff --git a/irr/include/SAnimatedMesh.h b/irr/include/SAnimatedMesh.h index 8fe14b41f..dd7306633 100644 --- a/irr/include/SAnimatedMesh.h +++ b/irr/include/SAnimatedMesh.h @@ -36,11 +36,9 @@ struct SAnimatedMesh final : public IAnimatedMesh mesh->drop(); } - //! Gets the frame count of the animated mesh. - /** \return Amount of frames. If the amount is 1, it is a static, non animated mesh. */ - u32 getFrameCount() const override + f32 getMaxFrameNumber() const override { - return static_cast(Meshes.size()); + return static_cast(Meshes.size() - 1); } //! Gets the default animation speed of the animated mesh. @@ -59,19 +57,14 @@ struct SAnimatedMesh final : public IAnimatedMesh } //! Returns the IMesh interface for a frame. - /** \param frame: Frame number as zero based index. The maximum frame number is - getFrameCount() - 1; - \param detailLevel: Level of detail. 0 is the lowest, - 255 the highest level of detail. Most meshes will ignore the detail level. - \param startFrameLoop: start frame - \param endFrameLoop: end frame - \return The animated mesh based on a detail level. */ - IMesh *getMesh(s32 frame, s32 detailLevel = 255, s32 startFrameLoop = -1, s32 endFrameLoop = -1) override + /** \param frame: Frame number as zero based index. + \return The animated mesh based for the given frame */ + IMesh *getMesh(f32 frame) override { if (Meshes.empty()) - return 0; + return nullptr; - return Meshes[frame]; + return Meshes[static_cast(frame)]; } //! adds a Mesh diff --git a/irr/include/SMaterial.h b/irr/include/SMaterial.h index cceccad78..8c0a51dfd 100644 --- a/irr/include/SMaterial.h +++ b/irr/include/SMaterial.h @@ -410,6 +410,7 @@ public: { bool different = MaterialType != b.MaterialType || + ColorParam != b.ColorParam || MaterialTypeParam != b.MaterialTypeParam || Thickness != b.Thickness || Wireframe != b.Wireframe || diff --git a/irr/include/coreutil.h b/irr/include/coreutil.h index 60014c4a7..73d1c4b43 100644 --- a/irr/include/coreutil.h +++ b/irr/include/coreutil.h @@ -63,7 +63,7 @@ inline io::path &getFileNameExtension(io::path &dest, const io::path &source) } //! delete path from filename -inline io::path &deletePathFromFilename(io::path &filename) +inline io::path deletePathFromFilename(const io::path &filename) { // delete path from filename const fschar_t *s = filename.c_str(); @@ -73,11 +73,10 @@ inline io::path &deletePathFromFilename(io::path &filename) while (*p != '/' && *p != '\\' && p != s) p--; - if (p != s) { + if (p != s) ++p; - filename = p; - } - return filename; + + return p; } //! trim paths diff --git a/irr/include/irrString.h b/irr/include/irrString.h index 9d9b288d8..76e0548d3 100644 --- a/irr/include/irrString.h +++ b/irr/include/irrString.h @@ -173,13 +173,24 @@ public: return *this; } - // no longer allowed! - _IRR_DEBUG_BREAK_IF((void *)c == (void *)c_str()); + if constexpr (sizeof(T) != sizeof(B)) { + _IRR_DEBUG_BREAK_IF( + (uintptr_t)c >= (uintptr_t)(str.data()) && + (uintptr_t)c < (uintptr_t)(str.data() + str.size())); + } + + if ((void *)c == (void *)c_str()) + return *this; u32 len = calclen(c); - str.resize(len); + // In case `c` is a pointer to our own buffer, we may not resize first + // or it can become invalid. + if (len > str.size()) + str.resize(len); for (u32 l = 0; l < len; ++l) - str[l] = (T)c[l]; + str[l] = static_cast(c[l]); + if (len < str.size()) + str.resize(len); return *this; } diff --git a/irr/include/matrix4.h b/irr/include/matrix4.h index 374fc6e4a..8fce0157a 100644 --- a/irr/include/matrix4.h +++ b/irr/include/matrix4.h @@ -24,7 +24,12 @@ namespace core { //! 4x4 matrix. Mostly used as transformation matrix for 3d calculations. -/** The matrix is a D3D style matrix, row major with translations in the 4th row. */ +/** Conventions: Matrices are considered to be in row-major order. + * Multiplication of a matrix A with a row vector v is the premultiplication vA. + * Translations are thus in the 4th row. + * The matrix product AB yields a matrix C such that vC = (vB)A: + * B is applied first, then A. + */ template class CMatrix4 { @@ -242,17 +247,11 @@ public: //! Translate a vector by the inverse of the translation part of this matrix. void inverseTranslateVect(vector3df &vect) const; - //! Rotate a vector by the inverse of the rotation part of this matrix. - void inverseRotateVect(vector3df &vect) const; + //! Scale a vector, then rotate by the inverse of the rotation part of this matrix. + [[nodiscard]] vector3d scaleThenInvRotVect(const vector3d &vect) const; - //! Rotate a vector by the rotation part of this matrix. - void rotateVect(vector3df &vect) const; - - //! An alternate transform vector method, writing into a second vector - void rotateVect(core::vector3df &out, const core::vector3df &in) const; - - //! An alternate transform vector method, writing into an array of 3 floats - void rotateVect(T *out, const core::vector3df &in) const; + //! Rotate and scale a vector. Applies both rotation & scale part of the matrix. + [[nodiscard]] vector3d rotateAndScaleVect(const vector3d &vect) const; //! Transforms the vector by this matrix /** This operation is performed as if the vector was 4d with the 4th component =1 */ @@ -1154,39 +1153,23 @@ inline bool CMatrix4::isIdentity_integer_base() const } template -inline void CMatrix4::rotateVect(vector3df &vect) const +inline vector3d CMatrix4::rotateAndScaleVect(const vector3d &v) const { - vector3d tmp(static_cast(vect.X), static_cast(vect.Y), static_cast(vect.Z)); - vect.X = static_cast(tmp.X * M[0] + tmp.Y * M[4] + tmp.Z * M[8]); - vect.Y = static_cast(tmp.X * M[1] + tmp.Y * M[5] + tmp.Z * M[9]); - vect.Z = static_cast(tmp.X * M[2] + tmp.Y * M[6] + tmp.Z * M[10]); -} - -//! An alternate transform vector method, writing into a second vector -template -inline void CMatrix4::rotateVect(core::vector3df &out, const core::vector3df &in) const -{ - out.X = in.X * M[0] + in.Y * M[4] + in.Z * M[8]; - out.Y = in.X * M[1] + in.Y * M[5] + in.Z * M[9]; - out.Z = in.X * M[2] + in.Y * M[6] + in.Z * M[10]; -} - -//! An alternate transform vector method, writing into an array of 3 floats -template -inline void CMatrix4::rotateVect(T *out, const core::vector3df &in) const -{ - out[0] = in.X * M[0] + in.Y * M[4] + in.Z * M[8]; - out[1] = in.X * M[1] + in.Y * M[5] + in.Z * M[9]; - out[2] = in.X * M[2] + in.Y * M[6] + in.Z * M[10]; + return { + v.X * M[0] + v.Y * M[4] + v.Z * M[8], + v.X * M[1] + v.Y * M[5] + v.Z * M[9], + v.X * M[2] + v.Y * M[6] + v.Z * M[10] + }; } template -inline void CMatrix4::inverseRotateVect(vector3df &vect) const +inline vector3d CMatrix4::scaleThenInvRotVect(const vector3d &v) const { - vector3d tmp(static_cast(vect.X), static_cast(vect.Y), static_cast(vect.Z)); - vect.X = static_cast(tmp.X * M[0] + tmp.Y * M[1] + tmp.Z * M[2]); - vect.Y = static_cast(tmp.X * M[4] + tmp.Y * M[5] + tmp.Z * M[6]); - vect.Z = static_cast(tmp.X * M[8] + tmp.Y * M[9] + tmp.Z * M[10]); + return { + v.X * M[0] + v.Y * M[1] + v.Z * M[2], + v.X * M[4] + v.Y * M[5] + v.Z * M[6], + v.X * M[8] + v.Y * M[9] + v.Z * M[10] + }; } template @@ -1247,8 +1230,7 @@ inline void CMatrix4::transformPlane(core::plane3d &plane) const // Transform the normal by the transposed inverse of the matrix CMatrix4 transposedInverse(*this, EM4CONST_INVERSE_TRANSPOSED); - vector3df normal = plane.Normal; - transposedInverse.rotateVect(normal); + vector3df normal = transposedInverse.rotateAndScaleVect(plane.Normal); plane.setPlane(member, normal.normalize()); } diff --git a/irr/include/vector2d.h b/irr/include/vector2d.h index 4c41389f4..182965295 100644 --- a/irr/include/vector2d.h +++ b/irr/include/vector2d.h @@ -8,6 +8,7 @@ #include "dimension2d.h" #include +#include namespace irr { @@ -34,6 +35,15 @@ public: constexpr vector2d(const dimension2d &other) : X(other.Width), Y(other.Height) {} + explicit constexpr vector2d(const std::array &arr) : + X(arr[0]), Y(arr[1]) {} + + template + constexpr static vector2d from(const vector2d &other) + { + return {static_cast(other.X), static_cast(other.Y)}; + } + // operators vector2d operator-() const { return vector2d(-X, -Y); } diff --git a/irr/src/CAnimatedMeshSceneNode.cpp b/irr/src/CAnimatedMeshSceneNode.cpp index 295d408f3..ba8bc3b78 100644 --- a/irr/src/CAnimatedMeshSceneNode.cpp +++ b/irr/src/CAnimatedMeshSceneNode.cpp @@ -16,6 +16,7 @@ #include "IAnimatedMesh.h" #include "IFileSystem.h" #include "quaternion.h" +#include namespace irr { @@ -80,7 +81,7 @@ void CAnimatedMeshSceneNode::buildFrameNr(u32 timeMs) } if (StartFrame == EndFrame) { - CurrentFrameNr = (f32)StartFrame; // Support for non animated meshes + CurrentFrameNr = StartFrame; // Support for non animated meshes } else if (Looping) { // play animation looped CurrentFrameNr += timeMs * FramesPerSecond; @@ -89,26 +90,26 @@ void CAnimatedMeshSceneNode::buildFrameNr(u32 timeMs) // the last frame must be identical to first one with our current solution. if (FramesPerSecond > 0.f) { // forwards... if (CurrentFrameNr > EndFrame) - CurrentFrameNr = StartFrame + fmodf(CurrentFrameNr - StartFrame, (f32)(EndFrame - StartFrame)); + CurrentFrameNr = StartFrame + fmodf(CurrentFrameNr - StartFrame, EndFrame - StartFrame); } else // backwards... { if (CurrentFrameNr < StartFrame) - CurrentFrameNr = EndFrame - fmodf(EndFrame - CurrentFrameNr, (f32)(EndFrame - StartFrame)); + CurrentFrameNr = EndFrame - fmodf(EndFrame - CurrentFrameNr, EndFrame - StartFrame); } } else { // play animation non looped CurrentFrameNr += timeMs * FramesPerSecond; if (FramesPerSecond > 0.f) { // forwards... - if (CurrentFrameNr > (f32)EndFrame) { - CurrentFrameNr = (f32)EndFrame; + if (CurrentFrameNr > EndFrame) { + CurrentFrameNr = EndFrame; if (LoopCallBack) LoopCallBack->OnAnimationEnd(this); } } else // backwards... { - if (CurrentFrameNr < (f32)StartFrame) { - CurrentFrameNr = (f32)StartFrame; + if (CurrentFrameNr < StartFrame) { + CurrentFrameNr = StartFrame; if (LoopCallBack) LoopCallBack->OnAnimationEnd(this); } @@ -159,9 +160,7 @@ void CAnimatedMeshSceneNode::OnRegisterSceneNode() IMesh *CAnimatedMeshSceneNode::getMeshForCurrentFrame() { if (Mesh->getMeshType() != EAMT_SKINNED) { - s32 frameNr = (s32)getFrameNr(); - s32 frameBlend = (s32)(core::fract(getFrameNr()) * 1000.f); - return Mesh->getMesh(frameNr, frameBlend, StartFrame, EndFrame); + return Mesh->getMesh(getFrameNr()); } else { // As multiple scene nodes may be sharing the same skinned mesh, we have to // re-animate it every frame to ensure that this node gets the mesh that it needs. @@ -331,33 +330,33 @@ void CAnimatedMeshSceneNode::render() } //! Returns the current start frame number. -s32 CAnimatedMeshSceneNode::getStartFrame() const +f32 CAnimatedMeshSceneNode::getStartFrame() const { return StartFrame; } //! Returns the current start frame number. -s32 CAnimatedMeshSceneNode::getEndFrame() const +f32 CAnimatedMeshSceneNode::getEndFrame() const { return EndFrame; } //! sets the frames between the animation is looped. //! the default is 0 - MaximalFrameCount of the mesh. -bool CAnimatedMeshSceneNode::setFrameLoop(s32 begin, s32 end) +bool CAnimatedMeshSceneNode::setFrameLoop(f32 begin, f32 end) { - const s32 maxFrameCount = Mesh->getFrameCount() - 1; + const f32 maxFrame = Mesh->getMaxFrameNumber(); if (end < begin) { - StartFrame = core::s32_clamp(end, 0, maxFrameCount); - EndFrame = core::s32_clamp(begin, StartFrame, maxFrameCount); + StartFrame = std::clamp(end, 0, maxFrame); + EndFrame = std::clamp(begin, StartFrame, maxFrame); } else { - StartFrame = core::s32_clamp(begin, 0, maxFrameCount); - EndFrame = core::s32_clamp(end, StartFrame, maxFrameCount); + StartFrame = std::clamp(begin, 0, maxFrame); + EndFrame = std::clamp(end, StartFrame, maxFrame); } if (FramesPerSecond < 0) - setCurrentFrame((f32)EndFrame); + setCurrentFrame(EndFrame); else - setCurrentFrame((f32)StartFrame); + setCurrentFrame(StartFrame); return true; } @@ -532,7 +531,7 @@ void CAnimatedMeshSceneNode::setMesh(IAnimatedMesh *mesh) // get materials and bounding box Box = Mesh->getBoundingBox(); - IMesh *m = Mesh->getMesh(0, 0); + IMesh *m = Mesh->getMesh(0); if (m) { Materials.clear(); Materials.reallocate(m->getMeshBufferCount()); @@ -554,7 +553,7 @@ void CAnimatedMeshSceneNode::setMesh(IAnimatedMesh *mesh) // get start and begin time setAnimationSpeed(Mesh->getAnimationSpeed()); // NOTE: This had been commented out (but not removed!) in r3526. Which caused meshloader-values for speed to be ignored unless users specified explicitly. Missing a test-case where this could go wrong so I put the code back in. - setFrameLoop(0, Mesh->getFrameCount() - 1); + setFrameLoop(0, Mesh->getMaxFrameNumber()); } //! updates the absolute position based on the relative and the parents position diff --git a/irr/src/CAnimatedMeshSceneNode.h b/irr/src/CAnimatedMeshSceneNode.h index 0364ab527..e45edca86 100644 --- a/irr/src/CAnimatedMeshSceneNode.h +++ b/irr/src/CAnimatedMeshSceneNode.h @@ -45,7 +45,7 @@ public: //! sets the frames between the animation is looped. //! the default is 0 - MaximalFrameCount of the mesh. //! NOTE: setMesh will also change this value and set it to the full range of animations of the mesh - bool setFrameLoop(s32 begin, s32 end) override; + bool setFrameLoop(f32 begin, f32 end) override; //! Sets looping mode which is on by default. If set to false, //! animations will not be looped. @@ -93,9 +93,9 @@ public: //! Returns the current displayed frame number. f32 getFrameNr() const override; //! Returns the current start frame number. - s32 getStartFrame() const override; + f32 getStartFrame() const override; //! Returns the current end frame number. - s32 getEndFrame() const override; + f32 getEndFrame() const override; //! Sets if the scene node should not copy the materials of the mesh but use them in a read only style. /* In this way it is possible to change the materials a mesh causing all mesh scene nodes @@ -148,8 +148,8 @@ private: core::aabbox3d Box; IAnimatedMesh *Mesh; - s32 StartFrame; - s32 EndFrame; + f32 StartFrame; + f32 EndFrame; f32 FramesPerSecond; f32 CurrentFrameNr; diff --git a/irr/src/CB3DMeshFileLoader.cpp b/irr/src/CB3DMeshFileLoader.cpp index 4d78860b2..cf6a980d8 100644 --- a/irr/src/CB3DMeshFileLoader.cpp +++ b/irr/src/CB3DMeshFileLoader.cpp @@ -389,7 +389,8 @@ bool CB3DMeshFileLoader::readChunkVRTS(CSkinnedMesh::SJoint *inJoint) // Transform the Vertex position by nested node... inJoint->GlobalMatrix.transformVect(Vertex.Pos); - inJoint->GlobalMatrix.rotateVect(Vertex.Normal); + Vertex.Normal = inJoint->GlobalMatrix.rotateAndScaleVect(Vertex.Normal); + Vertex.Normal.normalize(); // renormalize: normal might have been skewed by scaling // Add it... BaseVertices.push_back(Vertex); diff --git a/irr/src/CFileList.cpp b/irr/src/CFileList.cpp index dde8e75ac..cd6c85df4 100644 --- a/irr/src/CFileList.cpp +++ b/irr/src/CFileList.cpp @@ -80,7 +80,7 @@ u32 CFileList::addItem(const io::path &fullPath, u32 offset, u32 size, bool isDi entry.FullName = entry.Name; - core::deletePathFromFilename(entry.Name); + entry.Name = core::deletePathFromFilename(entry.Name); if (IgnorePaths) entry.FullName = entry.Name; @@ -140,7 +140,7 @@ s32 CFileList::findFile(const io::path &filename, bool isDirectory = false) cons entry.FullName.make_lower(); if (IgnorePaths) - core::deletePathFromFilename(entry.FullName); + entry.FullName = core::deletePathFromFilename(entry.FullName); return Files.binary_search(entry); } diff --git a/irr/src/CGLTFMeshFileLoader.cpp b/irr/src/CGLTFMeshFileLoader.cpp index 64bbc10f1..54d207e5f 100644 --- a/irr/src/CGLTFMeshFileLoader.cpp +++ b/irr/src/CGLTFMeshFileLoader.cpp @@ -3,19 +3,19 @@ #include "CGLTFMeshFileLoader.h" +#include "SMaterialLayer.h" #include "coreutil.h" #include "CSkinnedMesh.h" -#include "ISkinnedMesh.h" -#include "irrTypes.h" +#include "IAnimatedMesh.h" #include "IReadFile.h" +#include "irrTypes.h" #include "matrix4.h" #include "path.h" #include "quaternion.h" +#include "vector2d.h" #include "vector3d.h" #include "os.h" -#include "tiniergltf.hpp" - #include #include #include @@ -23,9 +23,11 @@ #include #include #include +#include #include #include #include +#include namespace irr { @@ -51,6 +53,28 @@ core::vector3df convertHandedness(const core::vector3df &p) return core::vector3df(p.X, p.Y, -p.Z); } +template <> +core::quaternion convertHandedness(const core::quaternion &q) +{ + return core::quaternion(q.X, q.Y, -q.Z, q.W); +} + +template <> +core::matrix4 convertHandedness(const core::matrix4 &mat) +{ + // Base transformation between left & right handed coordinate systems. + static const core::matrix4 invertZ = core::matrix4( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, -1, 0, + 0, 0, 0, 1); + // Convert from left-handed to right-handed, + // then apply mat, + // then convert from right-handed to left-handed. + // Both conversions just invert Z. + return invertZ * mat * invertZ; +} + namespace scene { using SelfType = CGLTFMeshFileLoader; @@ -196,6 +220,8 @@ ACCESSOR_PRIMITIVE(u16, UNSIGNED_SHORT) ACCESSOR_PRIMITIVE(u32, UNSIGNED_INT) ACCESSOR_TYPES(core::vector3df, VEC3, FLOAT) +ACCESSOR_TYPES(core::quaternion, VEC4, FLOAT) +ACCESSOR_TYPES(core::matrix4, MAT4, FLOAT) template T SelfType::Accessor::get(std::size_t i) const @@ -303,13 +329,11 @@ std::array SelfType::getNormalizedValues( return values; } -/** - * The most basic portion of the code base. This tells irllicht if this file has a .gltf extension. -*/ bool SelfType::isALoadableFileExtension( const io::path& filename) const { - return core::hasFileExtension(filename, "gltf"); + return core::hasFileExtension(filename, "gltf") || + core::hasFileExtension(filename, "glb"); } /** @@ -324,6 +348,11 @@ IAnimatedMesh* SelfType::createMesh(io::IReadFile* file) if (!model.has_value()) { return nullptr; } + if (model->extensionsRequired) { + os::Printer::log("glTF loader", + "model requires extensions, but we support none", ELL_ERROR); + return nullptr; + } if (!(model->buffers.has_value() && model->bufferViews.has_value() @@ -337,7 +366,7 @@ IAnimatedMesh* SelfType::createMesh(io::IReadFile* file) auto *mesh = new CSkinnedMesh(); MeshExtractor parser(std::move(model.value()), mesh); try { - parser.loadNodes(); + parser.load(); } catch (std::runtime_error &e) { os::Printer::log("glTF loader", e.what(), ELL_ERROR); mesh->drop(); @@ -354,8 +383,7 @@ static void transformVertices(std::vector &vertices, const cor // Apply scaling, rotation and rotation (in that order) to the position. transform.transformVect(vertex.Pos); // For the normal, we do not want to apply the translation. - // TODO note that this also applies scaling; the Irrlicht method is misnamed. - transform.rotateVect(vertex.Normal); + vertex.Normal = transform.rotateAndScaleVect(vertex.Normal); // Renormalize (length might have been affected by scaling). vertex.Normal.normalize(); } @@ -381,52 +409,148 @@ static std::vector generateIndices(const std::size_t nVerts) return indices; } -/** - * Load up the rawest form of the model. The vertex positions and indices. - * Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes - * If material is undefined, then a default material MUST be used. -*/ -void SelfType::MeshExtractor::loadMesh( - const std::size_t meshIdx, - ISkinnedMesh::SJoint *parent) const +using Wrap = tiniergltf::Sampler::Wrap; +static video::E_TEXTURE_CLAMP convertTextureWrap(const Wrap wrap) { + switch (wrap) { + case Wrap::REPEAT: + return video::ETC_REPEAT; + case Wrap::CLAMP_TO_EDGE: + return video::ETC_CLAMP_TO_EDGE; + case Wrap::MIRRORED_REPEAT: + return video::ETC_MIRROR; + default: + throw std::runtime_error("invalid sampler wrapping mode"); + } +} + +void SelfType::MeshExtractor::addPrimitive( + const tiniergltf::MeshPrimitive &primitive, + const std::optional skinIdx, + CSkinnedMesh::SJoint *parent) { - for (std::size_t pi = 0; pi < getPrimitiveCount(meshIdx); ++pi) { - const auto &primitive = m_gltf_model.meshes->at(meshIdx).primitives.at(pi); - auto vertices = getVertices(primitive); - if (!vertices.has_value()) - continue; // "When positions are not specified, client implementations SHOULD skip primitive’s rendering" + auto vertices = getVertices(primitive); + if (!vertices.has_value()) + return; // "When positions are not specified, client implementations SHOULD skip primitive’s rendering" - // Excludes the max value for consistency. - if (vertices->size() >= std::numeric_limits::max()) - throw std::runtime_error("too many vertices"); + const auto n_vertices = vertices->size(); - // Apply the global transform along the parent chain. - transformVertices(*vertices, parent->GlobalMatrix); + // Excludes the max value for consistency. + if (n_vertices >= std::numeric_limits::max()) + throw std::runtime_error("too many vertices"); - auto maybeIndices = getIndices(primitive); - std::vector indices; - if (maybeIndices.has_value()) { - indices = std::move(*maybeIndices); - checkIndices(indices, vertices->size()); - } else { - // Non-indexed geometry - indices = generateIndices(vertices->size()); - } + // Apply the global transform along the parent chain. + transformVertices(*vertices, parent->GlobalMatrix); - m_irr_model->addMeshBuffer( - new SSkinMeshBuffer(std::move(*vertices), std::move(indices))); + auto maybeIndices = getIndices(primitive); + std::vector indices; + if (maybeIndices.has_value()) { + indices = std::move(*maybeIndices); + checkIndices(indices, vertices->size()); + } else { + // Non-indexed geometry + indices = generateIndices(vertices->size()); + } - if (primitive.material.has_value()) { - const auto &material = m_gltf_model.materials->at(*primitive.material); - if (material.pbrMetallicRoughness.has_value()) { - const auto &texture = material.pbrMetallicRoughness->baseColorTexture; - if (texture.has_value()) { - const auto meshbufNr = m_irr_model->getMeshBufferCount() - 1; - m_irr_model->setTextureSlot(meshbufNr, static_cast(texture->index)); + m_irr_model->addMeshBuffer( + new SSkinMeshBuffer(std::move(*vertices), std::move(indices))); + const auto meshbufNr = m_irr_model->getMeshBufferCount() - 1; + auto *meshbuf = m_irr_model->getMeshBuffer(meshbufNr); + + if (primitive.material.has_value()) { + const auto &material = m_gltf_model.materials->at(*primitive.material); + if (material.pbrMetallicRoughness.has_value()) { + const auto &texture = material.pbrMetallicRoughness->baseColorTexture; + if (texture.has_value()) { + m_irr_model->setTextureSlot(meshbufNr, static_cast(texture->index)); + const auto samplerIdx = m_gltf_model.textures->at(texture->index).sampler; + if (samplerIdx.has_value()) { + auto &sampler = m_gltf_model.samplers->at(*samplerIdx); + auto &layer = meshbuf->getMaterial().TextureLayers[0]; + layer.TextureWrapU = convertTextureWrap(sampler.wrapS); + layer.TextureWrapV = convertTextureWrap(sampler.wrapT); } } } } + + if (!skinIdx.has_value()) { + // No skin => all vertices belong entirely to their parent + for (std::size_t v = 0; v < n_vertices; ++v) { + auto *weight = m_irr_model->addWeight(parent); + weight->buffer_id = meshbufNr; + weight->vertex_id = v; + weight->strength = 1.0f; + } + return; + } + + const auto &skin = m_gltf_model.skins->at(*skinIdx); + + const auto &attrs = primitive.attributes; + const auto &joints = attrs.joints; + if (!joints.has_value()) + return; + + const auto &weights = attrs.weights; + for (std::size_t set = 0; set < joints->size(); ++set) { + const auto jointAccessor = ([&]() -> ArrayAccessorVariant<4, u8, u16> { + const auto idx = joints->at(set); + const auto &acc = m_gltf_model.accessors->at(idx); + + switch (acc.componentType) { + case tiniergltf::Accessor::ComponentType::UNSIGNED_BYTE: + return Accessor>::make(m_gltf_model, idx); + case tiniergltf::Accessor::ComponentType::UNSIGNED_SHORT: + return Accessor>::make(m_gltf_model, idx); + default: + throw std::runtime_error("invalid component type"); + } + })(); + + const auto weightAccessor = createNormalizedValuesAccessor<4>(m_gltf_model, weights->at(set)); + + for (std::size_t v = 0; v < n_vertices; ++v) { + std::array jointIdxs; + if (std::holds_alternative>>(jointAccessor)) { + const auto jointIdxsU8 = std::get>>(jointAccessor).get(v); + jointIdxs = {jointIdxsU8[0], jointIdxsU8[1], jointIdxsU8[2], jointIdxsU8[3]}; + } else if (std::holds_alternative>>(jointAccessor)) { + jointIdxs = std::get>>(jointAccessor).get(v); + } + std::array strengths = getNormalizedValues(weightAccessor, v); + + // 4 joints per set + for (std::size_t in_set = 0; in_set < 4; ++in_set) { + u16 jointIdx = jointIdxs[in_set]; + f32 strength = strengths[in_set]; + if (strength == 0) + continue; + + CSkinnedMesh::SWeight *weight = m_irr_model->addWeight(m_loaded_nodes.at(skin.joints.at(jointIdx))); + weight->buffer_id = meshbufNr; + weight->vertex_id = v; + weight->strength = strength; + } + } + } +} + +/** + * Load up the rawest form of the model. The vertex positions and indices. + * Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes + * If material is undefined, then a default material MUST be used. + */ +void SelfType::MeshExtractor::deferAddMesh( + const std::size_t meshIdx, + const std::optional skinIdx, + CSkinnedMesh::SJoint *parent) +{ + m_mesh_loaders.emplace_back([=] { + for (std::size_t pi = 0; pi < getPrimitiveCount(meshIdx); ++pi) { + const auto &primitive = m_gltf_model.meshes->at(meshIdx).primitives.at(pi); + addPrimitive(primitive, skinIdx, parent); + } + }); } // Base transformation between left & right handed coordinate systems. @@ -439,51 +563,75 @@ static const core::matrix4 leftToRight = core::matrix4( ); static const core::matrix4 rightToLeft = leftToRight; -static core::matrix4 loadTransform(const tiniergltf::Node::Matrix &m) +static core::matrix4 loadTransform(const tiniergltf::Node::Matrix &m, CSkinnedMesh::SJoint *joint) { // Note: Under the hood, this casts these doubles to floats. - return core::matrix4( + core::matrix4 mat = convertHandedness(core::matrix4( m[0], m[1], m[2], m[3], m[4], m[5], m[6], m[7], m[8], m[9], m[10], m[11], - m[12], m[13], m[14], m[15]); + m[12], m[13], m[14], m[15])); + + // Decompose the matrix into translation, scale, and rotation. + joint->Animatedposition = mat.getTranslation(); + + auto scale = mat.getScale(); + joint->Animatedscale = scale; + core::matrix4 inverseScale; + inverseScale.setScale(core::vector3df( + scale.X == 0 ? 0 : 1 / scale.X, + scale.Y == 0 ? 0 : 1 / scale.Y, + scale.Z == 0 ? 0 : 1 / scale.Z)); + + core::matrix4 axisNormalizedMat = inverseScale * mat; + joint->Animatedrotation = axisNormalizedMat.getRotationDegrees(); + // Invert the rotation because it is applied using `getMatrix_transposed`, + // which again inverts. + joint->Animatedrotation.makeInverse(); + + return mat; } -static core::matrix4 loadTransform(const tiniergltf::Node::TRS &trs) +static core::matrix4 loadTransform(const tiniergltf::Node::TRS &trs, CSkinnedMesh::SJoint *joint) { const auto &trans = trs.translation; const auto &rot = trs.rotation; const auto &scale = trs.scale; core::matrix4 transMat; - transMat.setTranslation(core::vector3df(trans[0], trans[1], trans[2])); - core::matrix4 rotMat = core::quaternion(rot[0], rot[1], rot[2], rot[3]).getMatrix(); + joint->Animatedposition = convertHandedness(core::vector3df(trans[0], trans[1], trans[2])); + transMat.setTranslation(joint->Animatedposition); + core::matrix4 rotMat; + joint->Animatedrotation = convertHandedness(core::quaternion(rot[0], rot[1], rot[2], rot[3])); + core::quaternion(joint->Animatedrotation).getMatrix_transposed(rotMat); + joint->Animatedscale = core::vector3df(scale[0], scale[1], scale[2]); core::matrix4 scaleMat; - scaleMat.setScale(core::vector3df(scale[0], scale[1], scale[2])); + scaleMat.setScale(joint->Animatedscale); return transMat * rotMat * scaleMat; } -static core::matrix4 loadTransform(std::optional> transform) { +static core::matrix4 loadTransform(std::optional> transform, + CSkinnedMesh::SJoint *joint) { if (!transform.has_value()) { return core::matrix4(); } - core::matrix4 mat = std::visit([](const auto &t) { return loadTransform(t); }, *transform); - return rightToLeft * mat * leftToRight; + return std::visit([joint](const auto &t) { return loadTransform(t, joint); }, *transform); } void SelfType::MeshExtractor::loadNode( const std::size_t nodeIdx, - ISkinnedMesh::SJoint *parent) const + CSkinnedMesh::SJoint *parent) { const auto &node = m_gltf_model.nodes->at(nodeIdx); auto *joint = m_irr_model->addJoint(parent); - const core::matrix4 transform = loadTransform(node.transform); + const core::matrix4 transform = loadTransform(node.transform, joint); joint->LocalMatrix = transform; joint->GlobalMatrix = parent ? parent->GlobalMatrix * joint->LocalMatrix : joint->LocalMatrix; if (node.name.has_value()) { joint->Name = node.name->c_str(); } + m_loaded_nodes[nodeIdx] = joint; if (node.mesh.has_value()) { - loadMesh(*node.mesh, joint); + deferAddMesh(*node.mesh, node.skin, joint); } if (node.children.has_value()) { for (const auto &child : *node.children) { @@ -492,8 +640,10 @@ void SelfType::MeshExtractor::loadNode( } } -void SelfType::MeshExtractor::loadNodes() const +void SelfType::MeshExtractor::loadNodes() { + m_loaded_nodes = std::vector(m_gltf_model.nodes->size()); + std::vector isChild(m_gltf_model.nodes->size()); for (const auto &node : *m_gltf_model.nodes) { if (!node.children.has_value()) @@ -511,6 +661,92 @@ void SelfType::MeshExtractor::loadNodes() const } } +void SelfType::MeshExtractor::loadSkins() +{ + if (!m_gltf_model.skins.has_value()) + return; + + for (const auto &skin : *m_gltf_model.skins) { + if (!skin.inverseBindMatrices.has_value()) + continue; + const auto accessor = Accessor::make(m_gltf_model, *skin.inverseBindMatrices); + if (accessor.getCount() < skin.joints.size()) + throw std::runtime_error("accessor contains too few matrices"); + for (std::size_t i = 0; i < skin.joints.size(); ++i) { + m_loaded_nodes.at(skin.joints[i])->GlobalInversedMatrix = convertHandedness(accessor.get(i)); + } + } +} + +void SelfType::MeshExtractor::loadAnimation(const std::size_t animIdx) +{ + const auto &anim = m_gltf_model.animations->at(animIdx); + for (const auto &channel : anim.channels) { + + const auto &sampler = anim.samplers.at(channel.sampler); + if (sampler.interpolation != tiniergltf::AnimationSampler::Interpolation::LINEAR) + throw std::runtime_error("unsupported interpolation"); + + const auto inputAccessor = Accessor::make(m_gltf_model, sampler.input); + const auto n_frames = inputAccessor.getCount(); + + if (!channel.target.node.has_value()) + throw std::runtime_error("no animated node"); + + const auto &joint = m_loaded_nodes.at(*channel.target.node); + switch (channel.target.path) { + case tiniergltf::AnimationChannelTarget::Path::TRANSLATION: { + const auto outputAccessor = Accessor::make(m_gltf_model, sampler.output); + for (std::size_t i = 0; i < n_frames; ++i) { + auto *key = m_irr_model->addPositionKey(joint); + key->frame = inputAccessor.get(i); + key->position = convertHandedness(outputAccessor.get(i)); + } + break; + } + case tiniergltf::AnimationChannelTarget::Path::ROTATION: { + const auto outputAccessor = Accessor::make(m_gltf_model, sampler.output); + for (std::size_t i = 0; i < n_frames; ++i) { + auto *key = m_irr_model->addRotationKey(joint); + key->frame = inputAccessor.get(i); + key->rotation = convertHandedness(outputAccessor.get(i)); + } + break; + } + case tiniergltf::AnimationChannelTarget::Path::SCALE: { + const auto outputAccessor = Accessor::make(m_gltf_model, sampler.output); + for (std::size_t i = 0; i < n_frames; ++i) { + auto *key = m_irr_model->addScaleKey(joint); + key->frame = inputAccessor.get(i); + key->scale = outputAccessor.get(i); + } + break; + } + case tiniergltf::AnimationChannelTarget::Path::WEIGHTS: + throw std::runtime_error("no support for morph animations"); + } + } +} + +void SelfType::MeshExtractor::load() +{ + loadNodes(); + for (const auto &load_mesh : m_mesh_loaders) { + load_mesh(); + } + loadSkins(); + // Load the first animation, if there is one. + if (m_gltf_model.animations.has_value()) { + if (m_gltf_model.animations->size() > 1) { + os::Printer::log("glTF loader", + "multiple animations are not supported", ELL_WARNING); + } + loadAnimation(0); + m_irr_model->setAnimationSpeed(1); + } + m_irr_model->finalize(); +} + /** * Extracts GLTF mesh indices. */ @@ -650,11 +886,19 @@ void SelfType::MeshExtractor::copyTCoords( const std::size_t accessorIdx, std::vector& vertices) const { - const auto accessor = createNormalizedValuesAccessor<2>(m_gltf_model, accessorIdx); - const auto count = std::visit([](auto &&a) { return a.getCount(); }, accessor); - for (std::size_t i = 0; i < count; ++i) { - const auto vals = getNormalizedValues(accessor, i); - vertices[i].TCoords = core::vector2df(vals[0], vals[1]); + const auto componentType = m_gltf_model.accessors->at(accessorIdx).componentType; + if (componentType == tiniergltf::Accessor::ComponentType::FLOAT) { + // If floats are used, they need not be normalized: Wrapping may take effect. + const auto accessor = Accessor>::make(m_gltf_model, accessorIdx); + for (std::size_t i = 0; i < accessor.getCount(); ++i) { + vertices[i].TCoords = core::vector2d(accessor.get(i)); + } + } else { + const auto accessor = createNormalizedValuesAccessor<2>(m_gltf_model, accessorIdx); + const auto count = std::visit([](auto &&a) { return a.getCount(); }, accessor); + for (std::size_t i = 0; i < count; ++i) { + vertices[i].TCoords = core::vector2d(getNormalizedValues(accessor, i)); + } } } @@ -663,6 +907,7 @@ void SelfType::MeshExtractor::copyTCoords( */ std::optional SelfType::tryParseGLTF(io::IReadFile* file) { + const bool isGlb = core::hasFileExtension(file->getFileName(), "glb"); auto size = file->getSize(); if (size < 0) // this can happen if `ftell` fails return std::nullopt; @@ -671,15 +916,11 @@ std::optional SelfType::tryParseGLTF(io::IReadFile* file) return std::nullopt; // We probably don't need this, but add it just to be sure. buf[size] = '\0'; - Json::CharReaderBuilder builder; - const std::unique_ptr reader(builder.newCharReader()); - Json::Value json; - JSONCPP_STRING err; - if (!reader->parse(buf.get(), buf.get() + size, &json, &err)) { - return std::nullopt; - } try { - return tiniergltf::GlTF(json); + if (isGlb) + return tiniergltf::readGlb(buf.get(), size); + else + return tiniergltf::readGlTF(buf.get(), size); } catch (const std::runtime_error &e) { os::Printer::log("glTF loader", e.what(), ELL_ERROR); return std::nullopt; @@ -692,4 +933,3 @@ std::optional SelfType::tryParseGLTF(io::IReadFile* file) } // namespace scene } // namespace irr - diff --git a/irr/src/CGLTFMeshFileLoader.h b/irr/src/CGLTFMeshFileLoader.h index 39c3ea6dd..7674fd46a 100644 --- a/irr/src/CGLTFMeshFileLoader.h +++ b/irr/src/CGLTFMeshFileLoader.h @@ -10,9 +10,11 @@ #include "path.h" #include "S3DVertex.h" -#include +#include "tiniergltf.hpp" +#include #include +#include #include namespace irr @@ -26,9 +28,9 @@ class CGLTFMeshFileLoader : public IMeshLoader public: CGLTFMeshFileLoader() noexcept {}; - bool isALoadableFileExtension(const io::path& filename) const override; + bool isALoadableFileExtension(const io::path &filename) const override; - IAnimatedMesh* createMesh(io::IReadFile* file) override; + IAnimatedMesh *createMesh(io::IReadFile *file) override; private: template @@ -94,11 +96,12 @@ private: const NormalizedValuesAccessor &accessor, const std::size_t i); - class MeshExtractor { + class MeshExtractor + { public: MeshExtractor(tiniergltf::GlTF &&model, CSkinnedMesh *mesh) noexcept - : m_gltf_model(model), m_irr_model(mesh) {}; + : m_gltf_model(std::move(model)), m_irr_model(mesh) {}; /* Gets indices for the given mesh/primitive. * @@ -114,12 +117,15 @@ private: std::size_t getPrimitiveCount(const std::size_t meshIdx) const; - void loadNodes() const; + void load(); private: const tiniergltf::GlTF m_gltf_model; CSkinnedMesh *m_irr_model; + std::vector> m_mesh_loaders; + std::vector m_loaded_nodes; + void copyPositions(const std::size_t accessorIdx, std::vector& vertices) const; @@ -129,16 +135,24 @@ private: void copyTCoords(const std::size_t accessorIdx, std::vector& vertices) const; - void loadMesh( - std::size_t meshIdx, - ISkinnedMesh::SJoint *parentJoint) const; + void addPrimitive(const tiniergltf::MeshPrimitive &primitive, + const std::optional skinIdx, + CSkinnedMesh::SJoint *parent); - void loadNode( - const std::size_t nodeIdx, - ISkinnedMesh::SJoint *parentJoint) const; + void deferAddMesh(const std::size_t meshIdx, + const std::optional skinIdx, + CSkinnedMesh::SJoint *parentJoint); + + void loadNode(const std::size_t nodeIdx, CSkinnedMesh::SJoint *parentJoint); + + void loadNodes(); + + void loadSkins(); + + void loadAnimation(const std::size_t animIdx); }; - std::optional tryParseGLTF(io::IReadFile* file); + std::optional tryParseGLTF(io::IReadFile *file); }; } // namespace scene diff --git a/irr/src/CIrrDeviceSDL.cpp b/irr/src/CIrrDeviceSDL.cpp index 14d996e47..6d1b45886 100644 --- a/irr/src/CIrrDeviceSDL.cpp +++ b/irr/src/CIrrDeviceSDL.cpp @@ -721,12 +721,19 @@ bool CIrrDeviceSDL::run() irrevent.EventType = irr::EET_MOUSE_INPUT_EVENT; irrevent.MouseInput.Event = irr::EMIE_MOUSE_MOVED; - MouseX = irrevent.MouseInput.X = - static_cast(SDL_event.motion.x * ScaleX); - MouseY = irrevent.MouseInput.Y = - static_cast(SDL_event.motion.y * ScaleY); + MouseXRel = static_cast(SDL_event.motion.xrel * ScaleX); MouseYRel = static_cast(SDL_event.motion.yrel * ScaleY); + if (!SDL_GetRelativeMouseMode()) { + MouseX = static_cast(SDL_event.motion.x * ScaleX); + MouseY = static_cast(SDL_event.motion.y * ScaleY); + } else { + MouseX += MouseXRel; + MouseY += MouseYRel; + } + irrevent.MouseInput.X = MouseX; + irrevent.MouseInput.Y = MouseY; + irrevent.MouseInput.ButtonStates = MouseButtonStates; irrevent.MouseInput.Shift = (keymod & KMOD_SHIFT) != 0; irrevent.MouseInput.Control = (keymod & KMOD_CTRL) != 0; diff --git a/irr/src/CIrrDeviceSDL.h b/irr/src/CIrrDeviceSDL.h index f881bba5c..7156c19b6 100644 --- a/irr/src/CIrrDeviceSDL.h +++ b/irr/src/CIrrDeviceSDL.h @@ -158,9 +158,13 @@ public: //! Sets the new position of the cursor. void setPosition(s32 x, s32 y) override { +#ifndef __ANDROID__ + // On Android, this somehow results in a camera jump when enabling + // relative mouse mode and it isn't supported anyway. SDL_WarpMouseInWindow(Device->Window, static_cast(x / Device->ScaleX), static_cast(y / Device->ScaleY)); +#endif if (SDL_GetRelativeMouseMode()) { // There won't be an event for this warp (details on libsdl-org/SDL/issues/6034) @@ -298,6 +302,7 @@ private: #endif s32 MouseX, MouseY; + // these two only continue to exist for some Emscripten stuff idk about s32 MouseXRel, MouseYRel; u32 MouseButtonStates; diff --git a/irr/src/CMakeLists.txt b/irr/src/CMakeLists.txt index 22a0d0093..6e38220be 100644 --- a/irr/src/CMakeLists.txt +++ b/irr/src/CMakeLists.txt @@ -480,8 +480,8 @@ add_library(IrrlichtMt::IrrlichtMt ALIAS IrrlichtMt) target_include_directories(IrrlichtMt PUBLIC "$" - "$" PRIVATE + "$" ${link_includes} ) diff --git a/irr/src/CMeshManipulator.cpp b/irr/src/CMeshManipulator.cpp index 2c9d05336..67b22a07e 100644 --- a/irr/src/CMeshManipulator.cpp +++ b/irr/src/CMeshManipulator.cpp @@ -193,7 +193,7 @@ s32 CMeshManipulator::getPolyCount(scene::IMesh *mesh) const //! Returns amount of polygons in mesh. s32 CMeshManipulator::getPolyCount(scene::IAnimatedMesh *mesh) const { - if (mesh && mesh->getFrameCount() != 0) + if (mesh && mesh->getMaxFrameNumber() != 0) return getPolyCount(mesh->getMesh(0)); return 0; diff --git a/irr/src/CSkinnedMesh.cpp b/irr/src/CSkinnedMesh.cpp index 5db027abc..875fd8e7e 100644 --- a/irr/src/CSkinnedMesh.cpp +++ b/irr/src/CSkinnedMesh.cpp @@ -111,11 +111,9 @@ CSkinnedMesh::~CSkinnedMesh() } } -//! returns the amount of frames in milliseconds. -//! If the amount is 1, it is a static (=non animated) mesh. -u32 CSkinnedMesh::getFrameCount() const +f32 CSkinnedMesh::getMaxFrameNumber() const { - return core::floor32(EndFrame + 1.f); + return EndFrame; } //! Gets the default animation speed of the animated mesh. @@ -133,14 +131,14 @@ void CSkinnedMesh::setAnimationSpeed(f32 fps) FramesPerSecond = fps; } -//! returns the animated mesh based on a detail level. 0 is the lowest, 255 the highest detail. Note, that some Meshes will ignore the detail level. -IMesh *CSkinnedMesh::getMesh(s32 frame, s32 detailLevel, s32 startFrameLoop, s32 endFrameLoop) +//! returns the animated mesh based +IMesh *CSkinnedMesh::getMesh(f32 frame) { // animate(frame,startFrameLoop, endFrameLoop); if (frame == -1) return this; - animateMesh((f32)frame, 1.0f); + animateMesh(frame, 1.0f); skinMesh(); return this; } @@ -222,6 +220,7 @@ void CSkinnedMesh::buildAllLocalAnimatedMatrices() // IRR_TEST_BROKEN_QUATERNION_USE: TODO - switched to getMatrix_transposed instead of getMatrix for downward compatibility. // Not tested so far if this was correct or wrong before quaternion fix! + // Note that using getMatrix_transposed inverts the rotation. joint->Animatedrotation.getMatrix_transposed(joint->LocalAnimatedMatrix); // --- joint->LocalAnimatedMatrix *= joint->Animatedrotation.getMatrix() --- @@ -496,8 +495,8 @@ void CSkinnedMesh::skinJoint(SJoint *joint, SJoint *parentJoint) { if (joint->Weights.size()) { // Find this joints pull on vertices... - core::matrix4 jointVertexPull(core::matrix4::EM4CONST_NOTHING); - jointVertexPull.setbyproduct(joint->GlobalAnimatedMatrix, joint->GlobalInversedMatrix); + // Note: It is assumed that the global inversed matrix has been calculated at this point. + core::matrix4 jointVertexPull = joint->GlobalAnimatedMatrix * joint->GlobalInversedMatrix.value(); core::vector3df thisVertexMove, thisNormalMove; @@ -510,8 +509,10 @@ void CSkinnedMesh::skinJoint(SJoint *joint, SJoint *parentJoint) // Pull this vertex... jointVertexPull.transformVect(thisVertexMove, weight.StaticPos); - if (AnimateNormals) - jointVertexPull.rotateVect(thisNormalMove, weight.StaticNormal); + if (AnimateNormals) { + thisNormalMove = jointVertexPull.rotateAndScaleVect(weight.StaticNormal); + thisNormalMove.normalize(); // must renormalize after potentially scaling + } if (!(*(weight.Moved))) { *(weight.Moved) = true; @@ -764,9 +765,9 @@ void CSkinnedMesh::calculateGlobalMatrices(SJoint *joint, SJoint *parentJoint) joint->LocalAnimatedMatrix = joint->LocalMatrix; joint->GlobalAnimatedMatrix = joint->GlobalMatrix; - if (joint->GlobalInversedMatrix.isIdentity()) { // might be pre calculated + if (!joint->GlobalInversedMatrix.has_value()) { // might be pre calculated joint->GlobalInversedMatrix = joint->GlobalMatrix; - joint->GlobalInversedMatrix.makeInverse(); // slow + joint->GlobalInversedMatrix->makeInverse(); // slow } for (u32 j = 0; j < joint->Children.size(); ++j) diff --git a/irr/src/CSkinnedMesh.h b/irr/src/CSkinnedMesh.h index 4b4c5e3b7..1be6ee7bc 100644 --- a/irr/src/CSkinnedMesh.h +++ b/irr/src/CSkinnedMesh.h @@ -27,8 +27,8 @@ public: //! destructor virtual ~CSkinnedMesh(); - //! returns the amount of frames. If the amount is 1, it is a static (=non animated) mesh. - u32 getFrameCount() const override; + //! If the duration is 0, it is a static (=non animated) mesh. + f32 getMaxFrameNumber() const override; //! Gets the default animation speed of the animated mesh. /** \return Amount of frames per second. If the amount is 0, it is a static, non animated mesh. */ @@ -39,8 +39,8 @@ public: The actual speed is set in the scene node the mesh is instantiated in.*/ void setAnimationSpeed(f32 fps) override; - //! returns the animated mesh based on a detail level (which is ignored) - IMesh *getMesh(s32 frame, s32 detailLevel = 255, s32 startFrameLoop = -1, s32 endFrameLoop = -1) override; + //! returns the animated mesh for the given frame + IMesh *getMesh(f32) override; //! Animates this mesh's joints based on frame input //! blend: {0-old position, 1-New position} diff --git a/irr/src/CXMeshFileLoader.cpp b/irr/src/CXMeshFileLoader.cpp index fc0e6e237..967fc367c 100644 --- a/irr/src/CXMeshFileLoader.cpp +++ b/irr/src/CXMeshFileLoader.cpp @@ -990,9 +990,9 @@ bool CXMeshFileLoader::parseDataObjectSkinWeights(SXMesh &mesh) // transforms the mesh vertices to the space of the bone // When concatenated to the bone's transform, this provides the // world space coordinates of the mesh as affected by the bone - core::matrix4 &MatrixOffset = joint->GlobalInversedMatrix; - + core::matrix4 MatrixOffset; readMatrix(MatrixOffset); + joint->GlobalInversedMatrix = MatrixOffset; if (!checkForOneFollowingSemicolons()) { os::Printer::log("No finishing semicolon in Skin Weights found in x file", ELL_WARNING); diff --git a/irr/src/CZipReader.cpp b/irr/src/CZipReader.cpp index 2d2152719..036f6302a 100644 --- a/irr/src/CZipReader.cpp +++ b/irr/src/CZipReader.cpp @@ -191,8 +191,7 @@ bool CZipReader::scanGZipHeader() } } else { // no file name? - ZipFileName = Path; - core::deletePathFromFilename(ZipFileName); + ZipFileName = core::deletePathFromFilename(Path); // rename tgz to tar or remove gz extension if (core::hasFileExtension(ZipFileName, "tgz")) { diff --git a/lib/tiniergltf/tiniergltf.hpp b/lib/tiniergltf/tiniergltf.hpp index 6a861556e..35440f5dd 100644 --- a/lib/tiniergltf/tiniergltf.hpp +++ b/lib/tiniergltf/tiniergltf.hpp @@ -1,6 +1,9 @@ #pragma once #include +#include "util/base64.h" + +#include #include #include #include @@ -13,7 +16,6 @@ #include #include #include -#include "util/base64.h" namespace tiniergltf { @@ -460,7 +462,8 @@ struct Buffer { std::optional name; std::string data; Buffer(const Json::Value &o, - const std::function &resolveURI) + const std::function &resolveURI, + std::optional &&glbData = std::nullopt) : byteLength(as(o["byteLength"])) { check(o.isObject()); @@ -468,24 +471,32 @@ struct Buffer { if (o.isMember("name")) { name = as(o["name"]); } - check(o.isMember("uri")); - bool dataURI = false; - const std::string uri = as(o["uri"]); - for (auto &prefix : std::array { - "data:application/octet-stream;base64,", - "data:application/gltf-buffer;base64," - }) { - if (std::string_view(uri).substr(0, prefix.length()) == prefix) { - auto view = std::string_view(uri).substr(prefix.length()); - check(base64_is_valid(view)); - data = base64_decode(view); - dataURI = true; - break; + if (glbData.has_value()) { + check(!o.isMember("uri")); + data = *std::move(glbData); + // GLB allows padding, which need not be reflected in the JSON + check(byteLength + 3 >= data.size()); + check(data.size() >= byteLength); + } else { + check(o.isMember("uri")); + bool dataURI = false; + const std::string uri = as(o["uri"]); + for (auto &prefix : std::array { + "data:application/octet-stream;base64,", + "data:application/gltf-buffer;base64," + }) { + if (std::string_view(uri).substr(0, prefix.length()) == prefix) { + auto view = std::string_view(uri).substr(prefix.length()); + check(base64_is_valid(view)); + data = base64_decode(view); + dataURI = true; + break; + } } + if (!dataURI) + data = resolveURI(uri); + check(data.size() >= byteLength); } - if (!dataURI) - data = resolveURI(uri); - check(data.size() >= byteLength); data.resize(byteLength); } }; @@ -969,21 +980,16 @@ struct Sampler { }; std::optional minFilter; std::optional name; - enum class WrapS { + enum class Wrap { REPEAT, CLAMP_TO_EDGE, MIRRORED_REPEAT, }; - WrapS wrapS; - enum class WrapT { - REPEAT, - CLAMP_TO_EDGE, - MIRRORED_REPEAT, - }; - WrapT wrapT; + Wrap wrapS; + Wrap wrapT; Sampler(const Json::Value &o) - : wrapS(WrapS::REPEAT) - , wrapT(WrapT::REPEAT) + : wrapS(Wrap::REPEAT) + , wrapT(Wrap::REPEAT) { check(o.isObject()); if (o.isMember("magFilter")) { @@ -1009,21 +1015,16 @@ struct Sampler { if (o.isMember("name")) { name = as(o["name"]); } + static std::unordered_map map = { + {10497, Wrap::REPEAT}, + {33071, Wrap::CLAMP_TO_EDGE}, + {33648, Wrap::MIRRORED_REPEAT}, + }; if (o.isMember("wrapS")) { - static std::unordered_map map = { - {10497, WrapS::REPEAT}, - {33071, WrapS::CLAMP_TO_EDGE}, - {33648, WrapS::MIRRORED_REPEAT}, - }; const auto &v = o["wrapS"]; check(v.isUInt64()); wrapS = map.at(v.asUInt64()); } if (o.isMember("wrapT")) { - static std::unordered_map map = { - {10497, WrapT::REPEAT}, - {33071, WrapT::CLAMP_TO_EDGE}, - {33648, WrapT::MIRRORED_REPEAT}, - }; const auto &v = o["wrapT"]; check(v.isUInt64()); wrapT = map.at(v.asUInt64()); } @@ -1093,6 +1094,12 @@ struct Texture { }; template<> Texture as(const Json::Value &o) { return o; } +using UriResolver = std::function; +static inline std::string uriError(const std::string &uri) { + // only base64 data URI support by default + throw std::runtime_error("unsupported URI: " + uri); +} + struct GlTF { std::optional> accessors; std::optional> animations; @@ -1111,12 +1118,10 @@ struct GlTF { std::optional> scenes; std::optional> skins; std::optional> textures; - static std::string uriError(const std::string &uri) { - // only base64 data URI support by default - throw std::runtime_error("unsupported URI: " + uri); - } + GlTF(const Json::Value &o, - const std::function &resolveURI = uriError) + const UriResolver &resolveUri = uriError, + std::optional &&glbData = std::nullopt) : asset(as(o["asset"])) { check(o.isObject()); @@ -1138,7 +1143,8 @@ struct GlTF { std::vector bufs; bufs.reserve(b.size()); for (Json::ArrayIndex i = 0; i < b.size(); ++i) { - bufs.emplace_back(b[i], resolveURI); + bufs.emplace_back(b[i], resolveUri, + i == 0 ? std::move(glbData) : std::nullopt); } check(bufs.size() >= 1); buffers = std::move(bufs); @@ -1354,4 +1360,123 @@ struct GlTF { } }; +// std::span is C++ 20, so we roll our own little struct here. +template +struct Span { + T *ptr; + uint32_t len; + bool empty() const { + return len == 0; + } + T *end() const { + return ptr + len; + } + template + Span cast() const { + return {(U *) ptr, len}; + } +}; + +static Json::Value readJson(Span span) { + Json::CharReaderBuilder builder; + const std::unique_ptr reader(builder.newCharReader()); + Json::Value json; + JSONCPP_STRING err; + if (!reader->parse(span.ptr, span.end(), &json, &err)) + throw std::runtime_error(std::string("invalid JSON: ") + err); + return json; +} + +inline GlTF readGlb(const char *data, std::size_t len, const UriResolver &resolveUri = uriError) { + struct Chunk { + uint32_t type; + Span span; + }; + + struct Stream { + Span span; + + bool eof() const { + return span.empty(); + } + + void advance(uint32_t n) { + span.len -= n; + span.ptr += n; + } + + uint32_t readUint32() { + if (span.len < 4) + throw std::runtime_error("premature EOF"); + uint32_t res = 0; + for (int i = 0; i < 4; ++i) + res += span.ptr[i] << (i * 8); + advance(4); + return res; + } + + Chunk readChunk() { + const auto chunkLen = readUint32(); + if (chunkLen % 4 != 0) + throw std::runtime_error("chunk length must be multiple of 4"); + const auto chunkType = readUint32(); + + auto chunkPtr = span.ptr; + if (span.len < chunkLen) + throw std::runtime_error("premature EOF"); + advance(chunkLen); + return {chunkType, {chunkPtr, chunkLen}}; + } + }; + + constexpr uint32_t MAGIC_GLTF = 0x46546C67; + constexpr uint32_t MAGIC_JSON = 0x4E4F534A; + constexpr uint32_t MAGIC_BIN = 0x004E4942; + + if (len > std::numeric_limits::max()) + throw std::runtime_error("too large"); + + Stream is{{(const uint8_t *) data, static_cast(len)}}; + + const auto magic = is.readUint32(); + if (magic != MAGIC_GLTF) + throw std::runtime_error("wrong magic number"); + const auto version = is.readUint32(); + if (version != 2) + throw std::runtime_error("wrong version"); + const auto length = is.readUint32(); + if (length != len) + throw std::runtime_error("wrong length"); + + const auto json = is.readChunk(); + if (json.type != MAGIC_JSON) + throw std::runtime_error("expected JSON chunk"); + + std::optional buffer; + if (!is.eof()) { + const auto chunk = is.readChunk(); + if (chunk.type == MAGIC_BIN) + buffer = std::string((const char *) chunk.span.ptr, chunk.span.len); + else if (chunk.type == MAGIC_JSON) + throw std::runtime_error("unexpected chunk"); + // Ignore all other chunks. We still want to validate that + // 1. These chunks are valid; + // 2. These chunks are *not* JSON or BIN chunks + while (!is.eof()) { + const auto type = is.readChunk().type; + if (type == MAGIC_JSON || type == MAGIC_BIN) + throw std::runtime_error("unexpected chunk"); + } + } + + return GlTF(readJson(json.span.cast()), resolveUri, std::move(buffer)); +} + +inline GlTF readGlTF(const char *data, std::size_t len, const UriResolver &resolveUri = uriError) { + if (len > std::numeric_limits::max()) + throw std::runtime_error("too large"); + + return GlTF(readJson({data, static_cast(len)}), resolveUri); +} + } diff --git a/src/client/camera.cpp b/src/client/camera.cpp index bf9ec0bd5..615d30c87 100644 --- a/src/client/camera.cpp +++ b/src/client/camera.cpp @@ -405,10 +405,11 @@ void Camera::update(LocalPlayer* player, f32 frametime, f32 tool_reload_ratio) // Compute absolute camera position and target m_headnode->getAbsoluteTransformation().transformVect(m_camera_position, rel_cam_pos); - m_headnode->getAbsoluteTransformation().rotateVect(m_camera_direction, rel_cam_target - rel_cam_pos); + m_camera_direction = m_headnode->getAbsoluteTransformation() + .rotateAndScaleVect(rel_cam_target - rel_cam_pos); - v3f abs_cam_up; - m_headnode->getAbsoluteTransformation().rotateVect(abs_cam_up, rel_cam_up); + v3f abs_cam_up = m_headnode->getAbsoluteTransformation() + .rotateAndScaleVect(rel_cam_up); // Separate camera position for calculation v3f my_cp = m_camera_position; diff --git a/src/client/client.cpp b/src/client/client.cpp index 1a2f51db9..0f90bca97 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -827,7 +827,7 @@ bool Client::loadMedia(const std::string &data, const std::string &filename, } const char *model_ext[] = { - ".x", ".b3d", ".obj", ".gltf", + ".x", ".b3d", ".obj", ".gltf", ".glb", NULL }; name = removeStringEnd(filename, model_ext); @@ -1034,7 +1034,7 @@ void Client::Send(NetworkPacket* pkt) m_con->Send(PEER_ID_SERVER, scf.channel, pkt, scf.reliable); } -// Will fill up 12 + 12 + 4 + 4 + 4 + 1 + 1 + 1 bytes +// Will fill up 12 + 12 + 4 + 4 + 4 + 1 + 1 + 1 + 4 + 4 bytes void writePlayerPos(LocalPlayer *myplayer, ClientMap *clientMap, NetworkPacket *pkt, bool camera_inverted) { v3f pf = myplayer->getPosition() * 100; @@ -1046,6 +1046,8 @@ void writePlayerPos(LocalPlayer *myplayer, ClientMap *clientMap, NetworkPacket * u8 fov = std::fmin(255.0f, clientMap->getCameraFov() * 80.0f); u8 wanted_range = std::fmin(255.0f, std::ceil(clientMap->getWantedRange() * (1.0f / MAP_BLOCKSIZE))); + f32 movement_speed = myplayer->control.movement_speed; + f32 movement_dir = myplayer->control.movement_direction; v3s32 position(pf.X, pf.Y, pf.Z); v3s32 speed(sf.X, sf.Y, sf.Z); @@ -1060,10 +1062,13 @@ void writePlayerPos(LocalPlayer *myplayer, ClientMap *clientMap, NetworkPacket * [12+12+4+4+4] u8 fov*80 [12+12+4+4+4+1] u8 ceil(wanted_range / MAP_BLOCKSIZE) [12+12+4+4+4+1+1] u8 camera_inverted (bool) + [12+12+4+4+4+1+1+1] f32 movement_speed + [12+12+4+4+4+1+1+1+4] f32 movement_direction */ *pkt << position << speed << pitch << yaw << keyPressed; *pkt << fov << wanted_range; *pkt << camera_inverted; + *pkt << movement_speed << movement_dir; } void Client::interact(InteractAction action, const PointedThing& pointed) @@ -1142,7 +1147,7 @@ void Client::sendInit(const std::string &playerName) NetworkPacket pkt(TOSERVER_INIT, 1 + 2 + 2 + (1 + playerName.size())); pkt << (u8) SER_FMT_VER_HIGHEST_READ << (u16) 0; - pkt << (u16) CLIENT_PROTOCOL_VERSION_MIN << (u16) CLIENT_PROTOCOL_VERSION_MAX; + pkt << CLIENT_PROTOCOL_VERSION_MIN << LATEST_PROTOCOL_VERSION; pkt << playerName; Send(&pkt); @@ -1397,6 +1402,8 @@ void Client::sendPlayerPos() u32 keyPressed = player->control.getKeysPressed(); bool camera_inverted = m_camera->getCameraMode() == CAMERA_MODE_THIRD_FRONT; + f32 movement_speed = player->control.movement_speed; + f32 movement_dir = player->control.movement_direction; if ( player->last_position == player->getPosition() && @@ -1406,7 +1413,9 @@ void Client::sendPlayerPos() player->last_keyPressed == keyPressed && player->last_camera_fov == camera_fov && player->last_camera_inverted == camera_inverted && - player->last_wanted_range == wanted_range) + player->last_wanted_range == wanted_range && + player->last_movement_speed == movement_speed && + player->last_movement_dir == movement_dir) return; player->last_position = player->getPosition(); @@ -1417,8 +1426,10 @@ void Client::sendPlayerPos() player->last_camera_fov = camera_fov; player->last_camera_inverted = camera_inverted; player->last_wanted_range = wanted_range; + player->last_movement_speed = movement_speed; + player->last_movement_dir = movement_dir; - NetworkPacket pkt(TOSERVER_PLAYERPOS, 12 + 12 + 4 + 4 + 4 + 1 + 1 + 1); + NetworkPacket pkt(TOSERVER_PLAYERPOS, 12 + 12 + 4 + 4 + 4 + 1 + 1 + 1 + 4 + 4); writePlayerPos(player, &map, &pkt, camera_inverted); diff --git a/src/client/client.h b/src/client/client.h index f9f77ede4..0b26ff94d 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -35,6 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "gameparams.h" #include "script/common/c_types.h" // LuaError #include "util/numeric.h" +#include "util/string.h" // StringMap #ifdef SERVER #error Do not include in server builds diff --git a/src/client/clientmap.cpp b/src/client/clientmap.cpp index d608ae2f6..ab826c775 100644 --- a/src/client/clientmap.cpp +++ b/src/client/clientmap.cpp @@ -1015,8 +1015,7 @@ int ClientMap::getBackgroundBrightness(float max_d, u32 daylight_factor, v3f z_dir = z_directions[i]; core::CMatrix4 a; a.buildRotateFromTo(v3f(0,1,0), z_dir); - v3f dir = m_camera_direction; - a.rotateVect(dir); + v3f dir = a.rotateAndScaleVect(m_camera_direction); int br = 0; float step = BS*1.5; if(max_d > 35*BS) diff --git a/src/client/content_cao.cpp b/src/client/content_cao.cpp index adec70983..c8acb3875 100644 --- a/src/client/content_cao.cpp +++ b/src/client/content_cao.cpp @@ -1052,7 +1052,7 @@ void GenericCAO::step(float dtime, ClientEnvironment *env) walking = true; } - v2s32 new_anim = v2s32(0,0); + v2f new_anim(0,0); bool allow_update = false; // increase speed if using fast or flying fast @@ -1799,10 +1799,9 @@ void GenericCAO::processMessage(const std::string &data) phys.speed_walk = override_speed_walk; } } else if (cmd == AO_CMD_SET_ANIMATION) { - // TODO: change frames send as v2s32 value v2f range = readV2F32(is); if (!m_is_local_player) { - m_animation_range = v2s32((s32)range.X, (s32)range.Y); + m_animation_range = range; m_animation_speed = readF32(is); m_animation_blend = readF32(is); // these are sent inverted so we get true when the server sends nothing @@ -1812,7 +1811,7 @@ void GenericCAO::processMessage(const std::string &data) LocalPlayer *player = m_env->getLocalPlayer(); if(player->last_animation == LocalPlayerAnimation::NO_ANIM) { - m_animation_range = v2s32((s32)range.X, (s32)range.Y); + m_animation_range = range; m_animation_speed = readF32(is); m_animation_blend = readF32(is); // these are sent inverted so we get true when the server sends nothing diff --git a/src/client/content_cao.h b/src/client/content_cao.h index 3fdf01bc7..d138e39c3 100644 --- a/src/client/content_cao.h +++ b/src/client/content_cao.h @@ -99,7 +99,7 @@ private: v2s16 m_tx_basepos; bool m_initial_tx_basepos_set = false; bool m_tx_select_horiz_by_yawpitch = false; - v2s32 m_animation_range; + v2f m_animation_range; float m_animation_speed = 15.0f; float m_animation_blend = 0.0f; bool m_animation_loop = true; diff --git a/src/client/content_mapblock.cpp b/src/client/content_mapblock.cpp index 2a1352139..6ce3ca0f4 100644 --- a/src/client/content_mapblock.cpp +++ b/src/client/content_mapblock.cpp @@ -1016,13 +1016,6 @@ void MapblockMeshGenerator::drawGlasslikeFramedNode() } } -void MapblockMeshGenerator::drawAllfacesNode() -{ - static const aabb3f box(-BS / 2, -BS / 2, -BS / 2, BS / 2, BS / 2, BS / 2); - useTile(0, 0, 0); - drawAutoLightedCuboid(box); -} - void MapblockMeshGenerator::drawTorchlikeNode() { u8 wall = cur_node.n.getWallMounted(nodedef); @@ -1545,6 +1538,17 @@ namespace { }; } +void MapblockMeshGenerator::drawAllfacesNode() +{ + static const aabb3f box(-BS / 2, -BS / 2, -BS / 2, BS / 2, BS / 2, BS / 2); + TileSpec tiles[6]; + for (int face = 0; face < 6; face++) + getTile(nodebox_tile_dirs[face], &tiles[face]); + if (data->m_smooth_lighting) + getSmoothLightFrame(); + drawAutoLightedCuboid(box, nullptr, tiles, 6); +} + void MapblockMeshGenerator::drawNodeboxNode() { TileSpec tiles[6]; diff --git a/src/client/game.cpp b/src/client/game.cpp index faf0ef65d..695581f3f 100644 --- a/src/client/game.cpp +++ b/src/client/game.cpp @@ -43,6 +43,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "gui/touchcontrols.h" #include "itemdef.h" #include "log.h" +#include "log_internal.h" #include "filesys.h" #include "gameparams.h" #include "gettext.h" @@ -413,16 +414,8 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter float m_user_exposure_compensation; bool m_bloom_enabled; CachedPixelShaderSetting m_bloom_intensity_pixel{"bloomIntensity"}; - float m_bloom_intensity; CachedPixelShaderSetting m_bloom_strength_pixel{"bloomStrength"}; - float m_bloom_strength; CachedPixelShaderSetting m_bloom_radius_pixel{"bloomRadius"}; - float m_bloom_radius; - CachedPixelShaderSetting m_cloud_height_pixel{"cloudHeight"}; - CachedPixelShaderSetting m_cloud_thickness_pixel{"cloudThickness"}; - CachedPixelShaderSetting m_cloud_density_pixel{"cloudDensity"}; - CachedPixelShaderSetting m_cloud_offset_pixel{"cloudOffset"}; - CachedPixelShaderSetting m_cloud_radius_pixel{"cloudRadius"}; CachedPixelShaderSetting m_saturation_pixel{"saturation"}; float m_gamma; CachedPixelShaderSetting m_gamma_pixel{"gamma"}; @@ -436,12 +429,8 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter CachedPixelShaderSetting m_volumetric_light_strength_pixel{"volumetricLightStrength"}; - static constexpr std::array SETTING_CALLBACKS = { + static constexpr std::array SETTING_CALLBACKS = { "exposure_compensation", - "bloom_intensity", - "bloom_strength_factor", - "bloom_radius", - "gamma" }; public: @@ -449,14 +438,6 @@ public: { if (name == "exposure_compensation") m_user_exposure_compensation = g_settings->getFloat("exposure_compensation", -1.0f, 1.0f); - if (name == "bloom_intensity") - m_bloom_intensity = g_settings->getFloat("bloom_intensity", 0.01f, 1.0f); - if (name == "bloom_strength_factor") - m_bloom_strength = RenderingEngine::BASE_BLOOM_STRENGTH * g_settings->getFloat("bloom_strength_factor", 0.1f, 10.0f); - if (name == "bloom_radius") - m_bloom_radius = g_settings->getFloat("bloom_radius", 0.1f, 8.0f); - if (name == "gamma") - m_gamma = g_settings->getFloat("gamma", 1.0f, 5.0f); } static void settingsCallback(const std::string &name, void *userdata) @@ -475,10 +456,6 @@ public: m_user_exposure_compensation = g_settings->getFloat("exposure_compensation", -1.0f, 1.0f); m_bloom_enabled = g_settings->getBool("enable_bloom"); - m_bloom_intensity = g_settings->getFloat("bloom_intensity", 0.01f, 1.0f); - m_bloom_strength = RenderingEngine::BASE_BLOOM_STRENGTH * g_settings->getFloat("bloom_strength_factor", 0.1f, 10.0f); - m_bloom_radius = g_settings->getFloat("bloom_radius", 0.1f, 8.0f); - m_gamma = g_settings->getFloat("gamma", 1.0f, 5.0f); m_volumetric_light_enabled = g_settings->getBool("enable_volumetric_lighting") && m_bloom_enabled; } @@ -547,7 +524,9 @@ public: m_texel_size0_vertex.set(m_texel_size0, services); m_texel_size0_pixel.set(m_texel_size0, services); - const AutoExposure &exposure_params = m_client->getEnv().getLocalPlayer()->getLighting().exposure; + const auto &lighting = m_client->getEnv().getLocalPlayer()->getLighting(); + + const AutoExposure &exposure_params = lighting.exposure; std::array exposure_buffer = { std::pow(2.0f, exposure_params.luminance_min), std::pow(2.0f, exposure_params.luminance_max), @@ -560,14 +539,14 @@ public: m_exposure_params_pixel.set(exposure_buffer.data(), services); if (m_bloom_enabled) { - m_bloom_intensity_pixel.set(&m_bloom_intensity, services); - m_bloom_radius_pixel.set(&m_bloom_radius, services); - m_bloom_strength_pixel.set(&m_bloom_strength, services); + float intensity = std::max(lighting.bloom_intensity, 0.0f); + m_bloom_intensity_pixel.set(&intensity, services); + float strength_factor = std::max(lighting.bloom_strength_factor, 0.0f); + m_bloom_strength_pixel.set(&strength_factor, services); + float radius = std::max(lighting.bloom_radius, 0.0f); + m_bloom_radius_pixel.set(&radius, services); } - m_gamma_pixel.set(&m_gamma, services); - - const auto &lighting = m_client->getEnv().getLocalPlayer()->getLighting(); float saturation = lighting.saturation; m_saturation_pixel.set(&saturation, services); video::SColorf artificial_light = lighting.artificial_light_color; @@ -773,6 +752,7 @@ protected: void processUserInput(f32 dtime); void processKeyInput(); void processItemSelection(u16 *new_playeritem); + bool shouldShowTouchControls(); void dropSelectedItem(bool single_item = false); void openInventory(); @@ -1615,6 +1595,14 @@ bool Game::createClient(const GameStartData &start_data) return true; } +bool Game::shouldShowTouchControls() +{ + const std::string &touch_controls = g_settings->get("touch_controls"); + if (touch_controls == "auto") + return RenderingEngine::getLastPointerType() == PointerType::Touch; + return is_yes(touch_controls); +} + bool Game::initGui() { m_game_ui->init(); @@ -1629,7 +1617,7 @@ bool Game::initGui() gui_chat_console = make_irr(guienv, guienv->getRootGUIElement(), -1, chat_backend, client, &g_menumgr); - if (g_settings->getBool("touch_controls")) { + if (shouldShowTouchControls()) { g_touchcontrols = new TouchControls(device, texture_src); g_touchcontrols->setUseCrosshair(!isTouchCrosshairDisabled()); } @@ -2081,6 +2069,15 @@ void Game::updateStats(RunStats *stats, const FpsControl &draw_times, void Game::processUserInput(f32 dtime) { + bool desired = shouldShowTouchControls(); + if (desired && !g_touchcontrols) { + g_touchcontrols = new TouchControls(device, texture_src); + + } else if (!desired && g_touchcontrols) { + delete g_touchcontrols; + g_touchcontrols = nullptr; + } + // Reset input if window not active or some menu is active if (!device->isWindowActive() || isMenuActive() || guienv->hasFocus(gui_chat_console.get())) { if (m_game_focused) { @@ -2711,7 +2708,7 @@ void Game::updateCameraDirection(CameraOrientation *cam, float dtime) cur_control->setVisible(false); } - if (m_first_loop_after_window_activation) { + if (m_first_loop_after_window_activation && !g_touchcontrols) { m_first_loop_after_window_activation = false; input->setMousePos(driver->getScreenSize().Width / 2, @@ -2727,6 +2724,8 @@ void Game::updateCameraDirection(CameraOrientation *cam, float dtime) m_first_loop_after_window_activation = true; } + if (g_touchcontrols) + m_first_loop_after_window_activation = true; } // Get the factor to multiply with sensitivity to get the same mouse/joystick @@ -2792,9 +2791,10 @@ void Game::updatePlayerControl(const CameraOrientation &cam) isKeyDown(KeyType::PLACE), cam.camera_pitch, cam.camera_yaw, - input->getMovementSpeed(), - input->getMovementDirection() + input->getJoystickSpeed(), + input->getJoystickDirection() ); + control.setMovementFromKeys(); // autoforward if set: move at maximum speed if (player->getPlayerSettings().continuous_forward && diff --git a/src/client/hud.cpp b/src/client/hud.cpp index e4c06b542..2a1acb288 100644 --- a/src/client/hud.cpp +++ b/src/client/hud.cpp @@ -536,9 +536,9 @@ void Hud::drawLuaElements(const v3s16 &camera_offset) return; // Avoid zero divides // Angle according to camera view - v3f fore(0.f, 0.f, 1.f); scene::ICameraSceneNode *cam = client->getSceneManager()->getActiveCamera(); - cam->getAbsoluteTransformation().rotateVect(fore); + v3f fore = cam->getAbsoluteTransformation() + .rotateAndScaleVect(v3f(0.f, 0.f, 1.f)); int angle = - fore.getHorizontalAngle().Y; // Limit angle and ajust with given offset diff --git a/src/client/imagesource.cpp b/src/client/imagesource.cpp index bb6668264..195d57a41 100644 --- a/src/client/imagesource.cpp +++ b/src/client/imagesource.cpp @@ -1447,6 +1447,8 @@ bool ImageSource::generateImagePart(std::string_view part_of_name, video::IImage *img = generateImage(filename, source_image_names); if (img) { + upscaleImagesToMatchLargest(baseimg, img); + apply_mask(img, baseimg, v2s32(0, 0), v2s32(0, 0), img->getDimension()); img->drop(); diff --git a/src/client/inputhandler.cpp b/src/client/inputhandler.cpp index 39c212d2f..2ce058ff4 100644 --- a/src/client/inputhandler.cpp +++ b/src/client/inputhandler.cpp @@ -24,6 +24,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "gui/mainmenumanager.h" #include "gui/touchcontrols.h" #include "hud.h" +#include "log_internal.h" +#include "client/renderingengine.h" void KeyCache::populate_nonchanging() { @@ -141,6 +143,11 @@ bool MyEventReceiver::OnEvent(const SEvent &event) } } + if (event.EventType == EET_MOUSE_INPUT_EVENT && !event.MouseInput.Simulated) + last_pointer_type = PointerType::Mouse; + else if (event.EventType == EET_TOUCH_INPUT_EVENT) + last_pointer_type = PointerType::Touch; + // Let the menu handle events, if one is active. if (isMenuActive()) { if (g_touchcontrols) @@ -220,51 +227,42 @@ bool MyEventReceiver::OnEvent(const SEvent &event) /* * RealInputHandler */ -float RealInputHandler::getMovementSpeed() +float RealInputHandler::getJoystickSpeed() { - bool f = m_receiver->IsKeyDown(keycache.key[KeyType::FORWARD]), - b = m_receiver->IsKeyDown(keycache.key[KeyType::BACKWARD]), - l = m_receiver->IsKeyDown(keycache.key[KeyType::LEFT]), - r = m_receiver->IsKeyDown(keycache.key[KeyType::RIGHT]); - if (f || b || l || r) - { - // if contradictory keys pressed, stay still - if (f && b && l && r) - return 0.0f; - else if (f && b && !l && !r) - return 0.0f; - else if (!f && !b && l && r) - return 0.0f; - return 1.0f; // If there is a keyboard event, assume maximum speed - } - if (g_touchcontrols && g_touchcontrols->getMovementSpeed()) - return g_touchcontrols->getMovementSpeed(); + if (g_touchcontrols && g_touchcontrols->getJoystickSpeed()) + return g_touchcontrols->getJoystickSpeed(); return joystick.getMovementSpeed(); } -float RealInputHandler::getMovementDirection() +float RealInputHandler::getJoystickDirection() { - float x = 0, z = 0; - - /* Check keyboard for input */ - if (m_receiver->IsKeyDown(keycache.key[KeyType::FORWARD])) - z += 1; - if (m_receiver->IsKeyDown(keycache.key[KeyType::BACKWARD])) - z -= 1; - if (m_receiver->IsKeyDown(keycache.key[KeyType::RIGHT])) - x += 1; - if (m_receiver->IsKeyDown(keycache.key[KeyType::LEFT])) - x -= 1; - - if (x != 0 || z != 0) /* If there is a keyboard event, it takes priority */ - return std::atan2(x, z); - // `getMovementDirection() == 0` means forward, so we cannot use - // `getMovementDirection()` as a condition. - else if (g_touchcontrols && g_touchcontrols->getMovementSpeed()) - return g_touchcontrols->getMovementDirection(); + // `getJoystickDirection() == 0` means forward, so we cannot use + // `getJoystickDirection()` as a condition. + if (g_touchcontrols && g_touchcontrols->getJoystickSpeed()) + return g_touchcontrols->getJoystickDirection(); return joystick.getMovementDirection(); } +v2s32 RealInputHandler::getMousePos() +{ + auto control = RenderingEngine::get_raw_device()->getCursorControl(); + if (control) { + return control->getPosition(); + } + + return m_mousepos; +} + +void RealInputHandler::setMousePos(s32 x, s32 y) +{ + auto control = RenderingEngine::get_raw_device()->getCursorControl(); + if (control) { + control->setPosition(x, y); + } else { + m_mousepos = v2s32(x, y); + } +} + /* * RandomInputHandler */ @@ -320,25 +318,11 @@ void RandomInputHandler::step(float dtime) counterMovement -= dtime; if (counterMovement < 0.0) { counterMovement = 0.1 * Rand(1, 40); - movementSpeed = Rand(0,100)*0.01; - movementDirection = Rand(-100, 100)*0.01 * M_PI; + joystickSpeed = Rand(0,100)*0.01; + joystickDirection = Rand(-100, 100)*0.01 * M_PI; } } else { - bool f = keydown[keycache.key[KeyType::FORWARD]], - l = keydown[keycache.key[KeyType::LEFT]]; - if (f || l) { - movementSpeed = 1.0f; - if (f && !l) - movementDirection = 0.0; - else if (!f && l) - movementDirection = -M_PI_2; - else if (f && l) - movementDirection = -M_PI_4; - else - movementDirection = 0.0; - } else { - movementSpeed = 0.0; - movementDirection = 0.0; - } + joystickSpeed = 0.0f; + joystickDirection = 0.0f; } } diff --git a/src/client/inputhandler.h b/src/client/inputhandler.h index daf01c488..ee76151f4 100644 --- a/src/client/inputhandler.h +++ b/src/client/inputhandler.h @@ -23,10 +23,14 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "joystick_controller.h" #include #include "keycode.h" -#include "renderingengine.h" class InputHandler; +enum class PointerType { + Mouse, + Touch, +}; + /**************************************************************************** Fast key cache for main game loop ****************************************************************************/ @@ -199,6 +203,8 @@ public: JoystickController *joystick = nullptr; + PointerType getLastPointerType() { return last_pointer_type; } + private: s32 mouse_wheel = 0; @@ -223,6 +229,8 @@ private: // Intentionally not reset by clearInput/releaseAllKeys. bool fullscreen_is_down = false; + + PointerType last_pointer_type = PointerType::Mouse; }; class InputHandler @@ -247,8 +255,8 @@ public: virtual bool wasKeyReleased(GameKeyType k) = 0; virtual bool cancelPressed() = 0; - virtual float getMovementSpeed() = 0; - virtual float getMovementDirection() = 0; + virtual float getJoystickSpeed() = 0; + virtual float getJoystickDirection() = 0; virtual void clearWasKeyPressed() {} virtual void clearWasKeyReleased() {} @@ -304,9 +312,9 @@ public: return m_receiver->WasKeyReleased(keycache.key[k]) || joystick.wasKeyReleased(k); } - virtual float getMovementSpeed(); + virtual float getJoystickSpeed(); - virtual float getMovementDirection(); + virtual float getJoystickDirection(); virtual bool cancelPressed() { @@ -331,25 +339,8 @@ public: m_receiver->dontListenForKeys(); } - virtual v2s32 getMousePos() - { - auto control = RenderingEngine::get_raw_device()->getCursorControl(); - if (control) { - return control->getPosition(); - } - - return m_mousepos; - } - - virtual void setMousePos(s32 x, s32 y) - { - auto control = RenderingEngine::get_raw_device()->getCursorControl(); - if (control) { - control->setPosition(x, y); - } else { - m_mousepos = v2s32(x, y); - } - } + virtual v2s32 getMousePos(); + virtual void setMousePos(s32 x, s32 y); virtual s32 getMouseWheel() { @@ -388,8 +379,8 @@ public: virtual bool wasKeyPressed(GameKeyType k) { return false; } virtual bool wasKeyReleased(GameKeyType k) { return false; } virtual bool cancelPressed() { return false; } - virtual float getMovementSpeed() { return movementSpeed; } - virtual float getMovementDirection() { return movementDirection; } + virtual float getJoystickSpeed() { return joystickSpeed; } + virtual float getJoystickDirection() { return joystickDirection; } virtual v2s32 getMousePos() { return mousepos; } virtual void setMousePos(s32 x, s32 y) { mousepos = v2s32(x, y); } @@ -403,6 +394,6 @@ private: KeyList keydown; v2s32 mousepos; v2s32 mousespeed; - float movementSpeed; - float movementDirection; + float joystickSpeed; + float joystickDirection; }; diff --git a/src/client/localplayer.h b/src/client/localplayer.h index 815fafa8b..275f556e4 100644 --- a/src/client/localplayer.h +++ b/src/client/localplayer.h @@ -105,6 +105,8 @@ public: u8 last_camera_fov = 0; u8 last_wanted_range = 0; bool last_camera_inverted = false; + f32 last_movement_speed = 0.0f; + f32 last_movement_dir = 0.0f; float camera_impact = 0.0f; diff --git a/src/client/particles.cpp b/src/client/particles.cpp index 1eab93579..3a2dace12 100644 --- a/src/client/particles.cpp +++ b/src/client/particles.cpp @@ -357,16 +357,18 @@ void ParticleSpawner::spawnParticle(ClientEnvironment *env, float radius, if (attached_absolute_pos_rot_matrix) { // Apply attachment rotation - attached_absolute_pos_rot_matrix->rotateVect(pp.vel); - attached_absolute_pos_rot_matrix->rotateVect(pp.acc); + pp.vel = attached_absolute_pos_rot_matrix->rotateAndScaleVect(pp.vel); + pp.acc = attached_absolute_pos_rot_matrix->rotateAndScaleVect(pp.acc); } if (attractor_obj) attractor_origin += attractor_obj->getPosition() / BS; if (attractor_direction_obj) { auto *attractor_absolute_pos_rot_matrix = attractor_direction_obj->getAbsolutePosRotMatrix(); - if (attractor_absolute_pos_rot_matrix) - attractor_absolute_pos_rot_matrix->rotateVect(attractor_direction); + if (attractor_absolute_pos_rot_matrix) { + attractor_direction = attractor_absolute_pos_rot_matrix + ->rotateAndScaleVect(attractor_direction); + } } pp.expirationtime = r_exp.pickWithin(); diff --git a/src/client/renderingengine.cpp b/src/client/renderingengine.cpp index c4933e062..f0d2abddb 100644 --- a/src/client/renderingengine.cpp +++ b/src/client/renderingengine.cpp @@ -41,7 +41,6 @@ with this program; if not, write to the Free Software Foundation, Inc., RenderingEngine *RenderingEngine::s_singleton = nullptr; const video::SColor RenderingEngine::MENU_SKY_COLOR = video::SColor(255, 140, 186, 250); -const float RenderingEngine::BASE_BLOOM_STRENGTH = 1.0f; /* Helper classes */ @@ -173,7 +172,7 @@ static irr::IrrlichtDevice *createDevice(SIrrlichtCreationParameters params, std /* RenderingEngine class */ -RenderingEngine::RenderingEngine(IEventReceiver *receiver) +RenderingEngine::RenderingEngine(MyEventReceiver *receiver) { sanity_check(!s_singleton); @@ -226,6 +225,8 @@ RenderingEngine::RenderingEngine(IEventReceiver *receiver) // This changes the minimum allowed number of vertices in a VBO. Default is 500. driver->setMinHardwareBufferVertexCount(4); + m_receiver = receiver; + s_singleton = this; g_settings->registerChangedCallback("fullscreen", settingChangedCallback, this); diff --git a/src/client/renderingengine.h b/src/client/renderingengine.h index 7f7518f61..ffdda636c 100644 --- a/src/client/renderingengine.h +++ b/src/client/renderingengine.h @@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include +#include "client/inputhandler.h" #include "irrlichttypes_extrabloated.h" #include "debug.h" #include "client/shader.h" @@ -81,9 +82,8 @@ class RenderingEngine { public: static const video::SColor MENU_SKY_COLOR; - static const float BASE_BLOOM_STRENGTH; - RenderingEngine(IEventReceiver *eventReceiver); + RenderingEngine(MyEventReceiver *eventReceiver); ~RenderingEngine(); void setResizable(bool resize); @@ -168,6 +168,12 @@ public: const irr::core::dimension2d initial_screen_size, const bool initial_window_maximized); + static PointerType getLastPointerType() + { + sanity_check(s_singleton && s_singleton->m_receiver); + return s_singleton->m_receiver->getLastPointerType(); + } + private: static void settingChangedCallback(const std::string &name, void *data); v2u32 _getWindowSize() const; @@ -175,5 +181,6 @@ private: std::unique_ptr core; irr::IrrlichtDevice *m_device = nullptr; irr::video::IVideoDriver *driver; + MyEventReceiver *m_receiver = nullptr; static RenderingEngine *s_singleton; }; diff --git a/src/client/shader.cpp b/src/client/shader.cpp index f75d48d53..2f87d40d5 100644 --- a/src/client/shader.cpp +++ b/src/client/shader.cpp @@ -322,6 +322,9 @@ public: private: + // Are shaders even enabled? + bool m_enabled; + // The id of the thread that is allowed to use irrlicht directly std::thread::id m_main_thread; @@ -360,6 +363,12 @@ ShaderSource::ShaderSource() // Add a dummy ShaderInfo as the first index, named "" m_shaderinfo_cache.emplace_back(); + m_enabled = g_settings->getBool("enable_shaders"); + if (!m_enabled) { + warningstream << "You are running " PROJECT_NAME_C " with shaders disabled, " + "this is not a recommended configuration." << std::endl; + } + // Add main global constant setter addShaderConstantSetterFactory(new MainShaderConstantSetterFactory()); } @@ -368,9 +377,11 @@ ShaderSource::~ShaderSource() { MutexAutoLock lock(m_shaderinfo_cache_mutex); + if (!m_enabled) + return; + // Delete materials - video::IGPUProgrammingServices *gpu = RenderingEngine::get_video_driver()-> - getGPUProgrammingServices(); + auto *gpu = RenderingEngine::get_video_driver()->getGPUProgrammingServices(); for (ShaderInfo &i : m_shaderinfo_cache) { if (!i.name.empty()) gpu->deleteShaderMaterial(i.material); @@ -499,9 +510,11 @@ void ShaderSource::rebuildShaders() { MutexAutoLock lock(m_shaderinfo_cache_mutex); + if (!m_enabled) + return; + // Delete materials - video::IGPUProgrammingServices *gpu = RenderingEngine::get_video_driver()-> - getGPUProgrammingServices(); + auto *gpu = RenderingEngine::get_video_driver()->getGPUProgrammingServices(); for (ShaderInfo &i : m_shaderinfo_cache) { if (!i.name.empty()) { gpu->deleteShaderMaterial(i.material); @@ -548,12 +561,11 @@ ShaderInfo ShaderSource::generateShader(const std::string &name, } shaderinfo.material = shaderinfo.base_material; - bool enable_shaders = g_settings->getBool("enable_shaders"); - if (!enable_shaders) + if (!m_enabled) return shaderinfo; video::IVideoDriver *driver = RenderingEngine::get_video_driver(); - video::IGPUProgrammingServices *gpu = driver->getGPUProgrammingServices(); + auto *gpu = driver->getGPUProgrammingServices(); if (!driver->queryFeature(video::EVDF_ARB_GLSL) || !gpu) { throw ShaderException(gettext("Shaders are enabled but GLSL is not " "supported by the driver.")); @@ -561,7 +573,7 @@ ShaderInfo ShaderSource::generateShader(const std::string &name, // Create shaders header bool fully_programmable = driver->getDriverType() == video::EDT_OGLES2 || driver->getDriverType() == video::EDT_OPENGL3; - std::stringstream shaders_header; + std::ostringstream shaders_header; shaders_header << std::noboolalpha << std::showpoint // for GLSL ES @@ -588,10 +600,14 @@ ShaderInfo ShaderSource::generateShader(const std::string &name, attribute mediump vec4 inVertexTangent; attribute mediump vec4 inVertexBinormal; )"; + // Our vertex color has components reversed compared to what OpenGL + // normally expects, so we need to take that into account. + vertex_header += "#define inVertexColor (inVertexColor.bgra)\n"; fragment_header = R"( precision mediump float; )"; } else { + /* legacy OpenGL driver */ shaders_header << R"( #version 120 #define lowp diff --git a/src/client/shadows/dynamicshadows.cpp b/src/client/shadows/dynamicshadows.cpp index ffe7d4de5..2722c871b 100644 --- a/src/client/shadows/dynamicshadows.cpp +++ b/src/client/shadows/dynamicshadows.cpp @@ -137,8 +137,8 @@ void DirectionalLight::update_frustum(const Camera *cam, Client *client, bool fo // when camera offset changes, adjust the current frustum view matrix to avoid flicker v3s16 cam_offset = cam->getOffset(); if (cam_offset != shadow_frustum.camera_offset) { - v3f rotated_offset; - shadow_frustum.ViewMat.rotateVect(rotated_offset, intToFloat(cam_offset - shadow_frustum.camera_offset, BS)); + v3f rotated_offset = shadow_frustum.ViewMat.rotateAndScaleVect( + intToFloat(cam_offset - shadow_frustum.camera_offset, BS)); shadow_frustum.ViewMat.setTranslation(shadow_frustum.ViewMat.getTranslation() + rotated_offset); shadow_frustum.player += intToFloat(shadow_frustum.camera_offset - cam->getOffset(), BS); shadow_frustum.camera_offset = cam_offset; diff --git a/src/client/sky.cpp b/src/client/sky.cpp index 65577418e..27640bc28 100644 --- a/src/client/sky.cpp +++ b/src/client/sky.cpp @@ -838,14 +838,10 @@ void Sky::updateStars() ); core::CMatrix4 a; a.buildRotateFromTo(v3f(0, 1, 0), r); - v3f p = v3f(-d, 1, -d); - v3f p1 = v3f(d, 1, -d); - v3f p2 = v3f(d, 1, d); - v3f p3 = v3f(-d, 1, d); - a.rotateVect(p); - a.rotateVect(p1); - a.rotateVect(p2); - a.rotateVect(p3); + v3f p = a.rotateAndScaleVect(v3f(-d, 1, -d)); + v3f p1 = a.rotateAndScaleVect(v3f(d, 1, -d)); + v3f p2 = a.rotateAndScaleVect(v3f(d, 1, d)); + v3f p3 = a.rotateAndScaleVect(v3f(-d, 1, d)); vertices.push_back(video::S3DVertex(p, {}, {}, {})); vertices.push_back(video::S3DVertex(p1, {}, {}, {})); vertices.push_back(video::S3DVertex(p2, {}, {}, {})); diff --git a/src/client/sound/ogg_file.cpp b/src/client/sound/ogg_file.cpp index 11659c706..660dfdf94 100644 --- a/src/client/sound/ogg_file.cpp +++ b/src/client/sound/ogg_file.cpp @@ -26,6 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include // memcpy +#include namespace sound { diff --git a/src/client/sound/sound_singleton.h b/src/client/sound/sound_singleton.h index 32cd2d4f8..10ecc0d96 100644 --- a/src/client/sound/sound_singleton.h +++ b/src/client/sound/sound_singleton.h @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once +#include #include "al_helpers.h" namespace sound { diff --git a/src/craftdef.cpp b/src/craftdef.cpp index 72b8e8f9d..611632579 100644 --- a/src/craftdef.cpp +++ b/src/craftdef.cpp @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include +#include #include "gamedef.h" #include "inventory.h" #include "util/serialize.h" diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp index 6625dfb03..d8d035ee0 100644 --- a/src/defaultsettings.cpp +++ b/src/defaultsettings.cpp @@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "porting.h" #include "mapgen/mapgen.h" // Mapgen::setDefaultSettings #include "util/string.h" +#include "server.h" /* @@ -97,7 +98,20 @@ void set_default_settings() // Client settings->setDefault("address", ""); settings->setDefault("enable_sound", "true"); +#if defined(__unix__) && !defined(__APPLE__) && !defined (__ANDROID__) + // On Linux+X11 (not Linux+Wayland or Linux+XWayland), I've encountered a bug + // where fake mouse events were generated from touch events if in relative + // mouse mode, resulting in the touchscreen controls being instantly disabled + // again and thus making them unusable. + // => We can't switch based on the last input method used. + // => Fall back to hardware detection. settings->setDefault("touch_controls", bool_to_cstr(has_touch)); +#else + settings->setDefault("touch_controls", "auto"); +#endif + // Since GUI scaling shouldn't suddenly change during a session, we use + // hardware detection for "touch_gui" instead of switching based on the last + // input method used. settings->setDefault("touch_gui", bool_to_cstr(has_touch)); settings->setDefault("sound_volume", "0.8"); settings->setDefault("sound_volume_unfocused", "0.3"); @@ -335,9 +349,6 @@ void set_default_settings() settings->setDefault("antialiasing", "none"); settings->setDefault("enable_bloom", "false"); settings->setDefault("enable_bloom_debug", "false"); - settings->setDefault("bloom_strength_factor", "1.0"); - settings->setDefault("bloom_intensity", "0.05"); - settings->setDefault("bloom_radius", "1"); settings->setDefault("enable_volumetric_lighting", "false"); settings->setDefault("enable_bumpmaps", "false"); settings->setDefault("enable_water_reflections", "false"); @@ -454,7 +465,9 @@ void set_default_settings() settings->setDefault("enable_pvp", "true"); settings->setDefault("enable_mod_channels", "false"); settings->setDefault("disallow_empty_password", "false"); - settings->setDefault("disable_anticheat", "false"); + settings->setDefault("anticheat_flags", flagdesc_anticheat, + AC_DIGGING | AC_INTERACTION | AC_MOVEMENT); + settings->setDefault("anticheat_movement_tolerance", "1.0"); settings->setDefault("enable_rollback_recording", "false"); settings->setDefault("deprecated_lua_api_handling", "log"); diff --git a/src/emerge.cpp b/src/emerge.cpp index 425e294b8..788e2b745 100644 --- a/src/emerge.cpp +++ b/src/emerge.cpp @@ -31,6 +31,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "filesys.h" #include "log.h" #include "servermap.h" +#include "database/database.h" #include "mapblock.h" #include "mapgen/mg_biome.h" #include "mapgen/mg_ore.h" @@ -185,10 +186,22 @@ SchematicManager *EmergeManager::getWritableSchematicManager() return schemmgr; } +void EmergeManager::initMap(MapDatabaseAccessor *holder) +{ + FATAL_ERROR_IF(m_db, "Map database already initialized."); + assert(holder->dbase); + m_db = holder; +} + +void EmergeManager::resetMap() +{ + FATAL_ERROR_IF(m_threads_active, "Threads are still active."); + m_db = nullptr; +} void EmergeManager::initMapgens(MapgenParams *params) { - FATAL_ERROR_IF(!m_mapgens.empty(), "Mapgen already initialised."); + FATAL_ERROR_IF(!m_mapgens.empty(), "Mapgen already initialized."); mgparams = params; @@ -303,6 +316,12 @@ bool EmergeManager::enqueueBlockEmergeEx( } +size_t EmergeManager::getQueueSize() +{ + MutexAutoLock queuelock(m_queue_mutex); + return m_blocks_enqueued.size(); +} + bool EmergeManager::isBlockInQueue(v3s16 pos) { MutexAutoLock queuelock(m_queue_mutex); @@ -466,7 +485,7 @@ void EmergeThread::signal() } -bool EmergeThread::pushBlock(const v3s16 &pos) +bool EmergeThread::pushBlock(v3s16 pos) { m_block_queue.push(pos); return true; @@ -491,7 +510,7 @@ void EmergeThread::cancelPendingItems() } -void EmergeThread::runCompletionCallbacks(const v3s16 &pos, EmergeAction action, +void EmergeThread::runCompletionCallbacks(v3s16 pos, EmergeAction action, const EmergeCallbackList &callbacks) { m_emerge->reportCompletedEmerge(action); @@ -524,21 +543,38 @@ bool EmergeThread::popBlockEmerge(v3s16 *pos, BlockEmergeData *bedata) } -EmergeAction EmergeThread::getBlockOrStartGen( - const v3s16 &pos, bool allow_gen, MapBlock **block, BlockMakeData *bmdata) +EmergeAction EmergeThread::getBlockOrStartGen(const v3s16 pos, bool allow_gen, + const std::string *from_db, MapBlock **block, BlockMakeData *bmdata) { - MutexAutoLock envlock(m_server->m_env_mutex); + //TimeTaker tt("", nullptr, PRECISION_MICRO); + Server::EnvAutoLock envlock(m_server); + //g_profiler->avg("EmergeThread: lock wait time [us]", tt.stop()); + + auto block_ok = [] (MapBlock *b) { + return b && b->isGenerated(); + }; // 1). Attempt to fetch block from memory *block = m_map->getBlockNoCreateNoEx(pos); if (*block) { - if ((*block)->isGenerated()) + if (block_ok(*block)) { + // if we just read it from the db but the block exists that means + // someone else was faster. don't touch it to prevent data loss. + if (from_db) + verbosestream << "getBlockOrStartGen: block loading raced" << std::endl; return EMERGE_FROM_MEMORY; + } } else { - // 2). Attempt to load block from disk if it was not in the memory - *block = m_map->loadBlock(pos); - if (*block && (*block)->isGenerated()) + if (!from_db) { + // 2). We should attempt loading it return EMERGE_FROM_DISK; + } + // 2). Second invocation, we have the data + if (!from_db->empty()) { + *block = m_map->loadBlock(*from_db, pos); + if (block_ok(*block)) + return EMERGE_FROM_DISK; + } } // 3). Attempt to start generation @@ -553,7 +589,7 @@ EmergeAction EmergeThread::getBlockOrStartGen( MapBlock *EmergeThread::finishGen(v3s16 pos, BlockMakeData *bmdata, std::map *modified_blocks) { - MutexAutoLock envlock(m_server->m_env_mutex); + Server::EnvAutoLock envlock(m_server); ScopeProfiler sp(g_profiler, "EmergeThread: after Mapgen::makeChunk", SPT_AVG); @@ -643,7 +679,8 @@ void *EmergeThread::run() BEGIN_DEBUG_EXCEPTION_HANDLER v3s16 pos; - std::map modified_blocks; + std::map modified_blocks; + std::string databuf; m_map = &m_server->m_env->getServerMap(); m_emerge = m_server->getEmergeManager(); @@ -669,13 +706,30 @@ void *EmergeThread::run() continue; } + g_profiler->add(m_name + ": processed [#]", 1); + if (blockpos_over_max_limit(pos)) continue; bool allow_gen = bedata.flags & BLOCK_EMERGE_ALLOW_GEN; EMERGE_DBG_OUT("pos=" << pos << " allow_gen=" << allow_gen); - action = getBlockOrStartGen(pos, allow_gen, &block, &bmdata); + action = getBlockOrStartGen(pos, allow_gen, nullptr, &block, &bmdata); + + /* Try to load it */ + if (action == EMERGE_FROM_DISK) { + auto &m_db = *m_emerge->m_db; + { + ScopeProfiler sp(g_profiler, "EmergeThread: load block - async (sum)"); + MutexAutoLock dblock(m_db.mutex); + m_db.loadBlock(pos, databuf); + } + // actually load it, then decide again + action = getBlockOrStartGen(pos, allow_gen, &databuf, &block, &bmdata); + databuf.clear(); + } + + /* Generate it */ if (action == EMERGE_GENERATED) { bool error = false; m_trans_liquid = &bmdata.transforming_liquid; @@ -716,7 +770,7 @@ void *EmergeThread::run() MapEditEvent event; event.type = MEET_OTHER; event.setModifiedBlocks(modified_blocks); - MutexAutoLock envlock(m_server->m_env_mutex); + Server::EnvAutoLock envlock(m_server); m_map->dispatchEvent(event); } modified_blocks.clear(); diff --git a/src/emerge.h b/src/emerge.h index d7f018feb..cbdcc4c7c 100644 --- a/src/emerge.h +++ b/src/emerge.h @@ -46,6 +46,7 @@ class DecorationManager; class SchematicManager; class Server; class ModApiMapgen; +struct MapDatabaseAccessor; // Structure containing inputs/outputs for chunk generation struct BlockMakeData { @@ -173,6 +174,10 @@ public: SchematicManager *getWritableSchematicManager(); void initMapgens(MapgenParams *mgparams); + /// @param holder non-owned reference that must stay alive + void initMap(MapDatabaseAccessor *holder); + /// resets the reference + void resetMap(); void startThreads(); void stopThreads(); @@ -191,6 +196,7 @@ public: EmergeCompletionCallback callback, void *callback_param); + size_t getQueueSize(); bool isBlockInQueue(v3s16 pos); Mapgen *getCurrentMapgen(); @@ -206,6 +212,9 @@ private: std::vector m_threads; bool m_threads_active = false; + // The map database + MapDatabaseAccessor *m_db = nullptr; + std::mutex m_queue_mutex; std::map m_blocks_enqueued; std::unordered_map m_peer_queue_count; diff --git a/src/emerge_internal.h b/src/emerge_internal.h index 439c8227b..08e36778d 100644 --- a/src/emerge_internal.h +++ b/src/emerge_internal.h @@ -40,7 +40,7 @@ class EmergeScripting; class EmergeThread : public Thread { public: bool enable_mapgen_debug_info; - int id; + const int id; // Index of this thread EmergeThread(Server *server, int ethreadid); ~EmergeThread() = default; @@ -49,7 +49,7 @@ public: void signal(); // Requires queue mutex held - bool pushBlock(const v3s16 &pos); + bool pushBlock(v3s16 pos); void cancelPendingItems(); @@ -59,7 +59,7 @@ public: protected: void runCompletionCallbacks( - const v3s16 &pos, EmergeAction action, + v3s16 pos, EmergeAction action, const EmergeCallbackList &callbacks); private: @@ -79,8 +79,20 @@ private: bool popBlockEmerge(v3s16 *pos, BlockEmergeData *bedata); - EmergeAction getBlockOrStartGen( - const v3s16 &pos, bool allow_gen, MapBlock **block, BlockMakeData *data); + /** + * Try to get a block from memory and decide what to do. + * + * @param pos block position + * @param from_db serialized block data, optional + * (for second call after EMERGE_FROM_DISK was returned) + * @param allow_gen allow invoking mapgen? + * @param block output pointer for block + * @param data info for mapgen + * @return what to do for this block + */ + EmergeAction getBlockOrStartGen(v3s16 pos, bool allow_gen, + const std::string *from_db, MapBlock **block, BlockMakeData *data); + MapBlock *finishGen(v3s16 pos, BlockMakeData *bmdata, std::map *modified_blocks); diff --git a/src/filesys.cpp b/src/filesys.cpp index 4287c8b05..b0a1f318e 100644 --- a/src/filesys.cpp +++ b/src/filesys.cpp @@ -26,6 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include +#include #include "log.h" #include "config.h" #include "porting.h" @@ -34,6 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #endif + #ifdef __linux__ #include #include @@ -42,6 +44,19 @@ with this program; if not, write to the Free Software Foundation, Inc., #endif #endif +#ifdef _WIN32 +#include +#include +#include +#include +#else +#include +#include +#include +#include +#include +#endif + // Error from last OS call as string #ifdef _WIN32 #define LAST_OS_ERROR() porting::ConvertError(GetLastError()) @@ -58,11 +73,6 @@ namespace fs * Windows * ***********/ -#include -#include -#include -#include - std::vector GetDirListing(const std::string &pathstring) { std::vector listing; @@ -272,12 +282,6 @@ bool CopyFileContents(const std::string &source, const std::string &target) * POSIX * *********/ -#include -#include -#include -#include -#include - std::vector GetDirListing(const std::string &pathstring) { std::vector listing; @@ -380,41 +384,41 @@ bool RecursiveDelete(const std::string &path) Execute the 'rm' command directly, by fork() and execve() */ - infostream<<"Removing \""< argv = { + "rm", "-rf", path.c_str(), - NULL + nullptr }; - verbosestream<<"Executing '"<(argv.data())); - execv(argv[0], const_cast(argv)); - - // Execv shouldn't return. Failed. + // note: use cerr because our logging won't flush in forked process + std::cerr << "exec errno: " << errno << ": " << strerror(errno) + << std::endl; _exit(1); - } - else - { + } else { // Parent - int child_status; + int status; pid_t tpid; - do{ - tpid = wait(&child_status); - }while(tpid != child_pid); - return (child_status == 0); + do + tpid = waitpid(child_pid, &status, 0); + while (tpid != child_pid); + return WIFEXITED(status) && WEXITSTATUS(status) == 0; } } diff --git a/src/gamedef.h b/src/gamedef.h index 9a6c55ab1..f8d6d79e7 100644 --- a/src/gamedef.h +++ b/src/gamedef.h @@ -34,19 +34,19 @@ class Camera; class ModChannel; class ModStorage; class ModStorageDatabase; +struct SubgameSpec; +struct ModSpec; +struct ModIPCStore; namespace irr::scene { class IAnimatedMesh; class ISceneManager; } -struct SubgameSpec; -struct ModSpec; /* An interface for fetching game-global definitions like tool and mapnode properties */ - class IGameDef { public: @@ -63,6 +63,9 @@ public: // environment thread. virtual IRollbackManager* getRollbackManager() { return NULL; } + // Only usable on server. + virtual ModIPCStore *getModIPCStore() { return nullptr; } + // Shorthands // TODO: these should be made const-safe so that a const IGameDef* is // actually usable diff --git a/src/gui/guiEngine.cpp b/src/gui/guiEngine.cpp index 4a3d53f51..8a4e22b1d 100644 --- a/src/gui/guiEngine.cpp +++ b/src/gui/guiEngine.cpp @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "client/renderingengine.h" #include "client/shader.h" #include "client/tile.h" +#include "clientdynamicinfo.h" #include "config.h" #include "content/content.h" #include "content/mods.h" @@ -316,6 +317,7 @@ void GUIEngine::run() ); const bool initial_window_maximized = !g_settings->getBool("fullscreen") && g_settings->getBool("window_maximized"); + auto last_window_info = ClientDynamicInfo::getCurrent(); FpsControl fps_control; f32 dtime = 0.0f; @@ -335,6 +337,11 @@ void GUIEngine::run() updateTopLeftTextSize(); text_height = g_fontengine->getTextHeight(); } + auto window_info = ClientDynamicInfo::getCurrent(); + if (!window_info.equal(last_window_info)) { + m_script->handleMainMenuEvent("WindowInfoChange"); + last_window_info = window_info; + } driver->beginScene(true, true, RenderingEngine::MENU_SKY_COLOR); diff --git a/src/gui/guiFormSpecMenu.cpp b/src/gui/guiFormSpecMenu.cpp index 40a445a0c..efd7b7e8c 100644 --- a/src/gui/guiFormSpecMenu.cpp +++ b/src/gui/guiFormSpecMenu.cpp @@ -356,7 +356,7 @@ void GUIFormSpecMenu::parseContainerEnd(parserData* data, const std::string &) void GUIFormSpecMenu::parseScrollContainer(parserData *data, const std::string &element) { std::vector parts; - if (!precheckElement("scroll_container start", element, 4, 5, parts)) + if (!precheckElement("scroll_container start", element, 4, 6, parts)) return; std::vector v_pos = split(parts[0], ','); @@ -367,6 +367,12 @@ void GUIFormSpecMenu::parseScrollContainer(parserData *data, const std::string & if (parts.size() >= 5 && !parts[4].empty()) scroll_factor = stof(parts[4]); + std::optional content_padding_px; + if (parts.size() >= 6 && !parts[5].empty()) { + std::vector v_size = { parts[5], parts[5] }; + content_padding_px = getRealCoordinateGeometry(v_size)[orientation == "vertical" ? 1 : 0]; + } + MY_CHECKPOS("scroll_container", 0); MY_CHECKGEOM("scroll_container", 1); @@ -405,6 +411,7 @@ void GUIFormSpecMenu::parseScrollContainer(parserData *data, const std::string & GUIScrollContainer *mover = new GUIScrollContainer(Environment, clipper, spec_mover.fid, rect_mover, orientation, scroll_factor); + mover->setContentPadding(content_padding_px); data->current_parent = mover; @@ -3608,7 +3615,7 @@ void GUIFormSpecMenu::showTooltip(const std::wstring &text, int tooltip_offset_x = m_btn_height; int tooltip_offset_y = m_btn_height; - if (m_pointer_type == PointerType::Touch) { + if (RenderingEngine::getLastPointerType() == PointerType::Touch) { tooltip_offset_x *= 3; tooltip_offset_y = 0; if (m_pointer.X > (s32)screenSize.X / 2) diff --git a/src/gui/guiHyperText.cpp b/src/gui/guiHyperText.cpp index 6f30ac8ce..44019ebe2 100644 --- a/src/gui/guiHyperText.cpp +++ b/src/gui/guiHyperText.cpp @@ -1146,7 +1146,7 @@ bool GUIHyperText::OnEvent(const SEvent &event) } } - break; + return true; } } } diff --git a/src/gui/guiInventoryList.cpp b/src/gui/guiInventoryList.cpp index e5ed6e6ef..1dd36bfc9 100644 --- a/src/gui/guiInventoryList.cpp +++ b/src/gui/guiInventoryList.cpp @@ -21,6 +21,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "guiFormSpecMenu.h" #include "client/hud.h" #include "client/client.h" +#include "client/renderingengine.h" #include GUIInventoryList::GUIInventoryList(gui::IGUIEnvironment *env, @@ -154,7 +155,7 @@ void GUIInventoryList::draw() // Add hovering tooltip bool show_tooltip = !item.empty() && hovering && !selected_item; // Make it possible to see item tooltips on touchscreens - if (m_fs_menu->getPointerType() == PointerType::Touch) { + if (RenderingEngine::getLastPointerType() == PointerType::Touch) { show_tooltip |= hovering && selected && m_fs_menu->getSelectedAmount() != 0; } if (show_tooltip) { diff --git a/src/gui/guiScene.cpp b/src/gui/guiScene.cpp index 9293ebe22..06784cd6e 100644 --- a/src/gui/guiScene.cpp +++ b/src/gui/guiScene.cpp @@ -157,7 +157,7 @@ void GUIScene::setStyles(const std::array &sty /** * Sets the frame loop range for the mesh */ -void GUIScene::setFrameLoop(s32 begin, s32 end) +void GUIScene::setFrameLoop(f32 begin, f32 end) { if (m_mesh->getStartFrame() != begin || m_mesh->getEndFrame() != end) m_mesh->setFrameLoop(begin, end); @@ -225,8 +225,7 @@ void GUIScene::setCameraRotation(v3f rot) core::matrix4 mat; mat.setRotationDegrees(rot); - m_cam_pos = v3f(0.f, 0.f, m_cam_distance); - mat.rotateVect(m_cam_pos); + m_cam_pos = mat.rotateAndScaleVect(v3f(0.f, 0.f, m_cam_distance)); m_cam_pos += m_target_pos; m_cam->setPosition(m_cam_pos); diff --git a/src/gui/guiScene.h b/src/gui/guiScene.h index 0f5f3a891..0634669f7 100644 --- a/src/gui/guiScene.h +++ b/src/gui/guiScene.h @@ -36,7 +36,7 @@ public: scene::IAnimatedMeshSceneNode *setMesh(scene::IAnimatedMesh *mesh = nullptr); void setTexture(u32 idx, video::ITexture *texture); void setBackgroundColor(const video::SColor &color) noexcept { m_bgcolor = color; }; - void setFrameLoop(s32 begin, s32 end); + void setFrameLoop(f32 begin, f32 end); void setAnimationSpeed(f32 speed); void enableMouseControl(bool enable) noexcept { m_mouse_ctrl = enable; }; void setRotation(v2f rot) noexcept { m_custom_rot = rot; }; diff --git a/src/gui/guiScrollBar.h b/src/gui/guiScrollBar.h index 05e195aed..af3bc4652 100644 --- a/src/gui/guiScrollBar.h +++ b/src/gui/guiScrollBar.h @@ -45,6 +45,7 @@ public: s32 getSmallStep() const { return small_step; } s32 getPos() const; s32 getTargetPos() const; + bool isHorizontal() const { return is_horizontal; } void setMax(const s32 &max); void setMin(const s32 &min); diff --git a/src/gui/guiScrollContainer.cpp b/src/gui/guiScrollContainer.cpp index 2d71f3453..13ba5c35f 100644 --- a/src/gui/guiScrollContainer.cpp +++ b/src/gui/guiScrollContainer.cpp @@ -67,6 +67,50 @@ void GUIScrollContainer::draw() } } +void GUIScrollContainer::setScrollBar(GUIScrollBar *scrollbar) +{ + m_scrollbar = scrollbar; + + if (m_scrollbar && m_content_padding_px.has_value() && m_scrollfactor != 0.0f) { + // Set the scrollbar max value based on the content size. + + // Get content size based on elements + core::rect size; + for (gui::IGUIElement *e : Children) { + core::rect abs_rect = e->getAbsolutePosition(); + size.addInternalPoint(abs_rect.LowerRightCorner); + } + + s32 visible_content_px = ( + m_orientation == VERTICAL + ? AbsoluteClippingRect.getHeight() + : AbsoluteClippingRect.getWidth() + ); + + s32 total_content_px = *m_content_padding_px + ( + m_orientation == VERTICAL + ? (size.LowerRightCorner.Y - AbsoluteClippingRect.UpperLeftCorner.Y) + : (size.LowerRightCorner.X - AbsoluteClippingRect.UpperLeftCorner.X) + ); + + s32 hidden_content_px = std::max(0, total_content_px - visible_content_px); + m_scrollbar->setMin(0); + m_scrollbar->setMax(std::ceil(hidden_content_px / std::fabs(m_scrollfactor))); + + // Note: generally, the scrollbar has the same size as the scroll container. + // However, in case it isn't, proportional adjustments are needed. + s32 scrollbar_px = ( + m_scrollbar->isHorizontal() + ? m_scrollbar->getRelativePosition().getWidth() + : m_scrollbar->getRelativePosition().getHeight() + ); + + m_scrollbar->setPageSize((total_content_px * scrollbar_px) / visible_content_px); + } + + updateScrolling(); +} + void GUIScrollContainer::updateScrolling() { s32 pos = m_scrollbar->getPos(); diff --git a/src/gui/guiScrollContainer.h b/src/gui/guiScrollContainer.h index 9e3ec6e93..d6871a53e 100644 --- a/src/gui/guiScrollContainer.h +++ b/src/gui/guiScrollContainer.h @@ -34,17 +34,18 @@ public: virtual void draw() override; + inline void setContentPadding(std::optional padding) + { + m_content_padding_px = padding; + } + inline void onScrollEvent(gui::IGUIElement *caller) { if (caller == m_scrollbar) updateScrolling(); } - inline void setScrollBar(GUIScrollBar *scrollbar) - { - m_scrollbar = scrollbar; - updateScrolling(); - } + void setScrollBar(GUIScrollBar *scrollbar); private: enum OrientationEnum @@ -56,7 +57,8 @@ private: GUIScrollBar *m_scrollbar; OrientationEnum m_orientation; - f32 m_scrollfactor; + f32 m_scrollfactor; //< scrollbar pos * scrollfactor = scroll offset in pixels + std::optional m_content_padding_px; //< in pixels void updateScrolling(); }; diff --git a/src/gui/modalMenu.cpp b/src/gui/modalMenu.cpp index fd60d08c2..ad4839170 100644 --- a/src/gui/modalMenu.cpp +++ b/src/gui/modalMenu.cpp @@ -187,6 +187,7 @@ bool GUIModalMenu::simulateMouseEvent(ETOUCH_INPUT_EVENT touch_event, bool secon mouse_event.EventType = EET_MOUSE_INPUT_EVENT; mouse_event.MouseInput.X = m_pointer.X; mouse_event.MouseInput.Y = m_pointer.Y; + mouse_event.MouseInput.Simulated = true; switch (touch_event) { case ETIE_PRESSED_DOWN: mouse_event.MouseInput.Event = EMIE_LMOUSE_PRESSED_DOWN; @@ -210,7 +211,6 @@ bool GUIModalMenu::simulateMouseEvent(ETOUCH_INPUT_EVENT touch_event, bool secon } bool retval; - m_simulated_mouse = true; do { if (preprocessEvent(mouse_event)) { retval = true; @@ -222,7 +222,6 @@ bool GUIModalMenu::simulateMouseEvent(ETOUCH_INPUT_EVENT touch_event, bool secon } retval = target->OnEvent(mouse_event); } while (false); - m_simulated_mouse = false; if (!retval && !second_try) return simulateMouseEvent(touch_event, true); @@ -330,7 +329,6 @@ bool GUIModalMenu::preprocessEvent(const SEvent &event) holder.grab(this); // keep this alive until return (it might be dropped downstream [?]) if (event.TouchInput.touchedCount == 1) { - m_pointer_type = PointerType::Touch; m_pointer = v2s32(event.TouchInput.X, event.TouchInput.Y); gui::IGUIElement *hovered = Environment->getRootGUIElement()->getElementFromPoint(core::position2d(m_pointer)); @@ -373,9 +371,8 @@ bool GUIModalMenu::preprocessEvent(const SEvent &event) } if (event.EventType == EET_MOUSE_INPUT_EVENT) { - if (!m_simulated_mouse) { - // Only set the pointer type to mouse if this is a real mouse event. - m_pointer_type = PointerType::Mouse; + if (!event.MouseInput.Simulated) { + // Only process if this is a real mouse event. m_pointer = v2s32(event.MouseInput.X, event.MouseInput.Y); m_touch_hovered.reset(); } diff --git a/src/gui/modalMenu.h b/src/gui/modalMenu.h index 071024120..2f770f9f5 100644 --- a/src/gui/modalMenu.h +++ b/src/gui/modalMenu.h @@ -26,11 +26,6 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #endif -enum class PointerType { - Mouse, - Touch, -}; - struct PointerAction { v2s32 pos; u64 time; // ms @@ -74,14 +69,10 @@ public: porting::AndroidDialogState getAndroidUIInputState(); #endif - PointerType getPointerType() { return m_pointer_type; }; - protected: virtual std::wstring getLabelByID(s32 id) = 0; virtual std::string getNameByID(s32 id) = 0; - // Stores the last known pointer type. - PointerType m_pointer_type = PointerType::Mouse; // Stores the last known pointer position. // If the last input event was a mouse event, it's the cursor position. // If the last input event was a touch event, it's the finger position. @@ -102,9 +93,6 @@ protected: // This is set to true if the menu is currently processing a second-touch event. bool m_second_touch = false; - // This is set to true if the menu is currently processing a mouse event - // that was synthesized by the menu itself from a touch event. - bool m_simulated_mouse = false; private: IMenuManager *m_menumgr; diff --git a/src/gui/touchcontrols.cpp b/src/gui/touchcontrols.cpp index 4a673ccf3..f3301a64d 100644 --- a/src/gui/touchcontrols.cpp +++ b/src/gui/touchcontrols.cpp @@ -418,6 +418,11 @@ TouchControls::TouchControls(IrrlichtDevice *device, ISimpleTextureSource *tsrc) m_status_text->setVisible(false); } +TouchControls::~TouchControls() +{ + releaseAll(); +} + void TouchControls::addButton(std::vector &buttons, touch_gui_button_id id, const std::string &image, const recti &rect, bool visible) { @@ -843,6 +848,7 @@ void TouchControls::emitMouseEvent(EMOUSE_INPUT_EVENT type) event.MouseInput.Control = false; event.MouseInput.ButtonStates = 0; event.MouseInput.Event = type; + event.MouseInput.Simulated = true; m_receiver->OnEvent(event); } diff --git a/src/gui/touchcontrols.h b/src/gui/touchcontrols.h index 102c85f09..98ec806bd 100644 --- a/src/gui/touchcontrols.h +++ b/src/gui/touchcontrols.h @@ -33,6 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "itemdef.h" #include "client/game.h" +#include "util/basic_macros.h" namespace irr { @@ -136,6 +137,8 @@ class TouchControls { public: TouchControls(IrrlichtDevice *device, ISimpleTextureSource *tsrc); + ~TouchControls(); + DISABLE_CLASS_COPY(TouchControls); void translateEvent(const SEvent &event); void applyContextControls(const TouchInteractionMode &mode); @@ -163,8 +166,8 @@ public: */ line3d getShootline() { return m_shootline; } - float getMovementDirection() { return m_joystick_direction; } - float getMovementSpeed() { return m_joystick_speed; } + float getJoystickDirection() { return m_joystick_direction; } + float getJoystickSpeed() { return m_joystick_speed; } void step(float dtime); inline void setUseCrosshair(bool use_crosshair) { m_draw_crosshair = use_crosshair; } diff --git a/src/itemstackmetadata.cpp b/src/itemstackmetadata.cpp index be1715e1a..a2fc67c46 100644 --- a/src/itemstackmetadata.cpp +++ b/src/itemstackmetadata.cpp @@ -89,11 +89,11 @@ void ItemStackMetadata::deSerialize(std::istream &is) while (!fnd.at_end()) { std::string name = fnd.next(DESERIALIZE_KV_DELIM_STR); std::string var = fnd.next(DESERIALIZE_PAIR_DELIM_STR); - m_stringvars[name] = var; + m_stringvars[name] = std::move(var); } } else { // BACKWARDS COMPATIBILITY - m_stringvars[""] = in; + m_stringvars[""] = std::move(in); } } updateToolCapabilities(); diff --git a/src/lighting.h b/src/lighting.h index f6028ade2..ff9ba091d 100644 --- a/src/lighting.h +++ b/src/lighting.h @@ -57,5 +57,8 @@ struct Lighting float saturation {1.0f}; float volumetric_light_strength {0.0f}; video::SColor artificial_light_color{ 255, 133, 133, 133 }; - video::SColor shadow_tint; + video::SColor shadow_tint {255, 0, 0, 0}; + float bloom_intensity {0.05f}; + float bloom_strength_factor {1.0f}; + float bloom_radius {1.0f}; }; diff --git a/src/log.cpp b/src/log.cpp index f7eb691ac..5fac64f5c 100644 --- a/src/log.cpp +++ b/src/log.cpp @@ -17,7 +17,7 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "log.h" +#include "log_internal.h" #include "threading/mutex_auto_lock.h" #include "debug.h" @@ -27,7 +27,6 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "config.h" #include "exceptions.h" #include "util/numeric.h" -#include "log.h" #include "filesys.h" #ifdef __ANDROID__ diff --git a/src/log.h b/src/log.h index 721ce58ed..ccd13acf3 100644 --- a/src/log.h +++ b/src/log.h @@ -1,198 +1,9 @@ -/* -Minetest -Copyright (C) 2013 celeron55, Perttu Ahola - -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. -*/ +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once -#include -#include -#include -#include -#include -#include -#include -#include "threading/mutex_auto_lock.h" #include "util/basic_macros.h" #include "util/stream.h" -#include "irrlichttypes.h" - -class ILogOutput; - -enum LogLevel { - LL_NONE, // Special level that is always printed - LL_ERROR, - LL_WARNING, - LL_ACTION, // In-game actions - LL_INFO, - LL_VERBOSE, - LL_TRACE, - LL_MAX, -}; - -enum LogColor { - LOG_COLOR_NEVER, - LOG_COLOR_ALWAYS, - LOG_COLOR_AUTO, -}; - -typedef u8 LogLevelMask; -#define LOGLEVEL_TO_MASKLEVEL(x) (1 << x) - -class Logger { -public: - void addOutput(ILogOutput *out); - void addOutput(ILogOutput *out, LogLevel lev); - void addOutputMasked(ILogOutput *out, LogLevelMask mask); - void addOutputMaxLevel(ILogOutput *out, LogLevel lev); - LogLevelMask removeOutput(ILogOutput *out); - void setLevelSilenced(LogLevel lev, bool silenced); - - void registerThread(std::string_view name); - void deregisterThread(); - - void log(LogLevel lev, std::string_view text); - // Logs without a prefix - void logRaw(LogLevel lev, std::string_view text); - - static LogLevel stringToLevel(std::string_view name); - static const char *getLevelLabel(LogLevel lev); - - bool hasOutput(LogLevel level) { - return m_has_outputs[level].load(std::memory_order_relaxed); - } - - bool isLevelSilenced(LogLevel level) { - return m_silenced_levels[level].load(std::memory_order_relaxed); - } - - static LogColor color_mode; - -private: - void logToOutputsRaw(LogLevel, std::string_view line); - void logToOutputs(LogLevel, const std::string &combined, - const std::string &time, const std::string &thread_name, - std::string_view payload_text); - - const std::string &getThreadName(); - - std::vector m_outputs[LL_MAX]; - std::atomic m_has_outputs[LL_MAX]; - std::atomic m_silenced_levels[LL_MAX]; - std::map m_thread_names; - mutable std::mutex m_mutex; -}; - -class ILogOutput { -public: - virtual void logRaw(LogLevel, std::string_view line) = 0; - virtual void log(LogLevel, const std::string &combined, - const std::string &time, const std::string &thread_name, - std::string_view payload_text) = 0; -}; - -class ICombinedLogOutput : public ILogOutput { -public: - void log(LogLevel lev, const std::string &combined, - const std::string &time, const std::string &thread_name, - std::string_view payload_text) - { - logRaw(lev, combined); - } -}; - -class StreamLogOutput : public ICombinedLogOutput { -public: - StreamLogOutput(std::ostream &stream); - - void logRaw(LogLevel lev, std::string_view line); - -private: - std::ostream &m_stream; - bool is_tty = false; -}; - -class FileLogOutput : public ICombinedLogOutput { -public: - void setFile(const std::string &filename, s64 file_size_max); - - void logRaw(LogLevel lev, std::string_view line) - { - m_stream << line << std::endl; - } - -private: - std::ofstream m_stream; -}; - -class LogOutputBuffer : public ICombinedLogOutput { -public: - LogOutputBuffer(Logger &logger) : - m_logger(logger) - { - updateLogLevel(); - }; - - virtual ~LogOutputBuffer() - { - m_logger.removeOutput(this); - } - - void updateLogLevel(); - - void logRaw(LogLevel lev, std::string_view line); - - void clear() - { - MutexAutoLock lock(m_buffer_mutex); - m_buffer = std::queue(); - } - - bool empty() const - { - MutexAutoLock lock(m_buffer_mutex); - return m_buffer.empty(); - } - - std::string get() - { - MutexAutoLock lock(m_buffer_mutex); - if (m_buffer.empty()) - return ""; - std::string s = std::move(m_buffer.front()); - m_buffer.pop(); - return s; - } - -private: - // g_logger serializes calls to logRaw() with a mutex, but that - // doesn't prevent get() / clear() from being called on top of it. - // This mutex prevents that. - mutable std::mutex m_buffer_mutex; - std::queue m_buffer; - Logger &m_logger; -}; - -#ifdef __ANDROID__ -class AndroidLogOutput : public ICombinedLogOutput { -public: - void logRaw(LogLevel lev, std::string_view line); -}; -#endif /* * LogTarget @@ -325,16 +136,6 @@ private: }; -#ifdef __ANDROID__ -extern AndroidLogOutput stdout_output; -extern AndroidLogOutput stderr_output; -#else -extern StreamLogOutput stdout_output; -extern StreamLogOutput stderr_output; -#endif - -extern Logger g_logger; - /* * By making the streams thread_local, each thread has its own * private buffer. Two or more threads can write to the same stream diff --git a/src/log_internal.h b/src/log_internal.h new file mode 100644 index 000000000..c8bc1b310 --- /dev/null +++ b/src/log_internal.h @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "threading/mutex_auto_lock.h" +#include "util/basic_macros.h" +#include "util/stream.h" +#include "irrlichttypes.h" +#include "log.h" + +class ILogOutput; + +enum LogLevel { + LL_NONE, // Special level that is always printed + LL_ERROR, + LL_WARNING, + LL_ACTION, // In-game actions + LL_INFO, + LL_VERBOSE, + LL_TRACE, + LL_MAX, +}; + +enum LogColor { + LOG_COLOR_NEVER, + LOG_COLOR_ALWAYS, + LOG_COLOR_AUTO, +}; + +typedef u8 LogLevelMask; +#define LOGLEVEL_TO_MASKLEVEL(x) (1 << x) + +class Logger { +public: + void addOutput(ILogOutput *out); + void addOutput(ILogOutput *out, LogLevel lev); + void addOutputMasked(ILogOutput *out, LogLevelMask mask); + void addOutputMaxLevel(ILogOutput *out, LogLevel lev); + LogLevelMask removeOutput(ILogOutput *out); + void setLevelSilenced(LogLevel lev, bool silenced); + + void registerThread(std::string_view name); + void deregisterThread(); + + void log(LogLevel lev, std::string_view text); + // Logs without a prefix + void logRaw(LogLevel lev, std::string_view text); + + static LogLevel stringToLevel(std::string_view name); + static const char *getLevelLabel(LogLevel lev); + + bool hasOutput(LogLevel level) { + return m_has_outputs[level].load(std::memory_order_relaxed); + } + + bool isLevelSilenced(LogLevel level) { + return m_silenced_levels[level].load(std::memory_order_relaxed); + } + + static LogColor color_mode; + +private: + void logToOutputsRaw(LogLevel, std::string_view line); + void logToOutputs(LogLevel, const std::string &combined, + const std::string &time, const std::string &thread_name, + std::string_view payload_text); + + const std::string &getThreadName(); + + std::vector m_outputs[LL_MAX]; + std::atomic m_has_outputs[LL_MAX]; + std::atomic m_silenced_levels[LL_MAX]; + std::map m_thread_names; + mutable std::mutex m_mutex; +}; + +class ILogOutput { +public: + virtual void logRaw(LogLevel, std::string_view line) = 0; + virtual void log(LogLevel, const std::string &combined, + const std::string &time, const std::string &thread_name, + std::string_view payload_text) = 0; +}; + +class ICombinedLogOutput : public ILogOutput { +public: + void log(LogLevel lev, const std::string &combined, + const std::string &time, const std::string &thread_name, + std::string_view payload_text) + { + logRaw(lev, combined); + } +}; + +class StreamLogOutput : public ICombinedLogOutput { +public: + StreamLogOutput(std::ostream &stream); + + void logRaw(LogLevel lev, std::string_view line); + +private: + std::ostream &m_stream; + bool is_tty = false; +}; + +class FileLogOutput : public ICombinedLogOutput { +public: + void setFile(const std::string &filename, s64 file_size_max); + + void logRaw(LogLevel lev, std::string_view line) + { + m_stream << line << std::endl; + } + +private: + std::ofstream m_stream; +}; + +class LogOutputBuffer : public ICombinedLogOutput { +public: + LogOutputBuffer(Logger &logger) : + m_logger(logger) + { + updateLogLevel(); + }; + + virtual ~LogOutputBuffer() + { + m_logger.removeOutput(this); + } + + void updateLogLevel(); + + void logRaw(LogLevel lev, std::string_view line); + + void clear() + { + MutexAutoLock lock(m_buffer_mutex); + m_buffer = std::queue(); + } + + bool empty() const + { + MutexAutoLock lock(m_buffer_mutex); + return m_buffer.empty(); + } + + std::string get() + { + MutexAutoLock lock(m_buffer_mutex); + if (m_buffer.empty()) + return ""; + std::string s = std::move(m_buffer.front()); + m_buffer.pop(); + return s; + } + +private: + // g_logger serializes calls to logRaw() with a mutex, but that + // doesn't prevent get() / clear() from being called on top of it. + // This mutex prevents that. + mutable std::mutex m_buffer_mutex; + std::queue m_buffer; + Logger &m_logger; +}; + +#ifdef __ANDROID__ +class AndroidLogOutput : public ICombinedLogOutput { +public: + void logRaw(LogLevel lev, std::string_view line); +}; +#endif + +#ifdef __ANDROID__ +extern AndroidLogOutput stdout_output; +extern AndroidLogOutput stderr_output; +#else +extern StreamLogOutput stdout_output; +extern StreamLogOutput stderr_output; +#endif + +extern Logger g_logger; diff --git a/src/main.cpp b/src/main.cpp index 30db81aa9..803f3c6b0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -31,6 +31,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "migratesettings.h" #include "gettext.h" #include "log.h" +#include "log_internal.h" #include "util/quicktune.h" #include "httpfetch.h" #include "gameparams.h" @@ -728,7 +729,7 @@ static void startup_message() print_version(infostream); infostream << "SER_FMT_VER_HIGHEST_READ=" << TOSTRING(SER_FMT_VER_HIGHEST_READ) << - " LATEST_PROTOCOL_VERSION=" << TOSTRING(LATEST_PROTOCOL_VERSION) + " LATEST_PROTOCOL_VERSION=" << LATEST_PROTOCOL_VERSION << std::endl; } @@ -1278,8 +1279,7 @@ static bool recompress_map_database(const GameParams &game_params, const Setting { MapBlock mb(v3s16(0,0,0), &server); - u8 ver = readU8(iss); - mb.deSerialize(iss, ver, true); + ServerMap::deSerializeBlock(&mb, iss); oss.str(""); oss.clear(); diff --git a/src/migratesettings.h b/src/migratesettings.h index d4488702f..5f6396914 100644 --- a/src/migratesettings.h +++ b/src/migratesettings.h @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "settings.h" +#include "server.h" void migrate_settings() { @@ -19,4 +20,12 @@ void migrate_settings() g_settings->setBool("touch_gui", value); g_settings->remove("enable_touch"); } + + // Disables anticheat + if (g_settings->existsLocal("disable_anticheat")) { + if (g_settings->getBool("disable_anticheat")) { + g_settings->setFlagStr("anticheat_flags", 0, flagdesc_anticheat); + } + g_settings->remove("disable_anticheat"); + } } diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt index 8f17e58af..6291e23af 100644 --- a/src/network/CMakeLists.txt +++ b/src/network/CMakeLists.txt @@ -4,6 +4,7 @@ set(common_network_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/mtp/impl.cpp ${CMAKE_CURRENT_SOURCE_DIR}/mtp/threads.cpp ${CMAKE_CURRENT_SOURCE_DIR}/networkpacket.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/networkprotocol.cpp ${CMAKE_CURRENT_SOURCE_DIR}/serveropcodes.cpp ${CMAKE_CURRENT_SOURCE_DIR}/serverpackethandler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/socket.cpp diff --git a/src/network/clientpackethandler.cpp b/src/network/clientpackethandler.cpp index 40d11f73c..346effe08 100644 --- a/src/network/clientpackethandler.cpp +++ b/src/network/clientpackethandler.cpp @@ -19,6 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "client/client.h" +#include "irr_v2d.h" #include "util/base64.h" #include "client/camera.h" #include "client/mesh_generator_thread.h" @@ -1516,11 +1517,15 @@ void Client::handleCommand_LocalPlayerAnimations(NetworkPacket* pkt) LocalPlayer *player = m_env.getLocalPlayer(); assert(player != NULL); - *pkt >> player->local_animations[0]; - *pkt >> player->local_animations[1]; - *pkt >> player->local_animations[2]; - *pkt >> player->local_animations[3]; - *pkt >> player->local_animation_speed; + for (int i = 0; i < 4; ++i) { + if (getProtoVersion() >= 46) { + *pkt >> player->local_animations[i]; + } else { + v2s32 local_animation; + *pkt >> local_animation; + player->local_animations[i] = v2f::from(local_animation); + } + } player->last_animation = LocalPlayerAnimation::NO_ANIM; } @@ -1819,6 +1824,11 @@ void Client::handleCommand_SetLighting(NetworkPacket *pkt) *pkt >> lighting.volumetric_light_strength; if (pkt->getRemainingBytes() >= 4) *pkt >> lighting.shadow_tint; + if (pkt->getRemainingBytes() >= 12) { + *pkt >> lighting.bloom_intensity + >> lighting.bloom_strength_factor + >> lighting.bloom_radius; if (pkt->getRemainingBytes() >= 4) *pkt >> lighting.artificial_light_color; + } } diff --git a/src/network/networkpacket.h b/src/network/networkpacket.h index ee85b2951..0260f8072 100644 --- a/src/network/networkpacket.h +++ b/src/network/networkpacket.h @@ -19,7 +19,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once -#include "util/pointer.h" +#include "util/pointer.h" // Buffer +#include "irrlichttypes_bloated.h" #include "networkprotocol.h" #include diff --git a/src/network/networkprotocol.cpp b/src/network/networkprotocol.cpp new file mode 100644 index 000000000..40f8acef1 --- /dev/null +++ b/src/network/networkprotocol.cpp @@ -0,0 +1,67 @@ +// Minetest +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "networkprotocol.h" + + +/* + PROTOCOL VERSION < 37: + Until (and including) version 0.4.17.1 + PROTOCOL VERSION 37: + Redo detached inventory sending + Add TOCLIENT_NODEMETA_CHANGED + New network float format + ContentFeatures version 13 + Add full Euler rotations instead of just yaw + Add TOCLIENT_PLAYER_SPEED + [bump for 5.0.0] + PROTOCOL VERSION 38: + Incremental inventory sending mode + Unknown inventory serialization fields no longer throw an error + Mod-specific formspec version + Player FOV override API + "ephemeral" added to TOCLIENT_PLAY_SOUND + PROTOCOL VERSION 39: + Updated set_sky packet + Adds new sun, moon and stars packets + Minimap modes + PROTOCOL VERSION 40: + TOCLIENT_MEDIA_PUSH changed, TOSERVER_HAVE_MEDIA added + PROTOCOL VERSION 41: + Added new particlespawner parameters + [scheduled bump for 5.6.0] + PROTOCOL VERSION 42: + TOSERVER_UPDATE_CLIENT_INFO added + new fields for TOCLIENT_SET_LIGHTING and TOCLIENT_SET_SKY + Send forgotten TweenedParameter properties + [scheduled bump for 5.7.0] + PROTOCOL VERSION 43: + "start_time" added to TOCLIENT_PLAY_SOUND + place_param2 type change u8 -> optional + [scheduled bump for 5.8.0] + PROTOCOL VERSION 44: + AO_CMD_SET_BONE_POSITION extended + Add TOCLIENT_MOVE_PLAYER_REL + Move default minimap from client-side C++ to server-side builtin Lua + [scheduled bump for 5.9.0] + PROTOCOL VERSION 45: + Minimap HUD element supports negative size values as percentages + [bump for 5.9.1] + PROTOCOL VERSION 46: + Move default hotbar from client-side C++ to server-side builtin Lua + Add shadow tint to Lighting packets + Add shadow color to CloudParam packets + Move death screen to server and make it a regular formspec + The server no longer triggers the hardcoded client-side death + formspec, but the client still supports it for compatibility with + old servers. + Rename TOCLIENT_DEATHSCREEN to TOCLIENT_DEATHSCREEN_LEGACY + Rename TOSERVER_RESPAWN to TOSERVER_RESPAWN_LEGACY + Support float animation frame numbers in TOCLIENT_LOCAL_PLAYER_ANIMATIONS + [scheduled bump for 5.10.0] +*/ + +const u16 LATEST_PROTOCOL_VERSION = 46; + +// See also formspec [Version History] in doc/lua_api.md +const u16 FORMSPEC_API_VERSION = 8; diff --git a/src/network/networkprotocol.h b/src/network/networkprotocol.h index 0bbacd584..4ee02209a 100644 --- a/src/network/networkprotocol.h +++ b/src/network/networkprotocol.h @@ -19,243 +19,18 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once -#include "util/string.h" +#include "irrTypes.h" +using namespace irr; -/* - changes by PROTOCOL_VERSION: - - PROTOCOL_VERSION 3: - Base for writing changes here - PROTOCOL_VERSION 4: - Add TOCLIENT_MEDIA - Add TOCLIENT_TOOLDEF - Add TOCLIENT_NODEDEF - Add TOCLIENT_CRAFTITEMDEF - Add TOSERVER_INTERACT - Obsolete TOSERVER_CLICK_ACTIVEOBJECT - Obsolete TOSERVER_GROUND_ACTION - PROTOCOL_VERSION 5: - Make players to be handled mostly as ActiveObjects - PROTOCOL_VERSION 6: - Only non-cached textures are sent - PROTOCOL_VERSION 7: - Add TOCLIENT_ITEMDEF - Obsolete TOCLIENT_TOOLDEF - Obsolete TOCLIENT_CRAFTITEMDEF - Compress the contents of TOCLIENT_ITEMDEF and TOCLIENT_NODEDEF - PROTOCOL_VERSION 8: - Digging based on item groups - Many things - PROTOCOL_VERSION 9: - ContentFeatures and NodeDefManager use a different serialization - format; better for future version cross-compatibility - Many things - Obsolete TOCLIENT_PLAYERITEM - PROTOCOL_VERSION 10: - TOCLIENT_PRIVILEGES - Version raised to force 'fly' and 'fast' privileges into effect. - Node metadata change (came in later; somewhat incompatible) - PROTOCOL_VERSION 11: - TileDef in ContentFeatures - Nodebox drawtype - (some dev snapshot) - TOCLIENT_INVENTORY_FORMSPEC - (0.4.0, 0.4.1) - PROTOCOL_VERSION 12: - TOSERVER_INVENTORY_FIELDS - 16-bit node ids - TOCLIENT_DETACHED_INVENTORY - PROTOCOL_VERSION 13: - InventoryList field "Width" (deserialization fails with old versions) - PROTOCOL_VERSION 14: - Added transfer of player pressed keys to the server - Added new messages for mesh and bone animation, as well as attachments - AO_CMD_SET_ANIMATION - AO_CMD_SET_BONE_POSITION - GENERIC_CMD_SET_ATTACHMENT - PROTOCOL_VERSION 15: - Serialization format changes - PROTOCOL_VERSION 16: - TOCLIENT_SHOW_FORMSPEC - PROTOCOL_VERSION 17: - Serialization format change: include backface_culling flag in TileDef - Added rightclickable field in nodedef - TOCLIENT_SPAWN_PARTICLE - TOCLIENT_ADD_PARTICLESPAWNER - TOCLIENT_DELETE_PARTICLESPAWNER - PROTOCOL_VERSION 18: - damageGroups added to ToolCapabilities - sound_place added to ItemDefinition - PROTOCOL_VERSION 19: - AO_CMD_SET_PHYSICS_OVERRIDE - PROTOCOL_VERSION 20: - TOCLIENT_HUDADD - TOCLIENT_HUDRM - TOCLIENT_HUDCHANGE - TOCLIENT_HUD_SET_FLAGS - PROTOCOL_VERSION 21: - TOCLIENT_BREATH - TOSERVER_BREATH - range added to ItemDefinition - drowning, leveled and liquid_range added to ContentFeatures - stepheight and collideWithObjects added to object properties - version, heat and humidity transfer in MapBock - automatic_face_movement_dir and automatic_face_movement_dir_offset - added to object properties - PROTOCOL_VERSION 22: - add swap_node - PROTOCOL_VERSION 23: - Obsolete TOSERVER_RECEIVED_MEDIA - Server: Stop using TOSERVER_CLIENT_READY - PROTOCOL_VERSION 24: - ContentFeatures version 7 - ContentFeatures: change number of special tiles to 6 (CF_SPECIAL_COUNT) - PROTOCOL_VERSION 25: - Rename TOCLIENT_ACCESS_DENIED to TOCLIENT_ACCESS_DENIED_LEGAGY - Rename TOCLIENT_DELETE_PARTICLESPAWNER to - TOCLIENT_DELETE_PARTICLESPAWNER_LEGACY - Rename TOSERVER_PASSWORD to TOSERVER_PASSWORD_LEGACY - Rename TOSERVER_INIT to TOSERVER_INIT_LEGACY - Rename TOCLIENT_INIT to TOCLIENT_INIT_LEGACY - Add TOCLIENT_ACCESS_DENIED new opcode (0x0A), using error codes - for standard error, keeping customisation possible. This - permit translation - Add TOCLIENT_DELETE_PARTICLESPAWNER (0x53), fixing the u16 read and - reading u32 - Add new opcode TOSERVER_INIT for client presentation to server - Add new opcodes TOSERVER_FIRST_SRP, TOSERVER_SRP_BYTES_A, - TOSERVER_SRP_BYTES_M, TOCLIENT_SRP_BYTES_S_B - for the three supported auth mechanisms around srp - Add new opcodes TOCLIENT_ACCEPT_SUDO_MODE and TOCLIENT_DENY_SUDO_MODE - for sudo mode handling (auth mech generic way of changing password). - Add TOCLIENT_HELLO for presenting server to client after client - presentation - Add TOCLIENT_AUTH_ACCEPT to accept connection from client - Rename GENERIC_CMD_SET_ATTACHMENT to AO_CMD_ATTACH_TO - PROTOCOL_VERSION 26: - Add TileDef tileable_horizontal, tileable_vertical flags - PROTOCOL_VERSION 27: - backface_culling: backwards compatibility for playing with - newer client on pre-27 servers. - Add nodedef v3 - connected nodeboxes - PROTOCOL_VERSION 28: - CPT2_MESHOPTIONS - PROTOCOL_VERSION 29: - Server doesn't accept TOSERVER_BREATH anymore - serialization of TileAnimation params changed - TAT_SHEET_2D - Removed client-sided chat perdiction - PROTOCOL VERSION 30: - New ContentFeatures serialization version - Add node and tile color and palette - Fix plantlike visual_scale being applied squared and add compatibility - with pre-30 clients by sending sqrt(visual_scale) - PROTOCOL VERSION 31: - Add tile overlay - Stop sending TOSERVER_CLIENT_READY - PROTOCOL VERSION 32: - Add fading sounds - PROTOCOL VERSION 33: - Add TOCLIENT_UPDATE_PLAYER_LIST and send the player list to the client, - instead of guessing based on the active object list. - PROTOCOL VERSION 34: - Add sound pitch - PROTOCOL VERSION 35: - Rename TOCLIENT_CHAT_MESSAGE to TOCLIENT_CHAT_MESSAGE_OLD (0x30) - Add TOCLIENT_CHAT_MESSAGE (0x2F) - This chat message is a signalisation message containing various - informations: - * timestamp - * sender - * type (RAW, NORMAL, ANNOUNCE, SYSTEM) - * content - Add TOCLIENT_CSM_RESTRICTION_FLAGS to define which CSM features should be - limited - Add settable player collisionbox. Breaks compatibility with older - clients as a 1-node vertical offset has been removed from player's - position - Add settable player stepheight using existing object property. - Breaks compatibility with older clients. - PROTOCOL VERSION 36: - Backwards compatibility drop - Add 'can_zoom' to player object properties - Add glow to object properties - Change TileDef serialization format. - Add world-aligned tiles. - Mod channels - Raise ObjectProperties version to 3 for removing 'can_zoom' and adding - 'zoom_fov'. - Nodebox version 5 - Add disconnected nodeboxes - Add TOCLIENT_FORMSPEC_PREPEND - PROTOCOL VERSION 37: - Redo detached inventory sending - Add TOCLIENT_NODEMETA_CHANGED - New network float format - ContentFeatures version 13 - Add full Euler rotations instead of just yaw - Add TOCLIENT_PLAYER_SPEED - PROTOCOL VERSION 38: - Incremental inventory sending mode - Unknown inventory serialization fields no longer throw an error - Mod-specific formspec version - Player FOV override API - "ephemeral" added to TOCLIENT_PLAY_SOUND - PROTOCOL VERSION 39: - Updated set_sky packet - Adds new sun, moon and stars packets - Minimap modes - PROTOCOL VERSION 40: - TOCLIENT_MEDIA_PUSH changed, TOSERVER_HAVE_MEDIA added - PROTOCOL VERSION 41: - Added new particlespawner parameters - [scheduled bump for 5.6.0] - PROTOCOL VERSION 42: - TOSERVER_UPDATE_CLIENT_INFO added - new fields for TOCLIENT_SET_LIGHTING and TOCLIENT_SET_SKY - Send forgotten TweenedParameter properties - [scheduled bump for 5.7.0] - PROTOCOL VERSION 43: - "start_time" added to TOCLIENT_PLAY_SOUND - place_param2 type change u8 -> optional - [scheduled bump for 5.8.0] - PROTOCOL VERSION 44: - AO_CMD_SET_BONE_POSITION extended - Add TOCLIENT_MOVE_PLAYER_REL - Move default minimap from client-side C++ to server-side builtin Lua - [scheduled bump for 5.9.0] - PROTOCOL VERSION 45: - Minimap HUD element supports negative size values as percentages - [bump for 5.9.1] - PROTOCOL VERSION 46: - Move default hotbar from client-side C++ to server-side builtin Lua - Add shadow tint to Lighting packets - Add shadow color to CloudParam packets - Move death screen to server and make it a regular formspec - The server no longer triggers the hardcoded client-side death - formspec, but the client still supports it for compatibility with - old servers. - Rename TOCLIENT_DEATHSCREEN to TOCLIENT_DEATHSCREEN_LEGACY - Rename TOSERVER_RESPAWN to TOSERVER_RESPAWN_LEGACY - [scheduled bump for 5.10.0] - PROTOCOL VERSION 47: - Add artificial light color packet -*/ - -#define LATEST_PROTOCOL_VERSION 46 - -#define LATEST_PROTOCOL_VERSION_STRING TOSTRING(LATEST_PROTOCOL_VERSION) +extern const u16 LATEST_PROTOCOL_VERSION; // Server's supported network protocol range -#define SERVER_PROTOCOL_VERSION_MIN 37 -#define SERVER_PROTOCOL_VERSION_MAX LATEST_PROTOCOL_VERSION +constexpr u16 SERVER_PROTOCOL_VERSION_MIN = 37; // Client's supported network protocol range -#define CLIENT_PROTOCOL_VERSION_MIN 37 -#define CLIENT_PROTOCOL_VERSION_MAX LATEST_PROTOCOL_VERSION +constexpr u16 CLIENT_PROTOCOL_VERSION_MIN = 37; -// See also formspec [Version History] in doc/lua_api.md -#define FORMSPEC_API_VERSION 7 +extern const u16 FORMSPEC_API_VERSION; #define TEXTURENAME_ALLOWED_CHARS "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.-" @@ -965,6 +740,8 @@ enum ToServerCommand : u16 [2+12+12+4+4+4] u8 fov*80 [2+12+12+4+4+4+1] u8 ceil(wanted_range / MAP_BLOCKSIZE) [2+12+12+4+4+4+1+1] u8 camera_inverted (bool) + [2+12+12+4+4+4+1+1+1] f32 movement_speed + [2+12+12+4+4+4+1+1+1+4] f32 movement_direction */ @@ -1184,4 +961,4 @@ enum InteractAction : u8 INTERACT_PLACE, // 3: place block or item (to abovesurface) INTERACT_USE, // 4: use item INTERACT_ACTIVATE // 5: rightclick air ("activate") -}; \ No newline at end of file +}; diff --git a/src/network/serverpackethandler.cpp b/src/network/serverpackethandler.cpp index c17d32e41..449e164b6 100644 --- a/src/network/serverpackethandler.cpp +++ b/src/network/serverpackethandler.cpp @@ -135,10 +135,10 @@ void Server::handleCommand_Init(NetworkPacket* pkt) // Figure out a working version if it is possible at all if (max_net_proto_version >= SERVER_PROTOCOL_VERSION_MIN || - min_net_proto_version <= SERVER_PROTOCOL_VERSION_MAX) { + min_net_proto_version <= LATEST_PROTOCOL_VERSION) { // If maximum is larger than our maximum, go with our maximum - if (max_net_proto_version > SERVER_PROTOCOL_VERSION_MAX) - net_proto_version = SERVER_PROTOCOL_VERSION_MAX; + if (max_net_proto_version > LATEST_PROTOCOL_VERSION) + net_proto_version = LATEST_PROTOCOL_VERSION; // Else go with client's maximum else net_proto_version = max_net_proto_version; @@ -477,12 +477,24 @@ void Server::process_PlayerPos(RemotePlayer *player, PlayerSAO *playersao, u8 bits = 0; // bits instead of bool so it is extensible later *pkt >> keyPressed; + player->control.unpackKeysPressed(keyPressed); + *pkt >> f32fov; fov = (f32)f32fov / 80.0f; *pkt >> wanted_range; + if (pkt->getRemainingBytes() >= 1) *pkt >> bits; + if (pkt->getRemainingBytes() >= 8) { + *pkt >> player->control.movement_speed; + *pkt >> player->control.movement_direction; + } else { + player->control.movement_speed = 0.0f; + player->control.movement_direction = 0.0f; + player->control.setMovementFromKeys(); + } + v3f position((f32)ps.X / 100.0f, (f32)ps.Y / 100.0f, (f32)ps.Z / 100.0f); v3f speed((f32)ss.X / 100.0f, (f32)ss.Y / 100.0f, (f32)ss.Z / 100.0f); @@ -501,8 +513,6 @@ void Server::process_PlayerPos(RemotePlayer *player, PlayerSAO *playersao, playersao->setWantedRange(wanted_range); playersao->setCameraInverted(bits & 0x01); - player->control.unpackKeysPressed(keyPressed); - if (playersao->checkMovementCheat()) { // Call callbacks m_script->on_cheat(playersao, "moved_too_fast"); @@ -1001,12 +1011,12 @@ void Server::handleCommand_Interact(NetworkPacket *pkt) /* Check that target is reasonably close */ - static thread_local const bool enable_anticheat = - !g_settings->getBool("disable_anticheat"); + static thread_local const u32 anticheat_flags = + g_settings->getFlagStr("anticheat_flags", flagdesc_anticheat, nullptr); if ((action == INTERACT_START_DIGGING || action == INTERACT_DIGGING_COMPLETED || action == INTERACT_PLACE || action == INTERACT_USE) && - enable_anticheat && !isSingleplayer()) { + (anticheat_flags & AC_INTERACTION) && !isSingleplayer()) { v3f target_pos = player_pos; if (pointed.type == POINTEDTHING_NODE) { target_pos = intToFloat(pointed.node_undersurface, BS); @@ -1109,7 +1119,7 @@ void Server::handleCommand_Interact(NetworkPacket *pkt) /* Cheat prevention */ bool is_valid_dig = true; - if (enable_anticheat && !isSingleplayer()) { + if ((anticheat_flags & AC_DIGGING) && !isSingleplayer()) { v3s16 nocheat_p = playersao->getNoCheatDigPos(); float nocheat_t = playersao->getNoCheatDigTime(); playersao->noCheatDigEnd(); diff --git a/src/nodemetadata.cpp b/src/nodemetadata.cpp index a11503ebe..a86db15ad 100644 --- a/src/nodemetadata.cpp +++ b/src/nodemetadata.cpp @@ -62,14 +62,14 @@ void NodeMetadata::serialize(std::ostream &os, u8 version, bool disk) const void NodeMetadata::deSerialize(std::istream &is, u8 version) { clear(); - int num_vars = readU32(is); - for(int i=0; i= 2) { if (readU8(is) == 1) - markPrivate(name, true); + m_privatevars.insert(name); } } @@ -89,12 +89,12 @@ bool NodeMetadata::empty() const } -void NodeMetadata::markPrivate(const std::string &name, bool set) +bool NodeMetadata::markPrivate(const std::string &name, bool set) { if (set) - m_privatevars.insert(name); + return m_privatevars.insert(name).second; else - m_privatevars.erase(name); + return m_privatevars.erase(name) > 0; } int NodeMetadata::countNonPrivate() const @@ -144,6 +144,8 @@ void NodeMetadataList::serialize(std::ostream &os, u8 blockver, bool disk, writeS16(os, p.Z); } else { // Serialize positions within a mapblock + static_assert(MAP_BLOCKSIZE * MAP_BLOCKSIZE * MAP_BLOCKSIZE <= U16_MAX, + "position too big to serialize"); u16 p16 = (p.Z * MAP_BLOCKSIZE + p.Y) * MAP_BLOCKSIZE + p.X; writeU16(os, p16); } @@ -246,8 +248,7 @@ void NodeMetadataList::set(v3s16 p, NodeMetadata *d) void NodeMetadataList::clear() { if (m_is_metadata_owner) { - NodeMetadataMap::const_iterator it; - for (it = m_data.begin(); it != m_data.end(); ++it) + for (auto it = m_data.begin(); it != m_data.end(); ++it) delete it->second; } m_data.clear(); diff --git a/src/nodemetadata.h b/src/nodemetadata.h index da277aabd..3c2a67f53 100644 --- a/src/nodemetadata.h +++ b/src/nodemetadata.h @@ -57,7 +57,10 @@ public: { return m_privatevars.count(name) != 0; } - void markPrivate(const std::string &name, bool set); + + /// Marks a key as private. + /// @return metadata modified? + bool markPrivate(const std::string &name, bool set); private: int countNonPrivate() const; diff --git a/src/pathfinder.cpp b/src/pathfinder.cpp index 5420431f5..8b90a139c 100644 --- a/src/pathfinder.cpp +++ b/src/pathfinder.cpp @@ -40,6 +40,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #endif +#include + /******************************************************************************/ /* Typedefs and macros */ /******************************************************************************/ diff --git a/src/player.cpp b/src/player.cpp index fd25626ca..7361549e0 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -173,6 +173,42 @@ u16 Player::getMaxHotbarItemcount() return mainlist ? std::min(mainlist->getSize(), (u32) hud_hotbar_itemcount) : 0; } +void PlayerControl::setMovementFromKeys() +{ + bool a_up = direction_keys & (1 << 0), + a_down = direction_keys & (1 << 1), + a_left = direction_keys & (1 << 2), + a_right = direction_keys & (1 << 3); + + if (a_up || a_down || a_left || a_right) { + // if contradictory keys pressed, stay still + if (a_up && a_down && a_left && a_right) + movement_speed = 0.0f; + else if (a_up && a_down && !a_left && !a_right) + movement_speed = 0.0f; + else if (!a_up && !a_down && a_left && a_right) + movement_speed = 0.0f; + else + // If there is a keyboard event, assume maximum speed + movement_speed = 1.0f; + } + + // Check keyboard for input + float x = 0, y = 0; + if (a_up) + y += 1; + if (a_down) + y -= 1; + if (a_left) + x -= 1; + if (a_right) + x += 1; + + if (x != 0 || y != 0) + // If there is a keyboard event, it takes priority + movement_direction = std::atan2(x, y); +} + #ifndef SERVER u32 PlayerControl::getKeysPressed() const @@ -231,6 +267,11 @@ void PlayerControl::unpackKeysPressed(u32 keypress_bits) zoom = keypress_bits & (1 << 9); } +v2f PlayerControl::getMovement() const +{ + return v2f(std::sin(movement_direction), std::cos(movement_direction)) * movement_speed; +} + static auto tie(const PlayerPhysicsOverride &o) { // Make sure to add new members to this list! diff --git a/src/player.h b/src/player.h index 53411fea4..c729f98a0 100644 --- a/src/player.h +++ b/src/player.h @@ -86,6 +86,11 @@ struct PlayerControl movement_direction = a_movement_direction; } + // Sets movement_speed and movement_direction according to direction_keys + // if direction_keys != 0, otherwise leaves them unchanged to preserve + // joystick input. + void setMovementFromKeys(); + #ifndef SERVER // For client use u32 getKeysPressed() const; @@ -94,6 +99,7 @@ struct PlayerControl // For server use void unpackKeysPressed(u32 keypress_bits); + v2f getMovement() const; u8 direction_keys = 0; bool jump = false; @@ -102,7 +108,7 @@ struct PlayerControl bool zoom = false; bool dig = false; bool place = false; - // Note: These four are NOT available on the server + // Note: These two are NOT available on the server float pitch = 0.0f; float yaw = 0.0f; float movement_speed = 0.0f; @@ -197,7 +203,7 @@ public: f32 movement_liquid_sink; f32 movement_gravity; - v2s32 local_animations[4]; + v2f local_animations[4]; float local_animation_speed; std::string inventory_formspec; diff --git a/src/remoteplayer.h b/src/remoteplayer.h index 4923c307d..cbfc80d91 100644 --- a/src/remoteplayer.h +++ b/src/remoteplayer.h @@ -113,14 +113,14 @@ public: inline void setModified(const bool x) { m_dirty = x; } - void setLocalAnimations(v2s32 frames[4], float frame_speed) + void setLocalAnimations(v2f frames[4], float frame_speed) { for (int i = 0; i < 4; i++) local_animations[i] = frames[i]; local_animation_speed = frame_speed; } - void getLocalAnimations(v2s32 *frames, float *frame_speed) + void getLocalAnimations(v2f *frames, float *frame_speed) { for (int i = 0; i < 4; i++) frames[i] = local_animations[i]; diff --git a/src/script/common/c_packer.cpp b/src/script/common/c_packer.cpp index 579167952..bbef89c1f 100644 --- a/src/script/common/c_packer.cpp +++ b/src/script/common/c_packer.cpp @@ -507,6 +507,7 @@ PackedValue *script_pack(lua_State *L, int idx) void script_unpack(lua_State *L, PackedValue *pv) { + assert(pv); // table that tracks objects for keep_ref / PUSHREF (key = instr index) lua_newtable(L); const int top = lua_gettop(L); diff --git a/src/script/cpp_api/s_async.cpp b/src/script/cpp_api/s_async.cpp index 75b1a8205..bfcfb4f7d 100644 --- a/src/script/cpp_api/s_async.cpp +++ b/src/script/cpp_api/s_async.cpp @@ -50,11 +50,12 @@ AsyncEngine::~AsyncEngine() } // Wait for threads to finish + infostream << "AsyncEngine: Waiting for " << workerThreads.size() + << " threads" << std::endl; for (AsyncWorkerThread *workerThread : workerThreads) { workerThread->wait(); } - // Force kill all threads for (AsyncWorkerThread *workerThread : workerThreads) { delete workerThread; } diff --git a/src/script/cpp_api/s_env.cpp b/src/script/cpp_api/s_env.cpp index deac90f3c..007622d52 100644 --- a/src/script/cpp_api/s_env.cpp +++ b/src/script/cpp_api/s_env.cpp @@ -34,10 +34,11 @@ with this program; if not, write to the Free Software Foundation, Inc., class LuaABM : public ActiveBlockModifier { private: - int m_id; + const int m_id; std::vector m_trigger_contents; std::vector m_required_neighbors; + std::vector m_without_neighbors; float m_trigger_interval; u32 m_trigger_chance; bool m_simple_catch_up; @@ -47,11 +48,13 @@ public: LuaABM(int id, const std::vector &trigger_contents, const std::vector &required_neighbors, + const std::vector &without_neighbors, float trigger_interval, u32 trigger_chance, bool simple_catch_up, s16 min_y, s16 max_y): m_id(id), m_trigger_contents(trigger_contents), m_required_neighbors(required_neighbors), + m_without_neighbors(without_neighbors), m_trigger_interval(trigger_interval), m_trigger_chance(trigger_chance), m_simple_catch_up(simple_catch_up), @@ -67,6 +70,10 @@ public: { return m_required_neighbors; } + virtual const std::vector &getWithoutNeighbors() const + { + return m_without_neighbors; + } virtual float getTriggerInterval() { return m_trigger_interval; @@ -230,6 +237,11 @@ void ScriptApiEnv::readABMs() read_nodenames(L, -1, required_neighbors); lua_pop(L, 1); + std::vector without_neighbors; + lua_getfield(L, current_abm, "without_neighbors"); + read_nodenames(L, -1, without_neighbors); + lua_pop(L, 1); + float trigger_interval = 10.0; getfloatfield(L, current_abm, "interval", trigger_interval); @@ -250,7 +262,8 @@ void ScriptApiEnv::readABMs() lua_pop(L, 1); LuaABM *abm = new LuaABM(id, trigger_contents, required_neighbors, - trigger_interval, trigger_chance, simple_catch_up, min_y, max_y); + without_neighbors, trigger_interval, trigger_chance, + simple_catch_up, min_y, max_y); env->addActiveBlockModifier(abm); diff --git a/src/script/lua_api/CMakeLists.txt b/src/script/lua_api/CMakeLists.txt index d9405e4fe..2e12f8c56 100644 --- a/src/script/lua_api/CMakeLists.txt +++ b/src/script/lua_api/CMakeLists.txt @@ -6,6 +6,7 @@ set(common_SCRIPT_LUA_API_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/l_env.cpp ${CMAKE_CURRENT_SOURCE_DIR}/l_http.cpp ${CMAKE_CURRENT_SOURCE_DIR}/l_inventory.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/l_ipc.cpp ${CMAKE_CURRENT_SOURCE_DIR}/l_item.cpp ${CMAKE_CURRENT_SOURCE_DIR}/l_itemstackmeta.cpp ${CMAKE_CURRENT_SOURCE_DIR}/l_mapgen.cpp diff --git a/src/script/lua_api/l_env.cpp b/src/script/lua_api/l_env.cpp index 125a352bc..726300b07 100644 --- a/src/script/lua_api/l_env.cpp +++ b/src/script/lua_api/l_env.cpp @@ -160,7 +160,7 @@ void LuaEmergeAreaCallback(v3s16 blockpos, EmergeAction action, void *param) // state must be protected by envlock Server *server = state->script->getServer(); - MutexAutoLock envlock(server->m_env_mutex); + Server::EnvAutoLock envlock(server); state->refcount--; diff --git a/src/script/lua_api/l_ipc.cpp b/src/script/lua_api/l_ipc.cpp new file mode 100644 index 000000000..8b9f2aec9 --- /dev/null +++ b/src/script/lua_api/l_ipc.cpp @@ -0,0 +1,141 @@ +// Minetest +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "lua_api/l_ipc.h" +#include "lua_api/l_internal.h" +#include "common/c_packer.h" +#include "server.h" +#include "debug.h" +#include + +typedef std::shared_lock SharedReadLock; +typedef std::unique_lock SharedWriteLock; + +static inline auto read_pv(lua_State *L, int idx) +{ + std::unique_ptr ret; + if (!lua_isnil(L, idx)) { + ret.reset(script_pack(L, idx)); + if (ret->contains_userdata) + throw LuaError("Userdata not allowed"); + } + return ret; +} + +int ModApiIPC::l_ipc_get(lua_State *L) +{ + auto *store = getGameDef(L)->getModIPCStore(); + + auto key = readParam(L, 1); + + { + SharedReadLock autolock(store->mutex); + auto it = store->map.find(key); + if (it == store->map.end()) + lua_pushnil(L); + else + script_unpack(L, it->second.get()); + } + return 1; +} + +int ModApiIPC::l_ipc_set(lua_State *L) +{ + auto *store = getGameDef(L)->getModIPCStore(); + + auto key = readParam(L, 1); + + luaL_checkany(L, 2); + auto pv = read_pv(L, 2); + + { + SharedWriteLock autolock(store->mutex); + if (pv) + store->map[key] = std::move(pv); + else + store->map.erase(key); // delete the map value for nil + } + store->signal(); + return 0; +} + +int ModApiIPC::l_ipc_cas(lua_State *L) +{ + auto *store = getGameDef(L)->getModIPCStore(); + + auto key = readParam(L, 1); + + luaL_checkany(L, 2); + const int idx_old = 2; + + luaL_checkany(L, 3); + auto pv_new = read_pv(L, 3); + + bool ok = false; + { + SharedWriteLock autolock(store->mutex); + // unpack and compare old value + auto it = store->map.find(key); + if (it == store->map.end()) { + ok = lua_isnil(L, idx_old); + } else { + script_unpack(L, it->second.get()); + ok = lua_equal(L, idx_old, -1); + lua_pop(L, 1); + } + // put new value + if (ok) { + if (pv_new) + store->map[key] = std::move(pv_new); + else + store->map.erase(key); + } + } + + if (ok) + store->signal(); + lua_pushboolean(L, ok); + return 1; +} + +int ModApiIPC::l_ipc_poll(lua_State *L) +{ + auto *store = getGameDef(L)->getModIPCStore(); + + auto key = readParam(L, 1); + + auto timeout = std::chrono::milliseconds( + std::max(0, luaL_checkinteger(L, 2)) + ); + + bool ret; + { + SharedReadLock autolock(store->mutex); + + // wait until value exists or timeout + ret = store->condvar.wait_for(autolock, timeout, [&] () -> bool { + return store->map.count(key) != 0; + }); + } + + lua_pushboolean(L, ret); + return 1; +} + +/* + * Implementation note: + * Iterating over the IPC table is intentionally not supported. + * Mods should know what they have set. + * This has the nice side effect that mods are able to use a randomly generated key + * if they really *really* want to avoid other code touching their data. + */ + +void ModApiIPC::Initialize(lua_State *L, int top) +{ + FATAL_ERROR_IF(!getGameDef(L)->getModIPCStore(), "ModIPCStore missing from gamedef"); + + API_FCT(ipc_get); + API_FCT(ipc_set); + API_FCT(ipc_cas); + API_FCT(ipc_poll); +} diff --git a/src/script/lua_api/l_ipc.h b/src/script/lua_api/l_ipc.h new file mode 100644 index 000000000..dc73a5b86 --- /dev/null +++ b/src/script/lua_api/l_ipc.h @@ -0,0 +1,17 @@ +// Minetest +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "lua_api/l_base.h" + +class ModApiIPC : public ModApiBase { +private: + static int l_ipc_get(lua_State *L); + static int l_ipc_set(lua_State *L); + static int l_ipc_cas(lua_State *L); + static int l_ipc_poll(lua_State *L); + +public: + static void Initialize(lua_State *L, int top); +}; diff --git a/src/script/lua_api/l_itemstackmeta.cpp b/src/script/lua_api/l_itemstackmeta.cpp index ebabf7bae..730fab3b4 100644 --- a/src/script/lua_api/l_itemstackmeta.cpp +++ b/src/script/lua_api/l_itemstackmeta.cpp @@ -41,7 +41,7 @@ void ItemStackMetaRef::clearMeta() void ItemStackMetaRef::reportMetadataChange(const std::string *name) { - // TODO + // nothing to do } // Exported functions @@ -89,7 +89,6 @@ ItemStackMetaRef::~ItemStackMetaRef() void ItemStackMetaRef::create(lua_State *L, LuaItemStack *istack) { ItemStackMetaRef *o = new ItemStackMetaRef(istack); - //infostream<<"NodeMetaRef::create: o="<m_rendering_engine->get_gui_env(), + engine->m_parent, -1, engine->m_menumanager, + engine->m_texture_source.get(), url); + openURLMenu->drop(); + return 1; +} + /******************************************************************************/ int ModApiMainMenu::l_open_dir(lua_State *L) { @@ -1136,7 +1160,9 @@ void ModApiMainMenu::Initialize(lua_State *L, int top) API_FCT(get_active_irrlicht_device); API_FCT(get_min_supp_proto); API_FCT(get_max_supp_proto); + API_FCT(get_formspec_version); API_FCT(open_url); + API_FCT(open_url_dialog); API_FCT(open_dir); API_FCT(share_file); API_FCT(do_async_callback); @@ -1166,6 +1192,7 @@ void ModApiMainMenu::InitializeAsync(lua_State *L, int top) API_FCT(download_file); API_FCT(get_min_supp_proto); API_FCT(get_max_supp_proto); + API_FCT(get_formspec_version); API_FCT(get_language); API_FCT(gettext); } diff --git a/src/script/lua_api/l_mainmenu.h b/src/script/lua_api/l_mainmenu.h index 5535d2170..cb3e7f9ca 100644 --- a/src/script/lua_api/l_mainmenu.h +++ b/src/script/lua_api/l_mainmenu.h @@ -159,9 +159,13 @@ private: static int l_get_max_supp_proto(lua_State *L); + static int l_get_formspec_version(lua_State *L); + // other static int l_open_url(lua_State *L); + static int l_open_url_dialog(lua_State *L); + static int l_open_dir(lua_State *L); static int l_share_file(lua_State *L); diff --git a/src/script/lua_api/l_nodemeta.cpp b/src/script/lua_api/l_nodemeta.cpp index f1a2d5c4b..07bdced99 100644 --- a/src/script/lua_api/l_nodemeta.cpp +++ b/src/script/lua_api/l_nodemeta.cpp @@ -58,6 +58,8 @@ void NodeMetaRef::reportMetadataChange(const std::string *name) // Inform other things that the metadata has changed NodeMetadata *meta = dynamic_cast(getmeta(false)); + bool is_private_change = meta && name && meta->isPrivate(*name); + // If the metadata is now empty, get rid of it if (meta && meta->empty()) { clearMeta(); @@ -67,7 +69,7 @@ void NodeMetaRef::reportMetadataChange(const std::string *name) MapEditEvent event; event.type = MEET_BLOCK_NODE_METADATA_CHANGED; event.setPositionModified(m_p); - event.is_private_change = name && meta && meta->isPrivate(*name); + event.is_private_change = is_private_change; m_env->getMap().dispatchEvent(event); } @@ -94,21 +96,24 @@ int NodeMetaRef::l_mark_as_private(lua_State *L) NodeMetaRef *ref = checkObject(L, 1); NodeMetadata *meta = dynamic_cast(ref->getmeta(true)); - assert(meta); + if (!meta) + return 0; + bool modified = false; if (lua_istable(L, 2)) { lua_pushnil(L); while (lua_next(L, 2) != 0) { // key at index -2 and value at index -1 luaL_checktype(L, -1, LUA_TSTRING); - meta->markPrivate(readParam(L, -1), true); + modified |= meta->markPrivate(readParam(L, -1), true); // removes value, keeps key for next iteration lua_pop(L, 1); } } else if (lua_isstring(L, 2)) { - meta->markPrivate(readParam(L, 2), true); + modified |= meta->markPrivate(readParam(L, 2), true); } - ref->reportMetadataChange(); + if (modified) + ref->reportMetadataChange(); return 0; } @@ -145,12 +150,13 @@ bool NodeMetaRef::handleFromTable(lua_State *L, int table, IMetadata *_meta) Inventory *inv = meta->getInventory(); lua_getfield(L, table, "inventory"); if (lua_istable(L, -1)) { + auto *gamedef = getGameDef(L); int inventorytable = lua_gettop(L); lua_pushnil(L); while (lua_next(L, inventorytable) != 0) { // key at index -2 and value at index -1 - std::string name = luaL_checkstring(L, -2); - read_inventory_list(L, -1, inv, name.c_str(), getServer(L)); + const char *name = luaL_checkstring(L, -2); + read_inventory_list(L, -1, inv, name, gamedef); lua_pop(L, 1); // Remove value, keep key for next iteration } lua_pop(L, 1); @@ -177,7 +183,6 @@ NodeMetaRef::NodeMetaRef(IMetadata *meta): void NodeMetaRef::create(lua_State *L, v3s16 p, ServerEnvironment *env) { NodeMetaRef *o = new NodeMetaRef(p, env); - //infostream<<"NodeMetaRef::create: o="<(L, 6, 30.0f); @@ -453,12 +453,12 @@ int ObjectRef::l_get_local_animation(lua_State *L) if (player == nullptr) return 0; - v2s32 frames[4]; + v2f frames[4]; float frame_speed; player->getLocalAnimations(frames, &frame_speed); - for (const v2s32 &frame : frames) { - push_v2s32(L, frame); + for (const v2f &frame : frames) { + push_v2f(L, frame); } lua_pushnumber(L, frame_speed); @@ -1622,6 +1622,13 @@ int ObjectRef::l_get_player_control(lua_State *L) lua_setfield(L, -2, "dig"); lua_pushboolean(L, control.place); lua_setfield(L, -2, "place"); + + v2f movement = control.getMovement(); + lua_pushnumber(L, movement.X); + lua_setfield(L, -2, "movement_x"); + lua_pushnumber(L, movement.Y); + lua_setfield(L, -2, "movement_y"); + // Legacy fields to ensure mod compatibility lua_pushboolean(L, control.dig); lua_setfield(L, -2, "LMB"); @@ -2626,6 +2633,7 @@ int ObjectRef::l_set_lighting(lua_State *L) getfloatfield(L, -1, "intensity", lighting.shadow_intensity); lua_getfield(L, -1, "tint"); read_color(L, -1, &lighting.shadow_tint); + lua_pop(L, 1); // tint } lua_pop(L, 1); // shadows @@ -2648,6 +2656,14 @@ int ObjectRef::l_set_lighting(lua_State *L) lighting.volumetric_light_strength = rangelim(lighting.volumetric_light_strength, 0.0f, 1.0f); } lua_pop(L, 1); // volumetric_light + + lua_getfield(L, 2, "bloom"); + if (lua_istable(L, -1)) { + lighting.bloom_intensity = getfloatfield_default(L, -1, "intensity", lighting.bloom_intensity); + lighting.bloom_strength_factor = getfloatfield_default(L, -1, "strength_factor", lighting.bloom_strength_factor); + lighting.bloom_radius = getfloatfield_default(L, -1, "radius", lighting.bloom_radius); + } + lua_pop(L, 1); // bloom } getServer(L)->setLighting(player, lighting); @@ -2694,6 +2710,14 @@ int ObjectRef::l_get_lighting(lua_State *L) lua_pushnumber(L, lighting.volumetric_light_strength); lua_setfield(L, -2, "strength"); lua_setfield(L, -2, "volumetric_light"); + lua_newtable(L); // "bloom" + lua_pushnumber(L, lighting.bloom_intensity); + lua_setfield(L, -2, "intensity"); + lua_pushnumber(L, lighting.bloom_strength_factor); + lua_setfield(L, -2, "strength_factor"); + lua_pushnumber(L, lighting.bloom_radius); + lua_setfield(L, -2, "radius"); + lua_setfield(L, -2, "bloom"); return 1; } diff --git a/src/script/lua_api/l_playermeta.cpp b/src/script/lua_api/l_playermeta.cpp index e2e6ed8da..e937c145c 100644 --- a/src/script/lua_api/l_playermeta.cpp +++ b/src/script/lua_api/l_playermeta.cpp @@ -38,7 +38,7 @@ void PlayerMetaRef::clearMeta() void PlayerMetaRef::reportMetadataChange(const std::string *name) { - // TODO + // the server saves these on its own } // Creates an PlayerMetaRef and leaves it on top of stack @@ -54,9 +54,6 @@ void PlayerMetaRef::create(lua_State *L, IMetadata *metadata) void PlayerMetaRef::Register(lua_State *L) { registerMetadataClass(L, className, methods); - - // Cannot be created from Lua - // lua_register(L, className, create_object); } const char PlayerMetaRef::className[] = "PlayerMetaRef"; diff --git a/src/script/lua_api/l_util.cpp b/src/script/lua_api/l_util.cpp index 75a11a050..c899e55f4 100644 --- a/src/script/lua_api/l_util.cpp +++ b/src/script/lua_api/l_util.cpp @@ -33,6 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "convert_json.h" #include "debug.h" #include "log.h" +#include "log_internal.h" #include "tool.h" #include "filesys.h" #include "settings.h" @@ -531,7 +532,7 @@ int ModApiUtil::l_get_version(lua_State *L) lua_pushnumber(L, SERVER_PROTOCOL_VERSION_MIN); lua_setfield(L, table, "proto_min"); - lua_pushnumber(L, SERVER_PROTOCOL_VERSION_MAX); + lua_pushnumber(L, LATEST_PROTOCOL_VERSION); lua_setfield(L, table, "proto_max"); if (strcmp(g_version_string, g_version_hash) != 0) { diff --git a/src/script/scripting_emerge.cpp b/src/script/scripting_emerge.cpp index 3467b1495..f96a6c294 100644 --- a/src/script/scripting_emerge.cpp +++ b/src/script/scripting_emerge.cpp @@ -35,6 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "lua_api/l_util.h" #include "lua_api/l_vmanip.h" #include "lua_api/l_settings.h" +#include "lua_api/l_ipc.h" extern "C" { #include @@ -89,5 +90,6 @@ void EmergeScripting::InitializeModApi(lua_State *L, int top) ModApiMapgen::InitializeEmerge(L, top); ModApiServer::InitializeAsync(L, top); ModApiUtil::InitializeAsync(L, top); + ModApiIPC::Initialize(L, top); // TODO ^ these should also be renamed to InitializeRO or such } diff --git a/src/script/scripting_server.cpp b/src/script/scripting_server.cpp index 324850011..d7d2513bb 100644 --- a/src/script/scripting_server.cpp +++ b/src/script/scripting_server.cpp @@ -46,6 +46,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "lua_api/l_settings.h" #include "lua_api/l_http.h" #include "lua_api/l_storage.h" +#include "lua_api/l_ipc.h" extern "C" { #include @@ -121,6 +122,7 @@ void ServerScripting::initAsync() asyncEngine.registerStateInitializer(ModApiCraft::InitializeAsync); asyncEngine.registerStateInitializer(ModApiItem::InitializeAsync); asyncEngine.registerStateInitializer(ModApiServer::InitializeAsync); + asyncEngine.registerStateInitializer(ModApiIPC::Initialize); // not added: ModApiMapgen is a minefield for thread safety // not added: ModApiHttp async api can't really work together with our jobs // not added: ModApiStorage is probably not thread safe(?) @@ -176,6 +178,7 @@ void ServerScripting::InitializeModApi(lua_State *L, int top) ModApiHttp::Initialize(L, top); ModApiStorage::Initialize(L, top); ModApiChannels::Initialize(L, top); + ModApiIPC::Initialize(L, top); } void ServerScripting::InitializeAsync(lua_State *L, int top) diff --git a/src/serialization.cpp b/src/serialization.cpp index 0319b0159..d35e0f23f 100644 --- a/src/serialization.cpp +++ b/src/serialization.cpp @@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include +#include /* report a zlib or i/o error */ static void zerr(int ret) diff --git a/src/server.cpp b/src/server.cpp index 1a2d8cf4f..f690be745 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -21,6 +21,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include +#include "irr_v2d.h" #include "network/connection.h" #include "network/networkprotocol.h" #include "network/serveropcodes.h" @@ -85,6 +86,15 @@ public: {} }; +ModIPCStore::~ModIPCStore() +{ + // we don't have to do this, it's pure debugging aid + if (!std::unique_lock(mutex, std::try_to_lock).owns_lock()) { + errorstream << FUNCTION_NAME << ": lock is still in use!" << std::endl; + assert(0); + } +} + class ServerThread : public Thread { public: @@ -133,9 +143,13 @@ void *ServerThread::run() u64 t0 = porting::getTimeUs(); - const Server::StepSettings step_settings = m_server->getStepSettings(); + const auto step_settings = m_server->getStepSettings(); try { + // see explanation inside + if (dtime > step_settings.steplen) + m_server->yieldToOtherThreads(dtime); + m_server->AsyncRunStep(step_settings.pause ? 0.0f : dtime); const float remaining_time = step_settings.steplen @@ -353,7 +367,7 @@ Server::~Server() m_emerge->stopThreads(); if (m_env) { - MutexAutoLock envlock(m_env_mutex); + EnvAutoLock envlock(this); infostream << "Server: Executing shutdown hooks" << std::endl; try { @@ -389,6 +403,10 @@ Server::~Server() infostream << "Server: Saving environment metadata" << std::endl; m_env->saveMeta(); + // Delete classes that depend on the environment + m_inventory_mgr.reset(); + m_script.reset(); + // Note that this also deletes and saves the map. delete m_env; m_env = nullptr; @@ -405,6 +423,9 @@ Server::~Server() } } + // emerge may depend on definition managers, so destroy first + m_emerge.reset(); + // Delete the rest in the reverse order of creation delete m_game_settings; delete m_banmanager; @@ -461,7 +482,7 @@ void Server::init() } //lock environment - MutexAutoLock envlock(m_env_mutex); + EnvAutoLock envlock(this); // Create the Map (loads map_meta.txt, overriding configured mapgen params) auto startup_server_map = std::make_unique(m_path_world, this, @@ -653,9 +674,9 @@ void Server::AsyncRunStep(float dtime, bool initial_step) } { - MutexAutoLock lock(m_env_mutex); + EnvAutoLock lock(this); float max_lag = m_env->getMaxLagEstimate(); - constexpr float lag_warn_threshold = 2.0f; + constexpr float lag_warn_threshold = 1.0f; // Decrease value gradually, halve it every minute. if (m_max_lag_decrease.step(dtime, 0.5f)) { @@ -686,7 +707,7 @@ void Server::AsyncRunStep(float dtime, bool initial_step) static const float map_timer_and_unload_dtime = 2.92; if(m_map_timer_and_unload_interval.step(dtime, map_timer_and_unload_dtime)) { - MutexAutoLock lock(m_env_mutex); + EnvAutoLock lock(this); // Run Map's timers and unload unused data ScopeProfiler sp(g_profiler, "Server: map timer and unload"); m_env->getMap().timerUpdate(map_timer_and_unload_dtime, @@ -704,7 +725,7 @@ void Server::AsyncRunStep(float dtime, bool initial_step) */ if (m_admin_chat) { if (!m_admin_chat->command_queue.empty()) { - MutexAutoLock lock(m_env_mutex); + EnvAutoLock lock(this); while (!m_admin_chat->command_queue.empty()) { ChatEvent *evt = m_admin_chat->command_queue.pop_frontNoEx(); handleChatInterfaceEvent(evt); @@ -725,7 +746,7 @@ void Server::AsyncRunStep(float dtime, bool initial_step) { m_liquid_transform_timer -= m_liquid_transform_every; - MutexAutoLock lock(m_env_mutex); + EnvAutoLock lock(this); ScopeProfiler sp(g_profiler, "Server: liquid transform"); @@ -786,7 +807,7 @@ void Server::AsyncRunStep(float dtime, bool initial_step) */ { //infostream<<"Server: Checking added and deleted active objects"<getFloat("server_map_save_interval"); if (counter >= save_interval) { counter = 0.0; - MutexAutoLock lock(m_env_mutex); + EnvAutoLock lock(this); ScopeProfiler sp(g_profiler, "Server: map saving (sum)"); @@ -1113,6 +1134,52 @@ void Server::Receive(float timeout) } } +void Server::yieldToOtherThreads(float dtime) +{ + /* + * Problem: the server thread and emerge thread compete for the envlock. + * While the emerge thread needs it just once or twice for every processed item + * the server thread uses it much more generously. + * This is usually not a problem as the server sleeps between steps, which leaves + * enough chance. But if the server is overloaded it's busy all the time and + * - even with a fair envlock - the emerge thread can't get up to speed. + * This generally has a much worse impact on gameplay than server lag itself + * ever would. + * + * Workaround: If we detect that the server is overloaded, introduce some careful + * artificial sleeps to leave the emerge threads enough chance to do their job. + * + * In the future the emerge code should be reworked to exclusively use a result + * queue, thereby avoiding this problem (and terrible workaround). + */ + + // don't activate workaround too quickly + constexpr size_t MIN_EMERGE_QUEUE_SIZE = 32; + const size_t qs_initial = m_emerge->getQueueSize(); + if (qs_initial < MIN_EMERGE_QUEUE_SIZE) + return; + + // give the thread a chance to run for every 28ms (on average) + // this was experimentally determined + const float QUANTUM = 28.0f / 1000; + // put an upper limit to not cause too much lag, also so this doesn't become self-sustaining + const int SLEEP_MAX = 10; + + int sleep_count = std::clamp(dtime / QUANTUM, 1, SLEEP_MAX); + + ScopeProfiler sp(g_profiler, "Server::yieldTo...() sleep", SPT_AVG); + size_t qs = qs_initial; + while (sleep_count-- > 0) { + sleep_ms(1); + // abort if we don't make progress + size_t qs2 = m_emerge->getQueueSize(); + if (qs2 >= qs || qs2 == 0) + break; + qs = qs2; + } + g_profiler->avg("Server::yieldTo...() progress [#]", qs_initial - qs); +} + PlayerSAO* Server::StageTwoClientInit(session_t peer_id) { std::string playername; @@ -1191,7 +1258,7 @@ inline void Server::handleCommand(NetworkPacket *pkt) void Server::ProcessData(NetworkPacket *pkt) { // Environment is locked first. - MutexAutoLock envlock(m_env_mutex); + EnvAutoLock envlock(this); ScopeProfiler sp(g_profiler, "Server: Process network packet (sum)"); u32 peer_id = pkt->getPeerId(); @@ -1861,7 +1928,10 @@ void Server::SendSetLighting(session_t peer_id, const Lighting &lighting) << lighting.exposure.speed_bright_dark << lighting.exposure.center_weight_power; - pkt << lighting.volumetric_light_strength << lighting.shadow_tint << lighting.artificial_light_color; + pkt << lighting.volumetric_light_strength << lighting.shadow_tint; + pkt << lighting.bloom_intensity << lighting.bloom_strength_factor << + lighting.bloom_radius; + pkt << lighting.artificial_light_color; Send(&pkt); } @@ -1928,14 +1998,21 @@ void Server::SendPlayerFov(session_t peer_id) Send(&pkt); } -void Server::SendLocalPlayerAnimations(session_t peer_id, v2s32 animation_frames[4], +void Server::SendLocalPlayerAnimations(session_t peer_id, v2f animation_frames[4], f32 animation_speed) { NetworkPacket pkt(TOCLIENT_LOCAL_PLAYER_ANIMATIONS, 0, peer_id); - pkt << animation_frames[0] << animation_frames[1] << animation_frames[2] - << animation_frames[3] << animation_speed; + for (int i = 0; i < 4; ++i) { + if (m_clients.getProtocolVersion(peer_id) >= 46) { + pkt << animation_frames[i]; + } else { + pkt << v2s32::from(animation_frames[i]); + } + } + + pkt << animation_speed; Send(&pkt); } @@ -2363,8 +2440,7 @@ void Server::SendBlockNoLock(session_t peer_id, MapBlock *block, u8 ver, void Server::SendBlocks(float dtime) { - MutexAutoLock envlock(m_env_mutex); - //TODO check if one big lock could be faster then multiple small ones + EnvAutoLock envlock(this); std::vector queue; @@ -2461,7 +2537,7 @@ bool Server::addMediaFile(const std::string &filename, const char *supported_ext[] = { ".png", ".jpg", ".bmp", ".tga", ".ogg", - ".x", ".b3d", ".obj", ".gltf", + ".x", ".b3d", ".obj", ".gltf", ".glb", // Custom translation file format ".tr", NULL @@ -2695,7 +2771,7 @@ void Server::sendRequestedMedia(session_t peer_id, void Server::stepPendingDynMediaCallbacks(float dtime) { - MutexAutoLock lock(m_env_mutex); + EnvAutoLock lock(this); for (auto it = m_pending_dyn_media.begin(); it != m_pending_dyn_media.end();) { it->second.expiry_timer -= dtime; @@ -2914,7 +2990,7 @@ void Server::DeleteClient(session_t peer_id, ClientDeletionReason reason) } } { - MutexAutoLock env_lock(m_env_mutex); + EnvAutoLock envlock(this); m_clients.DeleteClient(peer_id); } } @@ -3366,7 +3442,7 @@ Address Server::getPeerAddress(session_t peer_id) } void Server::setLocalPlayerAnimations(RemotePlayer *player, - v2s32 animation_frames[4], f32 frame_speed) + v2f animation_frames[4], f32 frame_speed) { sanity_check(player); player->setLocalAnimations(animation_frames, frame_speed); @@ -4107,7 +4183,7 @@ Translations *Server::getTranslationLanguage(const std::string &lang_code) std::unordered_map Server::getMediaList() { - MutexAutoLock env_lock(m_env_mutex); + EnvAutoLock envlock(this); std::unordered_map ret; for (auto &it : m_media) { @@ -4236,12 +4312,10 @@ u16 Server::getProtocolVersionMin() min_proto = LATEST_PROTOCOL_VERSION; return rangelim(min_proto, SERVER_PROTOCOL_VERSION_MIN, - SERVER_PROTOCOL_VERSION_MAX); + LATEST_PROTOCOL_VERSION); } u16 Server::getProtocolVersionMax() { - return g_settings->getBool("strict_protocol_version_checking") - ? LATEST_PROTOCOL_VERSION - : SERVER_PROTOCOL_VERSION_MAX; + return LATEST_PROTOCOL_VERSION; } diff --git a/src/server.h b/src/server.h index 5f6086cde..f2a9083b6 100644 --- a/src/server.h +++ b/src/server.h @@ -35,6 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "util/metricsbackend.h" #include "serverenvironment.h" #include "server/clientiface.h" +#include "threading/ordered_mutex.h" #include "chatmessage.h" #include "sound.h" #include "translation.h" @@ -46,6 +47,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include +#include +#include class ChatEvent; struct ChatEventChat; @@ -78,6 +81,20 @@ struct PackedValue; struct ParticleParameters; struct ParticleSpawnerParameters; +// Anticheat flags +enum { + AC_DIGGING = 0x01, + AC_INTERACTION = 0x02, + AC_MOVEMENT = 0x04 +}; + +constexpr const static FlagDesc flagdesc_anticheat[] = { + {"digging", AC_DIGGING}, + {"interaction", AC_INTERACTION}, + {"movement", AC_MOVEMENT}, + {NULL, 0} +}; + enum ClientDeletionReason { CDR_LEAVE, CDR_TIMEOUT, @@ -141,6 +158,25 @@ struct ClientInfo { std::string vers_string, lang_code; }; +struct ModIPCStore { + ModIPCStore() = default; + ~ModIPCStore(); + + /// RW lock for this entire structure + std::shared_mutex mutex; + /// Signalled on any changes to the map contents + std::condition_variable_any condvar; + /** + * Map storing the data + * + * @note Do not store `nil` data in this map, instead remove the whole key. + */ + std::unordered_map> map; + + /// @note Should be called without holding the lock. + inline void signal() { condvar.notify_all(); } +}; + class Server : public con::PeerHandler, public MapEventReceiver, public IGameDef { @@ -166,9 +202,12 @@ public: // Actual processing is done in another thread. // This just checks if there was an error in that thread. void step(); + // This is run by ServerThread and does the actual processing void AsyncRunStep(float dtime, bool initial_step = false); void Receive(float timeout); + void yieldToOtherThreads(float dtime); + PlayerSAO* StageTwoClientInit(session_t peer_id); /* @@ -297,12 +336,14 @@ public: NodeDefManager* getWritableNodeDefManager(); IWritableCraftDefManager* getWritableCraftDefManager(); + // Not under envlock virtual const std::vector &getMods() const; virtual const ModSpec* getModSpec(const std::string &modname) const; virtual const SubgameSpec* getGameSpec() const { return &m_gamespec; } static std::string getBuiltinLuaPath(); virtual std::string getWorldPath() const { return m_path_world; } virtual std::string getModDataPath() const { return m_path_mod_data; } + virtual ModIPCStore *getModIPCStore() { return &m_ipcstore; } inline bool isSingleplayer() const { return m_simple_singleplayer_mode; } @@ -340,7 +381,7 @@ public: Address getPeerAddress(session_t peer_id); - void setLocalPlayerAnimations(RemotePlayer *player, v2s32 animation_frames[4], + void setLocalPlayerAnimations(RemotePlayer *player, v2f animation_frames[4], f32 frame_speed); void setPlayerEyeOffset(RemotePlayer *player, v3f first, v3f third, v3f third_front); @@ -424,8 +465,14 @@ public: // Bind address Address m_bind_addr; - // Environment mutex (envlock) - std::mutex m_env_mutex; + // Public helper for taking the envlock in a scope + class EnvAutoLock { + public: + EnvAutoLock(Server *server): m_lock(server->m_env_mutex) {} + + private: + std::lock_guard m_lock; + }; protected: /* Do not add more members here, this is only required to make unit tests work. */ @@ -491,7 +538,7 @@ private: virtual void SendChatMessage(session_t peer_id, const ChatMessage &message); void SendTimeOfDay(session_t peer_id, u16 time, f32 time_speed); - void SendLocalPlayerAnimations(session_t peer_id, v2s32 animation_frames[4], + void SendLocalPlayerAnimations(session_t peer_id, v2f animation_frames[4], f32 animation_speed); void SendEyeOffset(session_t peer_id, v3f first, v3f third, v3f third_front); void SendPlayerPrivileges(session_t peer_id); @@ -595,11 +642,13 @@ private: */ PlayerSAO *emergePlayer(const char *name, session_t peer_id, u16 proto_version); - void handlePeerChanges(); - /* Variables */ + + // Environment mutex (envlock) + ordered_mutex m_env_mutex; + // World directory std::string m_path_world; std::string m_path_mod_data; @@ -654,6 +703,8 @@ private: std::unordered_map server_translations; + ModIPCStore m_ipcstore; + /* Threads */ diff --git a/src/server/clientiface.h b/src/server/clientiface.h index 3f5ba6434..5b20d8f70 100644 --- a/src/server/clientiface.h +++ b/src/server/clientiface.h @@ -33,6 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include +#include #include #include #include diff --git a/src/server/player_sao.cpp b/src/server/player_sao.cpp index 30c41bb1e..57b39d403 100644 --- a/src/server/player_sao.cpp +++ b/src/server/player_sao.cpp @@ -519,12 +519,13 @@ void PlayerSAO::rightClick(ServerActiveObject *clicker) void PlayerSAO::setHP(s32 target_hp, const PlayerHPChangeReason &reason, bool from_client) { - target_hp = rangelim(target_hp, 0, U16_MAX); - - if (target_hp == m_hp) + if (target_hp == m_hp || (m_hp == 0 && target_hp < 0)) return; // Nothing to do - s32 hp_change = m_env->getScriptIface()->on_player_hpchange(this, target_hp - (s32)m_hp, reason); + // Protect against overflow. + s32 hp_change = std::max((s64)target_hp - (s64)m_hp, S32_MIN); + + hp_change = m_env->getScriptIface()->on_player_hpchange(this, hp_change, reason); hp_change = std::min(hp_change, U16_MAX); // Protect against overflow s32 hp = (s32)m_hp + hp_change; @@ -645,9 +646,12 @@ void PlayerSAO::setMaxSpeedOverride(const v3f &vel) bool PlayerSAO::checkMovementCheat() { + static thread_local const u32 anticheat_flags = + g_settings->getFlagStr("anticheat_flags", flagdesc_anticheat, nullptr); + if (m_is_singleplayer || isAttached() || - g_settings->getBool("disable_anticheat")) { + !(anticheat_flags & AC_MOVEMENT)) { m_last_good_position = m_base_position; return false; } @@ -728,6 +732,11 @@ bool PlayerSAO::checkMovementCheat() required_time = MYMAX(required_time, d_vert / s); } + static thread_local float anticheat_movement_tolerance = + std::max(g_settings->getFloat("anticheat_movement_tolerance"), 1.0f); + + required_time /= anticheat_movement_tolerance; + if (m_move_pool.grab(required_time)) { m_last_good_position = m_base_position; } else { diff --git a/src/serverenvironment.cpp b/src/serverenvironment.cpp index ac627dd50..813184de1 100644 --- a/src/serverenvironment.cpp +++ b/src/serverenvironment.cpp @@ -827,6 +827,7 @@ struct ActiveABM { ActiveBlockModifier *abm; std::vector required_neighbors; + std::vector without_neighbors; int chance; s16 min_y, max_y; }; @@ -885,6 +886,10 @@ public: ndef->getIds(s, aabm.required_neighbors); SORT_AND_UNIQUE(aabm.required_neighbors); + for (const auto &s : abm->getWithoutNeighbors()) + ndef->getIds(s, aabm.without_neighbors); + SORT_AND_UNIQUE(aabm.without_neighbors); + // Trigger contents std::vector ids; for (const auto &s : abm->getTriggerContents()) @@ -996,8 +1001,11 @@ public: continue; // Check neighbors - if (!aabm.required_neighbors.empty()) { + const bool check_required_neighbors = !aabm.required_neighbors.empty(); + const bool check_without_neighbors = !aabm.without_neighbors.empty(); + if (check_required_neighbors || check_without_neighbors) { v3s16 p1; + bool have_required = false; for(p1.X = p0.X-1; p1.X <= p0.X+1; p1.X++) for(p1.Y = p0.Y-1; p1.Y <= p0.Y+1; p1.Y++) for(p1.Z = p0.Z-1; p1.Z <= p0.Z+1; p1.Z++) @@ -1015,12 +1023,25 @@ public: MapNode n = map->getNode(p1 + block->getPosRelative()); c = n.getContent(); } - if (CONTAINS(aabm.required_neighbors, c)) - goto neighbor_found; + if (check_required_neighbors && !have_required) { + if (CONTAINS(aabm.required_neighbors, c)) { + if (!check_without_neighbors) + goto neighbor_found; + have_required = true; + } + } + if (check_without_neighbors) { + if (CONTAINS(aabm.without_neighbors, c)) + goto neighbor_invalid; + } } + if (have_required || !check_required_neighbors) + goto neighbor_found; // No required neighbor found + neighbor_invalid: continue; } + neighbor_found: abms_run++; diff --git a/src/serverenvironment.h b/src/serverenvironment.h index d20cc0b3f..0b00fac91 100644 --- a/src/serverenvironment.h +++ b/src/serverenvironment.h @@ -63,6 +63,9 @@ public: // Set of required neighbors (trigger doesn't happen if none are found) // Empty = do not check neighbors virtual const std::vector &getRequiredNeighbors() const = 0; + // Set of without neighbors (trigger doesn't happen if any are found) + // Empty = do not check neighbors + virtual const std::vector &getWithoutNeighbors() const = 0; // Trigger interval in seconds virtual float getTriggerInterval() = 0; // Random chance of (1 / return value), 0 is disallowed diff --git a/src/servermap.cpp b/src/servermap.cpp index 0248497c1..f57e5b5e4 100644 --- a/src/servermap.cpp +++ b/src/servermap.cpp @@ -51,6 +51,18 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "database/database-postgresql.h" #endif +/* + Helpers +*/ + +void MapDatabaseAccessor::loadBlock(v3s16 blockpos, std::string &ret) +{ + ret.clear(); + dbase->loadBlock(blockpos, &ret); + if (ret.empty() && dbase_ro) + dbase_ro->loadBlock(blockpos, &ret); +} + /* ServerMap */ @@ -67,7 +79,7 @@ ServerMap::ServerMap(const std::string &savedir, IGameDef *gamedef, emerge->map_settings_mgr = &settings_mgr; /* - Try to load map; if not found, create a new one. + Try to open map; if not found, create a new one. */ // Determine which database backend to use @@ -79,10 +91,10 @@ ServerMap::ServerMap(const std::string &savedir, IGameDef *gamedef, conf.set("backend", "sqlite3"); } std::string backend = conf.get("backend"); - dbase = createDatabase(backend, savedir, conf); + m_db.dbase = createDatabase(backend, savedir, conf); if (conf.exists("readonly_backend")) { std::string readonly_dir = savedir + DIR_DELIM + "readonly"; - dbase_ro = createDatabase(conf.get("readonly_backend"), readonly_dir, conf); + m_db.dbase_ro = createDatabase(conf.get("readonly_backend"), readonly_dir, conf); } if (!conf.updateConfigFile(conf_path.c_str())) errorstream << "ServerMap::ServerMap(): Failed to update world.mt!" << std::endl; @@ -90,6 +102,9 @@ ServerMap::ServerMap(const std::string &savedir, IGameDef *gamedef, m_savedir = savedir; m_map_saving_enabled = false; + // Inform EmergeManager of db handles + m_emerge->initMap(&m_db); + m_save_time_counter = mb->addCounter( "minetest_map_save_time", "Time spent saving blocks (in microseconds)"); m_save_count_counter = mb->addCounter( @@ -159,11 +174,15 @@ ServerMap::~ServerMap() << ", exception: " << e.what() << std::endl; } - /* - Close database if it was opened - */ - delete dbase; - delete dbase_ro; + m_emerge->resetMap(); + + { + MutexAutoLock dblock(m_db.mutex); + delete m_db.dbase; + m_db.dbase = nullptr; + delete m_db.dbase_ro; + m_db.dbase_ro = nullptr; + } deleteDetachedBlocks(); } @@ -547,9 +566,10 @@ void ServerMap::save(ModifiedState save_level) void ServerMap::listAllLoadableBlocks(std::vector &dst) { - dbase->listAllLoadableBlocks(dst); - if (dbase_ro) - dbase_ro->listAllLoadableBlocks(dst); + MutexAutoLock dblock(m_db.mutex); + m_db.dbase->listAllLoadableBlocks(dst); + if (m_db.dbase_ro) + m_db.dbase_ro->listAllLoadableBlocks(dst); } void ServerMap::listAllLoadedBlocks(std::vector &dst) @@ -597,17 +617,21 @@ MapDatabase *ServerMap::createDatabase( void ServerMap::beginSave() { - dbase->beginSave(); + MutexAutoLock dblock(m_db.mutex); + m_db.dbase->beginSave(); } void ServerMap::endSave() { - dbase->endSave(); + MutexAutoLock dblock(m_db.mutex); + m_db.dbase->endSave(); } bool ServerMap::saveBlock(MapBlock *block) { - return saveBlock(block, dbase, m_map_compression_level); + // FIXME: serialization happens under mutex + MutexAutoLock dblock(m_db.mutex); + return saveBlock(block, m_db.dbase, m_map_compression_level); } bool ServerMap::saveBlock(MapBlock *block, MapDatabase *db, int compression_level) @@ -634,18 +658,27 @@ bool ServerMap::saveBlock(MapBlock *block, MapDatabase *db, int compression_leve return ret; } -void ServerMap::loadBlock(std::string *blob, v3s16 p3d, MapSector *sector, bool save_after_load) +void ServerMap::deSerializeBlock(MapBlock *block, std::istream &is) { + ScopeProfiler sp(g_profiler, "ServerMap: deSer block", SPT_AVG, PRECISION_MICRO); + + u8 version = readU8(is); + if (is.fail()) + throw SerializationError("Failed to read MapBlock version"); + + block->deSerialize(is, version, true); +} + +MapBlock *ServerMap::loadBlock(const std::string &blob, v3s16 p3d, bool save_after_load) +{ + ScopeProfiler sp(g_profiler, "ServerMap: load block", SPT_AVG, PRECISION_MICRO); + MapBlock *block = nullptr; + bool created_new = false; + try { - std::istringstream is(*blob, std::ios_base::binary); + v2s16 p2d(p3d.X, p3d.Z); + MapSector *sector = createSector(p2d); - u8 version = readU8(is); - - if(is.fail()) - throw SerializationError("ServerMap::loadBlock(): Failed" - " to read MapBlock version"); - - MapBlock *block = nullptr; std::unique_ptr block_created_new; block = sector->getBlockNoCreateNoEx(p3d.Y); if (!block) { @@ -654,31 +687,16 @@ void ServerMap::loadBlock(std::string *blob, v3s16 p3d, MapSector *sector, bool } { - ScopeProfiler sp(g_profiler, "ServerMap: deSer block", SPT_AVG, PRECISION_MICRO); - block->deSerialize(is, version, true); + std::istringstream iss(blob, std::ios_base::binary); + deSerializeBlock(block, iss); } // If it's a new block, insert it to the map if (block_created_new) { sector->insertBlock(std::move(block_created_new)); - ReflowScan scanner(this, m_emerge->ndef); - scanner.scan(block, &m_transforming_liquid); + created_new = true; } - - /* - Save blocks loaded in old format in new format - */ - - //if(version < SER_FMT_VER_HIGHEST_READ || save_after_load) - // Only save if asked to; no need to update version - if(save_after_load) - saveBlock(block); - - // We just loaded it from, so it's up-to-date. - block->resetModified(); - } - catch(SerializationError &e) - { + } catch (SerializationError &e) { errorstream<<"Invalid block data in database" <<" ("<ndef); + scanner.scan(block, &m_transforming_liquid); - std::string ret; - dbase->loadBlock(blockpos, &ret); - if (!ret.empty()) { - loadBlock(&ret, blockpos, createSector(p2d), false); - } else if (dbase_ro) { - dbase_ro->loadBlock(blockpos, &ret); - if (!ret.empty()) { - loadBlock(&ret, blockpos, createSector(p2d), false); - } - } else { - return NULL; - } - - MapBlock *block = getBlockNoCreateNoEx(blockpos); - if (created_new && (block != NULL)) { std::map modified_blocks; // Fix lighting if necessary voxalgo::update_block_border_lighting(this, block, modified_blocks); if (!modified_blocks.empty()) { - //Modified lighting, send event MapEditEvent event; event.type = MEET_OTHER; event.setModifiedBlocks(modified_blocks); dispatchEvent(event); } } + + if (save_after_load) + saveBlock(block); + + // We just loaded it, so it's up-to-date. + block->resetModified(); + return block; } +MapBlock* ServerMap::loadBlock(v3s16 blockpos) +{ + std::string data; + { + ScopeProfiler sp(g_profiler, "ServerMap: load block - sync (sum)"); + MutexAutoLock dblock(m_db.mutex); + m_db.loadBlock(blockpos, data); + } + + if (!data.empty()) + return loadBlock(data, blockpos); + return getBlockNoCreateNoEx(blockpos); +} + bool ServerMap::deleteBlock(v3s16 blockpos) { - if (!dbase->deleteBlock(blockpos)) + MutexAutoLock dblock(m_db.mutex); + if (!m_db.dbase->deleteBlock(blockpos)) return false; MapBlock *block = getBlockNoCreateNoEx(blockpos); diff --git a/src/servermap.h b/src/servermap.h index 7a8a84b9b..3a2102668 100644 --- a/src/servermap.h +++ b/src/servermap.h @@ -33,9 +33,22 @@ class IRollbackManager; class EmergeManager; class ServerEnvironment; struct BlockMakeData; - class MetricsBackend; +// TODO: this could wrap all calls to MapDatabase, including locking +struct MapDatabaseAccessor { + /// Lock, to be taken for any operation + std::mutex mutex; + /// Main database + MapDatabase *dbase = nullptr; + /// Fallback database for read operations + MapDatabase *dbase_ro = nullptr; + + /// Load a block, taking dbase_ro into account. + /// @note call locked + void loadBlock(v3s16 blockpos, std::string &ret); +}; + /* ServerMap @@ -75,7 +88,7 @@ public: MapBlock *createBlock(v3s16 p); /* - Forcefully get a block from somewhere. + Forcefully get a block from somewhere (blocking!). - Memory - Load from disk - Create blank filled with CONTENT_IGNORE @@ -114,9 +127,16 @@ public: bool saveBlock(MapBlock *block) override; static bool saveBlock(MapBlock *block, MapDatabase *db, int compression_level = -1); - MapBlock* loadBlock(v3s16 p); - // Database version - void loadBlock(std::string *blob, v3s16 p3d, MapSector *sector, bool save_after_load=false); + + // Load block in a synchronous fashion + MapBlock *loadBlock(v3s16 p); + /// Load a block that was already read from disk. Used by EmergeManager. + /// @return non-null block (but can be blank) + MapBlock *loadBlock(const std::string &blob, v3s16 p, bool save_after_load=false); + + // Helper for deserializing blocks from disk + // @throws SerializationError + static void deSerializeBlock(MapBlock *block, std::istream &is); // Blocks are removed from the map but not deleted from memory until // deleteDetachedBlocks() is called, since pointers to them may still exist @@ -185,8 +205,8 @@ private: This is reset to false when written on disk. */ bool m_map_metadata_changed = true; - MapDatabase *dbase = nullptr; - MapDatabase *dbase_ro = nullptr; + + MapDatabaseAccessor m_db; // Map metrics MetricGaugePtr m_loaded_blocks_gauge; diff --git a/src/terminal_chat_console.h b/src/terminal_chat_console.h index 1bd226609..7ce2c5c2b 100644 --- a/src/terminal_chat_console.h +++ b/src/terminal_chat_console.h @@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "threading/thread.h" #include "util/container.h" #include "log.h" +#include "log_internal.h" #include #include diff --git a/src/texture_override.cpp b/src/texture_override.cpp index 1b8b4671d..e8386afe2 100644 --- a/src/texture_override.cpp +++ b/src/texture_override.cpp @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "util/string.h" #include #include +#include #define override_cast static_cast diff --git a/src/threading/ordered_mutex.h b/src/threading/ordered_mutex.h new file mode 100644 index 000000000..f7fb4d309 --- /dev/null +++ b/src/threading/ordered_mutex.h @@ -0,0 +1,46 @@ +// Minetest +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include + +/* + Fair mutex based on ticketing approach. + Satisfies `Mutex` C++11 requirements. +*/ +class ordered_mutex { +public: + ordered_mutex() : next_ticket(0), counter(0) {} + + void lock() + { + std::unique_lock autolock(cv_lock); + const auto ticket = next_ticket++; + cv.wait(autolock, [&] { return counter == ticket; }); + } + + bool try_lock() + { + std::lock_guard autolock(cv_lock); + if (counter != next_ticket) + return false; + next_ticket++; + return true; + } + + void unlock() + { + { + std::lock_guard autolock(cv_lock); + counter++; + } + cv.notify_all(); // intentionally outside lock + } + +private: + std::condition_variable cv; + std::mutex cv_lock; + uint_fast32_t next_ticket, counter; +}; diff --git a/src/threading/thread.cpp b/src/threading/thread.cpp index 21143f231..f9e356ab7 100644 --- a/src/threading/thread.cpp +++ b/src/threading/thread.cpp @@ -25,7 +25,7 @@ DEALINGS IN THE SOFTWARE. #include "threading/thread.h" #include "threading/mutex_auto_lock.h" -#include "log.h" +#include "log_internal.h" #include "porting.h" // for setName diff --git a/src/unittest/test.cpp b/src/unittest/test.cpp index 33f8dcbb5..a3b9250a0 100644 --- a/src/unittest/test.cpp +++ b/src/unittest/test.cpp @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "nodedef.h" #include "itemdef.h" #include "dummygamedef.h" +#include "log_internal.h" #include "modchannels.h" #include "util/numeric.h" #include "porting.h" diff --git a/src/unittest/test_filesys.cpp b/src/unittest/test_filesys.cpp index fd25d2de9..e24e79374 100644 --- a/src/unittest/test_filesys.cpp +++ b/src/unittest/test_filesys.cpp @@ -42,6 +42,7 @@ public: void testSafeWriteToFile(); void testCopyFileContents(); void testNonExist(); + void testRecursiveDelete(); }; static TestFileSys g_test_instance; @@ -56,6 +57,7 @@ void TestFileSys::runTests(IGameDef *gamedef) TEST(testSafeWriteToFile); TEST(testCopyFileContents); TEST(testNonExist); + TEST(testRecursiveDelete); } //////////////////////////////////////////////////////////////////////////////// @@ -338,3 +340,32 @@ void TestFileSys::testNonExist() auto ifs = open_ifstream(path.c_str(), false); UASSERT(!ifs.good()); } + +void TestFileSys::testRecursiveDelete() +{ + std::string dirs[2]; + dirs[0] = getTestTempDirectory() + DIR_DELIM "a"; + dirs[1] = dirs[0] + DIR_DELIM "b"; + + std::string files[2] = { + dirs[0] + DIR_DELIM "file1", + dirs[1] + DIR_DELIM "file2" + }; + + for (auto &it : dirs) + fs::CreateDir(it); + for (auto &it : files) + open_ofstream(it.c_str(), false).close(); + + for (auto &it : dirs) + UASSERT(fs::IsDir(it)); + for (auto &it : files) + UASSERT(fs::IsFile(it)); + + UASSERT(fs::RecursiveDelete(dirs[0])); + + for (auto &it : dirs) + UASSERT(!fs::IsDir(it)); + for (auto &it : files) + UASSERT(!fs::IsFile(it)); +} diff --git a/src/unittest/test_irr_gltf_mesh_loader.cpp b/src/unittest/test_irr_gltf_mesh_loader.cpp index 8ab57e590..674f3c0dd 100644 --- a/src/unittest/test_irr_gltf_mesh_loader.cpp +++ b/src/unittest/test_irr_gltf_mesh_loader.cpp @@ -1,14 +1,14 @@ // Minetest // SPDX-License-Identifier: LGPL-2.1-or-later -#include "CSceneManager.h" #include "content/subgames.h" #include "filesys.h" -#include "CReadFile.h" #include "irr_v3d.h" #include "irr_v2d.h" +#include "irr_ptr.h" +#include "ISkinnedMesh.h" #include #include "catch.h" @@ -20,10 +20,16 @@ const auto gamespec = findSubgame("devtest"); if (!gamespec.isValid()) SKIP(); -irr::scene::CSceneManager smgr(nullptr, nullptr, nullptr); -const auto loadMesh = [&smgr](const irr::io::path& filepath) { - irr::io::CReadFile file(filepath); - return smgr.getMesh(&file); +irr::SIrrlichtCreationParameters p; +p.DriverType = video::EDT_NULL; +auto *driver = irr::createDeviceEx(p); +REQUIRE(driver); + +auto *smgr = driver->getSceneManager(); +const auto loadMesh = [&] (const io::path& filepath) { + irr_ptr file(driver->getFileSystem()->createAndOpenFile(filepath)); + REQUIRE(file); + return smgr->getMesh(file.get()); }; const static auto model_stem = gamespec.gamemods_path + @@ -33,21 +39,21 @@ SECTION("error cases") { const static auto invalid_model_path = gamespec.gamemods_path + DIR_DELIM + "gltf" + DIR_DELIM + "invalid" + DIR_DELIM; SECTION("empty gltf file") { - CHECK(loadMesh(invalid_model_path + "empty.gltf") == nullptr); + CHECK(!loadMesh(invalid_model_path + "empty.gltf")); } SECTION("null file pointer") { - CHECK(smgr.getMesh(nullptr) == nullptr); + CHECK(!smgr->getMesh(nullptr)); } SECTION("invalid JSON") { - CHECK(loadMesh(invalid_model_path + "json_missing_brace.gltf") == nullptr); + CHECK(!loadMesh(invalid_model_path + "json_missing_brace.gltf")); } // This is an example of something that should be validated by tiniergltf. SECTION("invalid bufferview bounds") { - CHECK(loadMesh(invalid_model_path + "invalid_bufferview_bounds.gltf") == nullptr); + CHECK(!loadMesh(invalid_model_path + "invalid_bufferview_bounds.gltf")); } } @@ -59,7 +65,7 @@ SECTION("minimal triangle") { model_stem + "triangle_without_indices.gltf"); INFO(path); const auto mesh = loadMesh(path); - REQUIRE(mesh != nullptr); + REQUIRE(mesh); REQUIRE(mesh->getMeshBufferCount() == 1); SECTION("vertex coordinates are correct") { @@ -82,8 +88,11 @@ SECTION("minimal triangle") { } SECTION("blender cube") { - const auto mesh = loadMesh(model_stem + "blender_cube.gltf"); - REQUIRE(mesh != nullptr); + const auto path = GENERATE( + model_stem + "blender_cube.gltf", + model_stem + "blender_cube.glb"); + const auto mesh = loadMesh(path); + REQUIRE(mesh); REQUIRE(mesh->getMeshBufferCount() == 1); SECTION("vertex coordinates are correct") { REQUIRE(mesh->getMeshBuffer(0)->getVertexCount() == 24); @@ -136,7 +145,7 @@ SECTION("blender cube") { SECTION("blender cube scaled") { const auto mesh = loadMesh(model_stem + "blender_cube_scaled.gltf"); - REQUIRE(mesh != nullptr); + REQUIRE(mesh); REQUIRE(mesh->getMeshBufferCount() == 1); SECTION("Scaling is correct") { @@ -157,7 +166,7 @@ SECTION("blender cube scaled") { SECTION("blender cube matrix transform") { const auto mesh = loadMesh(model_stem + "blender_cube_matrix_transform.gltf"); - REQUIRE(mesh != nullptr); + REQUIRE(mesh); REQUIRE(mesh->getMeshBufferCount() == 1); SECTION("Transformation is correct") { @@ -183,7 +192,7 @@ SECTION("blender cube matrix transform") { SECTION("snow man") { const auto mesh = loadMesh(model_stem + "snow_man.gltf"); - REQUIRE(mesh != nullptr); + REQUIRE(mesh); REQUIRE(mesh->getMeshBufferCount() == 3); SECTION("vertex coordinates are correct for all buffers") { @@ -338,7 +347,7 @@ SECTION("snow man") { SECTION("simple sparse accessor") { const auto mesh = loadMesh(model_stem + "simple_sparse_accessor.gltf"); - REQUIRE(mesh != nullptr); + REQUIRE(mesh); const auto *vertices = reinterpret_cast( mesh->getMeshBuffer(0)->getVertices()); const std::array expectedPositions = { @@ -363,4 +372,91 @@ SECTION("simple sparse accessor") CHECK(vertices[i].Pos == expectedPositions[i]); } +// https://github.com/KhronosGroup/glTF-Sample-Models/tree/main/2.0/SimpleSkin +SECTION("simple skin") +{ + using ISkinnedMesh = irr::scene::ISkinnedMesh; + const auto mesh = loadMesh(model_stem + "simple_skin.gltf"); + REQUIRE(mesh != nullptr); + auto csm = dynamic_cast(mesh); + const auto joints = csm->getAllJoints(); + REQUIRE(joints.size() == 3); + + const auto findJoint = [&](const std::function &predicate) { + for (std::size_t i = 0; i < joints.size(); ++i) { + if (predicate(joints[i])) { + return joints[i]; + } + } + throw std::runtime_error("joint not found"); + }; + + // Check the node hierarchy + const auto parent = findJoint([](auto joint) { + return !joint->Children.empty(); + }); + REQUIRE(parent->Children.size() == 1); + const auto child = parent->Children[0]; + REQUIRE(child != parent); + + SECTION("transformations are correct") + { + CHECK(parent->Animatedposition == v3f(0, 0, 0)); + CHECK(parent->Animatedrotation == irr::core::quaternion()); + CHECK(parent->Animatedscale == v3f(1, 1, 1)); + CHECK(parent->GlobalInversedMatrix == irr::core::matrix4()); + const v3f childTranslation(0, 1, 0); + CHECK(child->Animatedposition == childTranslation); + CHECK(child->Animatedrotation == irr::core::quaternion()); + CHECK(child->Animatedscale == v3f(1, 1, 1)); + irr::core::matrix4 inverseBindMatrix; + inverseBindMatrix.setInverseTranslation(childTranslation); + CHECK(child->GlobalInversedMatrix == inverseBindMatrix); + } + + SECTION("weights are correct") + { + const auto weights = [&](const ISkinnedMesh::SJoint *joint) { + std::unordered_map weights; + for (std::size_t i = 0; i < joint->Weights.size(); ++i) { + const auto weight = joint->Weights[i]; + REQUIRE(weight.buffer_id == 0); + weights[weight.vertex_id] = weight.strength; + } + return weights; + }; + const auto parentWeights = weights(parent); + const auto childWeights = weights(child); + + const auto checkWeights = [&](irr::u32 index, irr::f32 parentWeight, irr::f32 childWeight) { + const auto getWeight = [](auto weights, auto index) { + const auto it = weights.find(index); + return it == weights.end() ? 0.0f : it->second; + }; + CHECK(getWeight(parentWeights, index) == parentWeight); + CHECK(getWeight(childWeights, index) == childWeight); + }; + checkWeights(0, 1.00, 0.00); + checkWeights(1, 1.00, 0.00); + checkWeights(2, 0.75, 0.25); + checkWeights(3, 0.75, 0.25); + checkWeights(4, 0.50, 0.50); + checkWeights(5, 0.50, 0.50); + checkWeights(6, 0.25, 0.75); + checkWeights(7, 0.25, 0.75); + checkWeights(8, 0.00, 1.00); + checkWeights(9, 0.00, 1.00); + } + + SECTION("there should be a third node not involved in skinning") + { + const auto other = findJoint([&](auto joint) { + return joint != child && joint != parent; + }); + CHECK(other->Weights.empty()); + } +} + +driver->closeDevice(); +driver->drop(); } diff --git a/src/unittest/test_servermodmanager.cpp b/src/unittest/test_servermodmanager.cpp index f26734ab3..03fdc7042 100644 --- a/src/unittest/test_servermodmanager.cpp +++ b/src/unittest/test_servermodmanager.cpp @@ -122,7 +122,7 @@ void TestServerModManager::testGetMods() ServerModManager sm(m_worlddir); const auto &mods = sm.getMods(); // `ls ./games/devtest/mods | wc -l` + 1 (test mod) - UASSERTEQ(std::size_t, mods.size(), 32 + 1); + UASSERTEQ(std::size_t, mods.size(), 33 + 1); // Ensure we found basenodes mod (part of devtest) // and test_mod (for testing MINETEST_MOD_PATH). diff --git a/src/util/colorize.cpp b/src/util/colorize.cpp index 873ec06fc..0814c2d34 100644 --- a/src/util/colorize.cpp +++ b/src/util/colorize.cpp @@ -23,6 +23,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #include "log.h" #include "string.h" #include +#include std::string colorize_url(const std::string &url) { diff --git a/src/util/timetaker.cpp b/src/util/timetaker.cpp index a18d813ba..47d8ab83a 100644 --- a/src/util/timetaker.cpp +++ b/src/util/timetaker.cpp @@ -35,7 +35,7 @@ u64 TimeTaker::stop(bool quiet) if (m_result != nullptr) { (*m_result) += dtime; } else { - if (!quiet) { + if (!quiet && !m_name.empty()) { infostream << m_name << " took " << dtime << TimePrecision_units[m_precision] << std::endl; } diff --git a/textures/base/pack/button_hover_semitrans.png b/textures/base/pack/button_hover_semitrans.png new file mode 100644 index 000000000..5cf294ead Binary files /dev/null and b/textures/base/pack/button_hover_semitrans.png differ diff --git a/textures/base/pack/button_press_semitrans.png b/textures/base/pack/button_press_semitrans.png new file mode 100644 index 000000000..ba0ddd510 Binary files /dev/null and b/textures/base/pack/button_press_semitrans.png differ diff --git a/textures/base/pack/cdb_add.png b/textures/base/pack/cdb_add.png deleted file mode 100644 index 3e3d067e3..000000000 Binary files a/textures/base/pack/cdb_add.png and /dev/null differ diff --git a/textures/base/pack/cdb_clear.png b/textures/base/pack/cdb_clear.png deleted file mode 100644 index d5df4a067..000000000 Binary files a/textures/base/pack/cdb_clear.png and /dev/null differ diff --git a/textures/base/pack/cdb_viewonline.png b/textures/base/pack/cdb_viewonline.png deleted file mode 100644 index ae2a146b8..000000000 Binary files a/textures/base/pack/cdb_viewonline.png and /dev/null differ diff --git a/util/bump_version.sh b/util/bump_version.sh index 699bbcf77..5e920dc70 100755 --- a/util/bump_version.sh +++ b/util/bump_version.sh @@ -43,7 +43,12 @@ read_versions() { # in: $1 read_proto_ver() { local ref=$1 - git show "$ref":src/network/networkprotocol.h | grep -oE 'LATEST_PROTOCOL_VERSION [0-9]+' | tr -dC 0-9 + local output=$(git show "$ref":src/network/networkprotocol.cpp 2>/dev/null) + if [ -z "$output" ]; then + # Fallback to previous file (for tags < 5.10.0) + output=$(git show "$ref":src/network/networkprotocol.h) + fi + grep -oE 'LATEST_PROTOCOL_VERSION\s+=?\s*[0-9]+' <<<"$output" | tr -dC 0-9 } ## Prompts for new version diff --git a/util/helper_mod/error.lua b/util/helper_mod/error.lua new file mode 100644 index 000000000..2a4b3e355 --- /dev/null +++ b/util/helper_mod/error.lua @@ -0,0 +1 @@ +error("intentional") diff --git a/util/helper_mod/init.lua b/util/helper_mod/init.lua index 4da832ed7..b2ed3b29d 100644 --- a/util/helper_mod/init.lua +++ b/util/helper_mod/init.lua @@ -48,4 +48,24 @@ elseif mode == "mapgen" then end core.after(0, next_, 1) +elseif mode == "error" then + + local n = tonumber(core.settings:get("error_type")) + local error_lua = core.get_modpath(core.get_current_modname()) .. "/error.lua" + if n == 1 then + print("=> error during startup <=") + error("intentional") + elseif n == 2 then + print("=> error on first step <=") + core.after(0, error, "intentional") + elseif n == 3 then + print("=> error in async script <=") + core.register_async_dofile(error_lua) + elseif n == 4 then + print("=> error in mapgen script <=") + core.register_mapgen_script(error_lua) + else + assert(false) + end + end diff --git a/util/test_error_cases.sh b/util/test_error_cases.sh new file mode 100755 index 000000000..801e097a3 --- /dev/null +++ b/util/test_error_cases.sh @@ -0,0 +1,46 @@ +#!/bin/bash +dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +gameid=${gameid:-devtest} +minetest=$dir/../bin/minetest +testspath=$dir/../tests +conf_server=$testspath/server.conf +worldpath=$testspath/world + +[ -e "$minetest" ] || { echo "executable $minetest missing"; exit 1; } + +write_config () { + printf '%s\n' >"$conf_server" \ + helper_mode=error mg_name=singlenode "$@" +} + +run () { + timeout 10 "$@" + r=$? + echo "Exit status: $r" + [ $r -eq 124 ] && echo "(timed out)" + if [ $r -ne 1 ]; then + echo "-> Test failed" + exit 1 + fi +} + +rm -rf "$worldpath" +mkdir -p "$worldpath/worldmods" + +ln -s "$dir/helper_mod" "$worldpath/worldmods/" + +args=(--server --config "$conf_server" --world "$worldpath" --gameid $gameid) + +# make sure we can tell apart sanitizer and minetest errors +export ASAN_OPTIONS="exitcode=42" +export MSAN_OPTIONS="exitcode=42" + +# see helper_mod/init.lua for the different types +for n in $(seq 1 4); do + write_config error_type=$n + run "$minetest" "${args[@]}" + echo "---------------" +done + +echo "All done." +exit 0