diff --git a/.editorconfig b/.editorconfig index ec0645241b998..e273afa920aa8 100755 --- a/.editorconfig +++ b/.editorconfig @@ -2,7 +2,7 @@ end_of_line = lf [*.{cpp,h,lua,txt,glsl,md,c,cmake,java,gradle}] -charset = utf8 +charset = utf-8 indent_size = 4 indent_style = tab insert_final_newline = true diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 835d6f564308e..0000000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: Unconfirmed bug -assignees: '' ---- - -##### Minetest version - -``` - -``` - - -Active renderer: -Irrlicht device: - -##### OS / Hardware - -Operating system: -CPU: - - -GPU model: -OpenGL version: - -##### Summary - - -##### Steps to reproduce - diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 0000000000000..24cee5aaedf6a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,91 @@ +name: Bug report +description: Create a report to help us improve +labels: ["Unconfirmed bug"] +body: + - type: markdown + attributes: + value: | + Please note the following: + 1. **Please update your Minetest Engine to the latest stable or dev version** before submitting bug reports. Make sure the bug is still reproducible on the latest version. + 2. This page is for reporting the bugs of **the engine itself**. For bugs in a particular game, please [search for the game in the ContentDB](https://content.minetest.net/packages/?type=game) and submit a bug report in their issue trackers. + * For example, you can submit issues about the Minetest Game (the official game of Minetest) [in its own repository](https://github.com/minetest/minetest_game/issues). + 3. Please provide as many details as possible for us to spot the problem quicker. + - type: textarea + attributes: + label: Minetest version + description: | + Paste the Minetest version below. + If you are on a devel version, please add a git commit hash. + You can use `minetest --version` to find it. + You can also refer to the "About" tab of the menu. + placeholder: | + Example: + Minetest 5.7.0-dev-ca13c51 (Linux) + Using Irrlicht 1.9.0mt9 + Using LuaJIT 2.1.0-beta3 + BUILD_TYPE=Release + RUN_IN_PLACE=1 + USE_CURL=1 + USE_GETTEXT=1 + USE_SOUND=1 + STATIC_SHAREDIR="." + STATIC_LOCALEDIR="locale" + render: "true" + validations: + required: true + - type: input + attributes: + label: Active renderer + description: For graphical and input-related issues. You can find these in the About tab in the mainmenu. + placeholder: "Example: OpenGL 4.6.0" + validations: + required: false + - type: input + attributes: + label: Irrlicht device + description: + placeholder: "Example: X11" + validations: + required: false + - type: input + attributes: + label: Operating system and version + description: It is recommended to upgrade your operating system to see if the problem still exists. + placeholder: "Example: Ubuntu 22.04" + validations: + required: true + - type: input + attributes: + label: CPU model + description: Usually found in system settings. + placeholder: "Example: Intel i5-2410M (4) @ 2.900GHz" + validations: + required: false + - type: markdown + attributes: + value: The GPU model and OpenGL version can be omitted if the bug is not a graphical issue. + - type: input + attributes: + label: GPU model + description: Usually found in system settings. + placeholder: "Example: NVIDA GeForce RTX 4090" + validations: + required: false + - type: input + attributes: + label: OpenGL version + placeholder: "Example: 4.6" + validations: + required: false + - type: textarea + attributes: + label: Summary + description: Describe your problem here. + validations: + required: true + - type: textarea + attributes: + label: Steps to reproduce + description: Explain how the problem has happened, providing a minimal test (i.e. a code snippet reduced to the bone) where possible. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000000..414391773aa72 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Submit issues about Minetest Game + url: https://github.com/minetest/minetest_game/issues/new/choose + about: Only submit issues of the engine in this repository's issue tracker. Submit those of Minetest Game in its own issue tracker. + - name: Search for issue trackers of third-party games + url: https://content.minetest.net/packages/?type=game + about: For issues of third-party games, search for the game in the ContentDB and then submit an issue in their issue tracker. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index ebcfa98eecdeb..0000000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: Feature request -assignees: '' ---- - -## Problem - -A clear and concise description of what the problem is. -ie: Why is this needed? -Ex. I'm always frustrated when [...] - -## Solutions - -A clear and concise description of what you want to happen. - -## Alternatives - -A clear and concise description of any alternative solutions or features you've considered. - -## Additional context - -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 0000000000000..5a16b24fe314c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,39 @@ +name: Feature request +description: Suggest an idea for this project +labels: ["Feature request"] +body: + - type: markdown + attributes: + value: | + Please note the following: + 1. Only submit a feature request if the feature does not exist on the latest dev version. + 2. This page is for suggesting changes to **the engine itself**. To suggest changes to games, please [search for the game in the ContentDB](https://content.minetest.net/packages/?type=game) and submit a feature request in their issue trackers. + - type: textarea + attributes: + label: Problem + description: | + A clear and concise description of the problem, i.e. "Why is this needed?" + Example: I'm always frustrated when [...] + validations: + required: true + - type: textarea + attributes: + label: Solutions + description: | + A clear and concise description of what you want to happen. + validations: + required: true + - type: textarea + attributes: + label: Alternatives + description: | + A clear and concise description of any alternative solutions or features you've considered. + validations: + required: true + - type: textarea + attributes: + label: Additional context + description: | + Add any other context or screenshots about the feature request here. + validations: + required: false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f2a03c22dad71..74ea839ae7558 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -76,21 +76,21 @@ jobs: ./bin/minetest --run-unittests # Older clang version (should be close to our minimum supported version) - clang_6_0: + clang_7: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3 - name: Install deps run: | source ./util/ci/common.sh - install_linux_deps clang-6.0 valgrind + install_linux_deps clang-7 valgrind - name: Build run: | ./util/ci/build.sh env: - CC: clang-6.0 - CXX: clang++-6.0 + CC: clang-7 + CXX: clang++-7 - name: Unittest run: | diff --git a/.github/workflows/lua_api_deploy.yml b/.github/workflows/lua_api_deploy.yml new file mode 100644 index 0000000000000..574e3ba3e5661 --- /dev/null +++ b/.github/workflows/lua_api_deploy.yml @@ -0,0 +1,48 @@ +name: lua_api_deploy + +permissions: + contents: read + pages: write + id-token: write + +on: + push: + paths: + - '.github/workflows/lua_api_deploy.yml' + - 'doc/lua_api.md' + - 'doc/mkdocs/' + branches: + - master + +jobs: + build: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.11 + + - name: Install mkdocs + run: | + pip install -U -r doc/mkdocs/requirements.txt + + - name: Build documentation + run: | + cd doc/mkdocs/ + ./build.sh + + - name: Setup Pages + uses: actions/configure-pages@v3 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v2 + with: + path: 'public/' + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v2 diff --git a/.luacheckrc b/.luacheckrc index b048e41e478a4..54ece656a35a3 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -81,3 +81,9 @@ files["builtin/common/tests"] = { "assert", }, } + +files["builtin/fstk"] = { + read_globals = { + "TOUCHSCREEN_GUI", + }, +} diff --git a/CNAME b/CNAME new file mode 100644 index 0000000000000..c8f58d46942cb --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +api.minetest.net diff --git a/LICENSE.txt b/LICENSE.txt index a171e4052c0ce..d1270c4b53077 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -62,6 +62,7 @@ Zughy: textures/base/pack/cdb_queued.png textures/base/pack/cdb_update.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 diff --git a/builtin/async/game.lua b/builtin/async/game.lua index 0b7a7ef0e4a34..f7c9892c4670b 100644 --- a/builtin/async/game.lua +++ b/builtin/async/game.lua @@ -36,11 +36,16 @@ do setmetatable(v, {__newindex = {}}) -- Reassemble the other tables if v.type == "node" then + getmetatable(v).__index = all.nodedef_default all.registered_nodes[k] = v - elseif v.type == "craftitem" then + elseif v.type == "craft" then + getmetatable(v).__index = all.craftitemdef_default all.registered_craftitems[k] = v elseif v.type == "tool" then + getmetatable(v).__index = all.tooldef_default all.registered_tools[k] = v + else + getmetatable(v).__index = all.noneitemdef_default end end diff --git a/builtin/fstk/buttonbar.lua b/builtin/fstk/buttonbar.lua index 465588324a4ae..effd0eba7bb8e 100644 --- a/builtin/fstk/buttonbar.lua +++ b/builtin/fstk/buttonbar.lua @@ -1,5 +1,6 @@ --Minetest --Copyright (C) 2014 sapier +--Copyright (C) 2023 Gregor Parzefall -- --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 @@ -16,115 +17,101 @@ --51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -local function buttonbar_formspec(self) +local BASE_SPACING = 0.1 +local SCROLL_BTN_WIDTH = TOUCHSCREEN_GUI and 0.8 or 0.5 +local function buttonbar_formspec(self) if self.hidden then return "" end - local formspec = string.format("box[%f,%f;%f,%f;%s]", - self.pos.x,self.pos.y ,self.size.x,self.size.y,self.bgcolor) + local formspec = { + "style_type[box;noclip=true]", + string.format("box[%f,%f;%f,%f;%s]", self.pos.x, self.pos.y, self.size.x, + self.size.y, self.bgcolor), + "style_type[box;noclip=false]", + } - for i=self.startbutton,#self.buttons,1 do - local btn_name = self.buttons[i].name - local btn_pos = {} + local btn_size = self.size.y - 2*BASE_SPACING - if self.orientation == "horizontal" then - btn_pos.x = self.pos.x + --base pos - (i - self.startbutton) * self.btn_size + --button offset - self.btn_initial_offset - else - btn_pos.x = self.pos.x + (self.btn_size * 0.05) - end + -- Spacing works like CSS Flexbox with `justify-content: space-evenly;`. + -- `BASE_SPACING` is used as the minimum spacing, like `gap` in CSS Flexbox. - if self.orientation == "vertical" then - btn_pos.y = self.pos.y + --base pos - (i - self.startbutton) * self.btn_size + --button offset - self.btn_initial_offset - else - btn_pos.y = self.pos.y + (self.btn_size * 0.05) - end + -- The number of buttons per page is always calculated as if the scroll + -- buttons were visible. + local avail_space = self.size.x - 2*BASE_SPACING - 2*SCROLL_BTN_WIDTH + local btns_per_page = math.floor((avail_space - BASE_SPACING) / (btn_size + BASE_SPACING)) - if (self.orientation == "vertical" and - (btn_pos.y + self.btn_size <= self.pos.y + self.size.y)) or - (self.orientation == "horizontal" and - (btn_pos.x + self.btn_size <= self.pos.x + self.size.x)) then + self.num_pages = math.ceil(#self.buttons / btns_per_page) + self.cur_page = math.min(self.cur_page, self.num_pages) + local first_btn = (self.cur_page - 1) * btns_per_page + 1 - local borders="true" + local show_scroll_btns = self.num_pages > 1 - if self.buttons[i].image ~= nil then - borders="false" - end + -- In contrast, the button spacing calculation takes hidden scroll buttons + -- into account. + local real_avail_space = show_scroll_btns and avail_space or self.size.x + local btn_spacing = (real_avail_space - btns_per_page * btn_size) / (btns_per_page + 1) - formspec = formspec .. - string.format("image_button[%f,%f;%f,%f;%s;%s;%s;true;%s]tooltip[%s;%s]", - btn_pos.x, btn_pos.y, self.btn_size, self.btn_size, - self.buttons[i].image, btn_name, self.buttons[i].caption, - borders, btn_name, self.buttons[i].tooltip) - else - --print("end of displayable buttons: orientation: " .. self.orientation) - --print( "button_end: " .. (btn_pos.y + self.btn_size - (self.btn_size * 0.05))) - --print( "bar_end: " .. (self.pos.x + self.size.x)) - break - end + local btn_start_x = self.pos.x + btn_spacing + if show_scroll_btns then + btn_start_x = btn_start_x + BASE_SPACING + SCROLL_BTN_WIDTH end - if (self.have_move_buttons) then - local btn_dec_pos = {} - btn_dec_pos.x = self.pos.x + (self.btn_size * 0.05) - btn_dec_pos.y = self.pos.y + (self.btn_size * 0.05) - local btn_inc_pos = {} - local btn_size = {} - - if self.orientation == "horizontal" then - btn_size.x = 0.5 - btn_size.y = self.btn_size - btn_inc_pos.x = self.pos.x + self.size.x - 0.5 - btn_inc_pos.y = self.pos.y + (self.btn_size * 0.05) - else - btn_size.x = self.btn_size - btn_size.y = 0.5 - btn_inc_pos.x = self.pos.x + (self.btn_size * 0.05) - btn_inc_pos.y = self.pos.y + self.size.y - 0.5 + for i = first_btn, first_btn + btns_per_page - 1 do + local btn = self.buttons[i] + if btn == nil then + break end - local text_dec = "<" - local text_inc = ">" - if self.orientation == "vertical" then - text_dec = "^" - text_inc = "v" - end + local btn_pos = { + x = btn_start_x + (i - first_btn) * (btn_size + btn_spacing), + y = self.pos.y + BASE_SPACING, + } - formspec = formspec .. - string.format("image_button[%f,%f;%f,%f;;btnbar_dec_%s;%s;true;true]", - btn_dec_pos.x, btn_dec_pos.y, btn_size.x, btn_size.y, - self.name, text_dec) + table.insert(formspec, string.format("image_button[%f,%f;%f,%f;%s;%s;%s;true;false]tooltip[%s;%s]", + btn_pos.x, btn_pos.y, btn_size, btn_size, btn.image, btn.name, + btn.caption, btn.name, btn.tooltip)) + end - formspec = formspec .. - string.format("image_button[%f,%f;%f,%f;;btnbar_inc_%s;%s;true;true]", - btn_inc_pos.x, btn_inc_pos.y, btn_size.x, btn_size.y, - self.name, text_inc) + if show_scroll_btns then + local btn_prev_pos = { + x = self.pos.x + BASE_SPACING, + y = self.pos.y + BASE_SPACING, + } + local btn_next_pos = { + x = self.pos.x + self.size.x - BASE_SPACING - SCROLL_BTN_WIDTH, + y = self.pos.y + BASE_SPACING, + } + + table.insert(formspec, string.format("style[%s,%s;noclip=true]", + self.btn_prev_name, self.btn_next_name)) + + table.insert(formspec, string.format("button[%f,%f;%f,%f;%s;<]", + btn_prev_pos.x, btn_prev_pos.y, SCROLL_BTN_WIDTH, btn_size, + self.btn_prev_name)) + + table.insert(formspec, string.format("button[%f,%f;%f,%f;%s;>]", + btn_next_pos.x, btn_next_pos.y, SCROLL_BTN_WIDTH, btn_size, + self.btn_next_name)) end - return formspec + return table.concat(formspec) end local function buttonbar_buttonhandler(self, fields) - - if fields["btnbar_inc_" .. self.name] ~= nil and - self.startbutton < #self.buttons then - - self.startbutton = self.startbutton + 1 + if fields[self.btn_prev_name] and self.cur_page > 1 then + self.cur_page = self.cur_page - 1 return true end - if fields["btnbar_dec_" .. self.name] ~= nil and self.startbutton > 1 then - self.startbutton = self.startbutton - 1 + if fields[self.btn_next_name] and self.cur_page < self.num_pages then + self.cur_page = self.cur_page + 1 return true end - for i=1,#self.buttons,1 do - if fields[self.buttons[i].name] ~= nil then + for _, btn in ipairs(self.buttons) do + if fields[btn.name] then return self.userbuttonhandler(fields) end end @@ -141,74 +128,45 @@ local buttonbar_metatable = { delete = function(self) ui.delete(self) end, add_button = function(self, name, caption, image, tooltip) - if caption == nil then caption = "" end - if image == nil then image = "" end - if tooltip == nil then tooltip = "" end - - self.buttons[#self.buttons + 1] = { - name = name, - caption = caption, - image = image, - tooltip = tooltip - } - if self.orientation == "horizontal" then - if ( (self.btn_size * #self.buttons) + (self.btn_size * 0.05 *2) - > self.size.x ) then - - self.btn_initial_offset = self.btn_size * 0.05 + 0.5 - self.have_move_buttons = true - end - else - if ((self.btn_size * #self.buttons) + (self.btn_size * 0.05 *2) - > self.size.y ) then - - self.btn_initial_offset = self.btn_size * 0.05 + 0.5 - self.have_move_buttons = true - end - end - end, - - set_bgparams = function(self, bgcolor) - if (type(bgcolor) == "string") then - self.bgcolor = bgcolor - end - end, + if caption == nil then caption = "" end + if image == nil then image = "" end + if tooltip == nil then tooltip = "" end + + table.insert(self.buttons, { + name = name, + caption = caption, + image = image, + tooltip = tooltip, + }) + end, } buttonbar_metatable.__index = buttonbar_metatable -function buttonbar_create(name, cbf_buttonhandler, pos, orientation, size) - assert(name ~= nil) - assert(cbf_buttonhandler ~= nil) - assert(orientation == "vertical" or orientation == "horizontal") - assert(pos ~= nil and type(pos) == "table") - assert(size ~= nil and type(size) == "table") +function buttonbar_create(name, pos, size, bgcolor, cbf_buttonhandler) + assert(type(name) == "string" ) + assert(type(pos) == "table" ) + assert(type(size) == "table" ) + assert(type(bgcolor) == "string" ) + assert(type(cbf_buttonhandler) == "function") local self = {} - self.name = name self.type = "addon" - self.bgcolor = "#000000" + self.name = name self.pos = pos self.size = size - self.orientation = orientation - self.startbutton = 1 - self.have_move_buttons = false - self.hidden = false - - if self.orientation == "horizontal" then - self.btn_size = self.size.y - else - self.btn_size = self.size.x - end - - if (self.btn_initial_offset == nil) then - self.btn_initial_offset = self.btn_size * 0.05 - end - + self.bgcolor = bgcolor self.userbuttonhandler = cbf_buttonhandler + + self.hidden = false self.buttons = {} + self.num_pages = 1 + self.cur_page = 1 + + self.btn_prev_name = "btnbar_prev_" .. self.name + self.btn_next_name = "btnbar_next_" .. self.name - setmetatable(self,buttonbar_metatable) + setmetatable(self, buttonbar_metatable) ui.add(self) return self diff --git a/builtin/fstk/tabview.lua b/builtin/fstk/tabview.lua index 63c205d6e5744..4d1a74eb6fae1 100644 --- a/builtin/fstk/tabview.lua +++ b/builtin/fstk/tabview.lua @@ -42,7 +42,7 @@ local function add_tab(self,tab) event_handler = tab.cbf_events, get_formspec = tab.cbf_formspec, tabsize = tab.tabsize, - formspec_version = tab.formspec_version, + formspec_version = tab.formspec_version or 6, on_change = tab.on_change, tabdata = {}, } @@ -66,8 +66,8 @@ local function get_formspec(self) local content, prepend = tab.get_formspec(self, tab.name, tab.tabdata, tab.tabsize) + local tsize = tab.tabsize or { width = self.width, height = self.height } if self.parent == nil and not prepend then - local tsize = tab.tabsize or {width=self.width, height=self.height} prepend = string.format("size[%f,%f,%s]", tsize.width, tsize.height, dump(self.fixed_size)) @@ -76,7 +76,28 @@ local function get_formspec(self) end end - local formspec = (prepend or "") .. self:tab_header() .. content + local end_button_size = 0.75 + + local tab_header_size = { width = tsize.width, height = 0.85 } + if self.end_button then + tab_header_size.width = tab_header_size.width - end_button_size - 0.1 + end + + local formspec = (prepend or "") .. self:tab_header(tab_header_size) .. content + + if self.end_button then + formspec = formspec .. + ("style[%s;noclip=true;border=false]"):format(self.end_button.name) .. + ("tooltip[%s;%s]"):format(self.end_button.name, self.end_button.label) .. + ("image_button[%f,%f;%f,%f;%s;%s;]"):format( + self.width - end_button_size, + (-tab_header_size.height - end_button_size) / 2, + end_button_size, + end_button_size, + core.formspec_escape(self.end_button.icon), + self.end_button.name) + end + return formspec end @@ -91,8 +112,12 @@ local function handle_buttons(self,fields) return true end + if self.end_button and fields[self.end_button.name] then + return self.end_button.on_click(self) + end + if self.glb_btn_handler ~= nil and - self.glb_btn_handler(self,fields) then + self.glb_btn_handler(self, fields) then return true end @@ -126,8 +151,7 @@ end -------------------------------------------------------------------------------- -local function tab_header(self) - +local function tab_header(self, size) local toadd = "" for i=1,#self.tablist,1 do @@ -138,8 +162,8 @@ local function tab_header(self) toadd = toadd .. self.tablist[i].caption end - return string.format("tabheader[%f,%f;%s;%s;%i;true;false]", - self.header_x, self.header_y, self.name, toadd, self.last_tab_index); + return string.format("tabheader[%f,%f;%f,%f;%s;%s;%i;true;false]", + self.header_x, self.header_y, size.width, size.height, self.name, toadd, self.last_tab_index) end -------------------------------------------------------------------------------- @@ -230,6 +254,8 @@ local tabview_metatable = { function(self,handler) self.glb_evt_handler = handler end, set_fixed_size = function(self,state) self.fixed_size = state end, + set_end_button = + function(self, v) self.end_button = v end, tab_header = tab_header, handle_tab_buttons = handle_tab_buttons } diff --git a/builtin/game/falling.lua b/builtin/game/falling.lua index f9d71c90a1cf4..717c0b748417a 100644 --- a/builtin/game/falling.lua +++ b/builtin/game/falling.lua @@ -529,16 +529,16 @@ function core.check_single_for_falling(p) if same and d_bottom.paramtype2 == "leveled" and core.get_node_level(p_bottom) < core.get_node_max_level(p_bottom) then - convert_to_falling_node(p, n) - return true + local success, _ = convert_to_falling_node(p, n) + return success end -- Otherwise only if the bottom node is considered "fall through" if not same and (not d_bottom.walkable or d_bottom.buildable_to) and (core.get_item_group(n.name, "float") == 0 or d_bottom.liquidtype == "none") then - convert_to_falling_node(p, n) - return true + local success, _ = convert_to_falling_node(p, n) + return success end end end diff --git a/builtin/game/item_entity.lua b/builtin/game/item_entity.lua index 624fd4b11b38a..b7b291c435d1c 100644 --- a/builtin/game/item_entity.lua +++ b/builtin/game/item_entity.lua @@ -69,6 +69,7 @@ core.register_entity(":__builtin:item", { automatic_rotate = math.pi * 0.5 * 0.2 / size, wield_item = self.itemstring, glow = glow, + infotext = stack:get_description(), }) -- cache for usage in on_step diff --git a/builtin/game/misc.lua b/builtin/game/misc.lua index fd0cacc8f8e7d..a30c42fa0fee6 100644 --- a/builtin/game/misc.lua +++ b/builtin/game/misc.lua @@ -261,6 +261,11 @@ function core.get_globals_to_transfer() local all = { registered_items = copy_filtering(core.registered_items), registered_aliases = core.registered_aliases, + + nodedef_default = copy_filtering(core.nodedef_default), + craftitemdef_default = copy_filtering(core.craftitemdef_default), + tooldef_default = copy_filtering(core.tooldef_default), + noneitemdef_default = copy_filtering(core.noneitemdef_default), } return all end diff --git a/builtin/mainmenu/dlg_contentstore.lua b/builtin/mainmenu/dlg_contentstore.lua index e32da250174aa..b66e98d57e25e 100644 --- a/builtin/mainmenu/dlg_contentstore.lua +++ b/builtin/mainmenu/dlg_contentstore.lua @@ -23,9 +23,18 @@ if not core.get_http_api then return end --- Unordered preserves the original order of the ContentDB API, --- before the package list is ordered based on installed state. -local store = { packages = {}, packages_full = {}, packages_full_unordered = {}, aliases = {} } +local store = { + loading = false, + load_ok = false, + load_error = false, + + -- Unordered preserves the original order of the ContentDB API, + -- before the package list is ordered based on installed state. + packages = {}, + packages_full = {}, + packages_full_unordered = {}, + aliases = {}, +} local http = core.get_http_api() @@ -65,7 +74,7 @@ local REASON_DEPENDENCY = "dependency" -- encodes for use as URL parameter or path component local function urlencode(str) return str:gsub("[^%a%d()._~-]", function(char) - return string.format("%%%02X", string.byte(char)) + return ("%%%02X"):format(char:byte()) end) end assert(urlencode("sample text?") == "sample%20text%3F") @@ -432,7 +441,7 @@ function install_dialog.get_formspec() "container_end[]", } - return table.concat(formspec, "") + return table.concat(formspec) end function install_dialog.handle_submit(this, fields) @@ -577,29 +586,33 @@ local function get_screenshot(package) return defaulttexturedir .. "loading_screenshot.png" end -function store.load() +local function fetch_pkgs(param) local version = core.get_version() local base_url = core.settings:get("contentdb_url") local url = base_url .. "/api/packages/?type=mod&type=game&type=txp&protocol_version=" .. - core.get_max_supp_proto() .. "&engine_version=" .. urlencode(version.string) + core.get_max_supp_proto() .. "&engine_version=" .. param.urlencode(version.string) for _, item in pairs(core.settings:get("contentdb_flag_blacklist"):split(",")) do item = item:trim() if item ~= "" then - url = url .. "&hide=" .. urlencode(item) + url = url .. "&hide=" .. param.urlencode(item) end end + local http = core.get_http_api() local response = http.fetch_sync({ url = url }) if not response.succeeded then return end - store.packages_full = core.parse_json(response.data) or {} - store.aliases = {} + local packages = core.parse_json(response.data) + if not packages or #packages == 0 then + return + end + local aliases = {} - for _, package in pairs(store.packages_full) do + for _, package in pairs(packages) do local name_len = #package.name -- This must match what store.update_paths() does! package.id = package.author:lower() .. "/" @@ -609,22 +622,58 @@ function store.load() package.id = package.id .. package.name end - package.url_part = urlencode(package.author) .. "/" .. urlencode(package.name) + package.url_part = param.urlencode(package.author) .. "/" .. param.urlencode(package.name) if package.aliases then for _, alias in ipairs(package.aliases) do -- We currently don't support name changing local suffix = "/" .. package.name if alias:sub(-#suffix) == suffix then - store.aliases[alias:lower()] = package.id + aliases[alias:lower()] = package.id end end end end - store.packages_full_unordered = store.packages_full - store.packages = store.packages_full - store.loaded = true + return { packages = packages, aliases = aliases } +end + +local function sort_and_filter_pkgs() + store.update_paths() + store.sort_packages() + store.filter_packages(search_string) +end + +function store.load() + if store.load_ok then + sort_and_filter_pkgs() + return + end + if store.loading then + return + end + store.loading = true + core.handle_async( + fetch_pkgs, + { urlencode = urlencode }, + function(result) + if result then + store.packages = result.packages + store.packages_full = result.packages + store.packages_full_unordered = result.packages + store.aliases = result.aliases + sort_and_filter_pkgs() + + store.load_ok = true + store.load_error = false + else + store.load_error = true + end + + store.loading = false + core.event_handler("Refresh") + end + ) end function store.update_paths() @@ -684,8 +733,16 @@ function store.sort_packages() end end - -- Sort installed content by title + -- Sort installed content first by "is there an update available?", then by title table.sort(ret, function(a, b) + local a_updatable = a.installed_release < a.release + local b_updatable = b.installed_release < b.release + if a_updatable and not b_updatable then + return true + elseif b_updatable and not a_updatable then + return false + end + return a.title < b.title end) @@ -735,7 +792,29 @@ function store.filter_packages(query) end end +local function get_info_formspec(text) + local H = 9.5 + return table.concat({ + "formspec_version[6]", + "size[15.75,9.5]", + TOUCHSCREEN_GUI and "padding[0.01,0.01]" or "position[0.5,0.55]", + + "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"), "]", + "container_end[]", + }) +end + function store.get_formspec(dlgdata) + if store.loading then + return get_info_formspec(fgettext("Loading...")) + end + if store.load_error then + return get_info_formspec(fgettext("No packages could be retrieved")) + end + assert(store.load_ok) + store.update_paths() dlgdata.pagemax = math.max(math.ceil(#store.packages / num_per_page), 1) @@ -745,82 +824,70 @@ function store.get_formspec(dlgdata) local W = 15.75 local H = 9.5 - local formspec - if #store.packages_full > 0 then - formspec = { - "formspec_version[3]", - "size[15.75,9.5]", - "position[0.5,0.55]", - - "style[status,downloading,queued;border=false]", - - "container[0.375,0.375]", - "field[0,0;7.225,0.8;search_string;;", core.formspec_escape(search_string), "]", - "field_close_on_enter[search_string;false]", - "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.6,0;2.4,0.8;type;", table.concat(filter_types_titles, ","), ";", filter_type, "]", - "container_end[]", - - -- Page nav buttons - "container[0,", H - 0.8 - 0.375, "]", - "button[0.375,0;4,0.8;back;", fgettext("Back to Main Menu"), "]", - - "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;]", - "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[]", - - "container_end[]", - } + local formspec = { + "formspec_version[6]", + "size[15.75,9.5]", + TOUCHSCREEN_GUI and "padding[0.01,0.01]" or "position[0.5,0.55]", + + "style[status,downloading,queued;border=false]", + + "container[0.375,0.375]", + "field[0,0;7.225,0.8;search_string;;", core.formspec_escape(search_string), "]", + "field_close_on_enter[search_string;false]", + "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, "]", + "container_end[]", - if number_downloading > 0 then - formspec[#formspec + 1] = "button[12.75,0.375;2.625,0.8;downloading;" - if #download_queue > 0 then - formspec[#formspec + 1] = fgettext("$1 downloading,\n$2 queued", number_downloading, #download_queue) - else - formspec[#formspec + 1] = fgettext("$1 downloading...", number_downloading) - end - formspec[#formspec + 1] = "]" - else - local num_avail_updates = 0 - for i=1, #store.packages_full do - local package = store.packages_full[i] - if package.path and package.installed_release < package.release and - not (package.downloading or package.queued) then - num_avail_updates = num_avail_updates + 1 - end - end + -- Page nav buttons + "container[0,", H - 0.8 - 0.375, "]", + "button[0.375,0;5,0.8;back;", fgettext("Back to Main Menu"), "]", + + "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;]", + "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[]", - if num_avail_updates == 0 then - formspec[#formspec + 1] = "button[12.75,0.375;2.625,0.8;status;" - formspec[#formspec + 1] = fgettext("No updates") - formspec[#formspec + 1] = "]" - else - formspec[#formspec + 1] = "button[12.75,0.375;2.625,0.8;update_all;" - formspec[#formspec + 1] = fgettext("Update All [$1]", num_avail_updates) - formspec[#formspec + 1] = "]" + "container_end[]", + } + + if number_downloading > 0 then + formspec[#formspec + 1] = "button[12.5875,0.375;2.7875,0.8;downloading;" + if #download_queue > 0 then + formspec[#formspec + 1] = fgettext("$1 downloading,\n$2 queued", number_downloading, #download_queue) + else + formspec[#formspec + 1] = fgettext("$1 downloading...", number_downloading) + end + formspec[#formspec + 1] = "]" + else + local num_avail_updates = 0 + for i=1, #store.packages_full do + local package = store.packages_full[i] + if package.path and package.installed_release < package.release and + not (package.downloading or package.queued) then + num_avail_updates = num_avail_updates + 1 end end - if #store.packages == 0 then - formspec[#formspec + 1] = "label[4,3;" - formspec[#formspec + 1] = fgettext("No results") + if num_avail_updates == 0 then + formspec[#formspec + 1] = "button[12.5875,0.375;2.7875,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] = fgettext("Update All [$1]", num_avail_updates) formspec[#formspec + 1] = "]" end - else - formspec = { - "size[12,7]", - "position[0.5,0.55]", - "label[4,3;", fgettext("No packages could be retrieved"), "]", - "container[0,", H - 0.8 - 0.375, "]", - "button[0,0;4,0.8;back;", fgettext("Back to Main Menu"), "]", - "container_end[]", - } + end + + if #store.packages == 0 then + formspec[#formspec + 1] = "label[4,4.75;" + formspec[#formspec + 1] = fgettext("No results") + formspec[#formspec + 1] = "]" end -- download/queued tooltips always have the same message @@ -849,7 +916,10 @@ function store.get_formspec(dlgdata) formspec[#formspec + 1] = "]" -- buttons - local left_base = "image_button[-1.55,0;0.7,0.7;" .. core.formspec_escape(defaulttexturedir) + local description_width = W - 2.625 - 2 * 0.7 - 2 * 0.15 + + 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]" @@ -859,28 +929,28 @@ function store.get_formspec(dlgdata) formspec[#formspec + 1] = core.formspec_escape(defaulttexturedir) formspec[#formspec + 1] = "cdb_downloading.png;3;400;]" elseif package.queued then - formspec[#formspec + 1] = left_base + 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] = left_base .. "cdb_add.png;" .. elem_name .. "]" + 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] = left_base .. "cdb_update.png;" .. elem_name .. "]" + formspec[#formspec + 1] = third_base .. "cdb_update.png;" .. elem_name .. "]" formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Update") .. tooltip_colors - else - local elem_name = "uninstall_" .. i .. ";" - formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#a93b3b]" - formspec[#formspec + 1] = left_base .. "cdb_clear.png;" .. elem_name .. "]" - formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Uninstall") .. tooltip_colors + description_width = description_width - 0.7 - 0.15 end + + 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 end local web_elem_name = "view_" .. i .. ";" @@ -891,7 +961,6 @@ function store.get_formspec(dlgdata) formspec[#formspec + 1] = "container_end[]" -- description - local description_width = W - 0.375*5 - 0.85 - 2*0.7 formspec[#formspec + 1] = "textarea[1.855,0.3;" formspec[#formspec + 1] = tostring(description_width) formspec[#formspec + 1] = ",0.8;;;" @@ -901,7 +970,7 @@ function store.get_formspec(dlgdata) formspec[#formspec + 1] = "container_end[]" end - return table.concat(formspec, "") + return table.concat(formspec) end function store.handle_submit(this, fields) @@ -1042,16 +1111,8 @@ function store.handle_submit(this, fields) end function create_store_dlg(type) - if not store.loaded or #store.packages_full == 0 then - store.load() - end - - store.update_paths() - store.sort_packages() - 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 @@ -1060,9 +1121,11 @@ function create_store_dlg(type) break end end + else + filter_type = 1 end - store.filter_packages(search_string) + store.load() return dialog_create("store", store.get_formspec, diff --git a/builtin/mainmenu/dlg_create_world.lua b/builtin/mainmenu/dlg_create_world.lua index 18d4c07a61e32..b844923ed569f 100644 --- a/builtin/mainmenu/dlg_create_world.lua +++ b/builtin/mainmenu/dlg_create_world.lua @@ -91,16 +91,6 @@ local mgv6_biomes = { local function create_world_formspec(dialogdata) - -- Point the player to ContentDB when no games are found - if #pkgmgr.games == 0 then - return "size[8,2.5,true]" .. - "style[label_button;border=false]" .. - "button[0.5,0.5;7,0.5;label_button;" .. - fgettext("You have no games installed.") .. "]" .. - "button[0.5,1.5;2.5,0.5;world_create_open_cdb;" .. fgettext("Install a game") .. "]" .. - "button[5.0,1.5;2.5,0.5;world_create_cancel;" .. fgettext("Cancel") .. "]" - end - local current_mg = dialogdata.mg local mapgens = core.get_mapgen_names() @@ -310,8 +300,8 @@ local function create_world_formspec(dialogdata) "label[0,2;" .. fgettext("Mapgen") .. "]".. "dropdown[0,2.5;6.3;dd_mapgen;" .. mglist .. ";" .. selindex .. "]" - -- Warning if only devtest is installed - if #pkgmgr.games == 1 and pkgmgr.games[1].id == "devtest" then + -- Warning when making a devtest world + if game.id == "devtest" then retval = retval .. "container[0,3.5]" .. "box[0,0;5.8,1.7;#ff8800]" .. diff --git a/builtin/mainmenu/init.lua b/builtin/mainmenu/init.lua index ede90ffa78077..8b544a63808ad 100644 --- a/builtin/mainmenu/init.lua +++ b/builtin/mainmenu/init.lua @@ -51,30 +51,13 @@ dofile(menupath .. DIR_DELIM .. "dlg_register.lua") dofile(menupath .. DIR_DELIM .. "dlg_rename_modpack.lua") dofile(menupath .. DIR_DELIM .. "dlg_version_info.lua") -local tabs = {} - -tabs.settings = { - name = "settings", - caption = fgettext("Settings"), - cbf_formspec = function() - return "button[0.1,0.1;3,0.8;open_settings;" .. fgettext("Open Settings") .. "]" - end, - cbf_button_handler = function(tabview, fields) - if fields.open_settings then - local dlg = create_settings_dlg() - dlg:set_parent(tabview) - tabview:hide() - dlg:show() - return true - end - end, +local tabs = { + content = dofile(menupath .. DIR_DELIM .. "tab_content.lua"), + about = dofile(menupath .. DIR_DELIM .. "tab_about.lua"), + local_game = dofile(menupath .. DIR_DELIM .. "tab_local.lua"), + play_online = dofile(menupath .. DIR_DELIM .. "tab_online.lua") } -tabs.content = dofile(menupath .. DIR_DELIM .. "tab_content.lua") -tabs.about = dofile(menupath .. DIR_DELIM .. "tab_about.lua") -tabs.local_game = dofile(menupath .. DIR_DELIM .. "tab_local.lua") -tabs.play_online = dofile(menupath .. DIR_DELIM .. "tab_online.lua") - -------------------------------------------------------------------------------- local function main_event_handler(tabview, event) if event == "MenuQuit" then @@ -104,25 +87,16 @@ local function init_globals() menudata.worldlist:add_sort_mechanism("alphabetic", sort_worlds_alphabetic) menudata.worldlist:set_sortmode("alphabetic") - local gameid = core.settings:get("menu_last_game") - local game = gameid and pkgmgr.find_by_gameid(gameid) - if not game then - gameid = core.settings:get("default_game") or "minetest" - game = pkgmgr.find_by_gameid(gameid) - core.settings:set("menu_last_game", gameid) - end - mm_game_theme.init() + mm_game_theme.reset() -- Create main tabview - local tv_main = tabview_create("maintab", {x = 12, y = 5.4}, {x = 0, y = 0}) - -- note: size would be 15.5,7.1 in real coordinates mode + local tv_main = tabview_create("maintab", {x = 15.5, y = 7.1}, {x = 0, y = 0}) tv_main:set_autosave_tab(true) tv_main:add(tabs.local_game) tv_main:add(tabs.play_online) tv_main:add(tabs.content) - tv_main:add(tabs.settings) tv_main:add(tabs.about) tv_main:set_global_event_handler(main_event_handler) @@ -133,11 +107,18 @@ local function init_globals() tv_main:set_tab(last_tab) end - -- In case the folder of the last selected game has been deleted, - -- display "Minetest" as a header - if tv_main.current_tab == "local" and not game then - mm_game_theme.reset() - end + tv_main:set_end_button({ + icon = defaulttexturedir .. "settings_btn.png", + label = fgettext("Settings"), + name = "open_settings", + on_click = function(tabview) + local dlg = create_settings_dlg() + dlg:set_parent(tabview) + tabview:hide() + dlg:show() + return true + end, + }) ui.set_default("maintab") check_new_version() diff --git a/builtin/mainmenu/settings/components.lua b/builtin/mainmenu/settings/components.lua index 3ff38c94377f8..1ddea6f0de2c3 100644 --- a/builtin/mainmenu/settings/components.lua +++ b/builtin/mainmenu/settings/components.lua @@ -113,7 +113,7 @@ end make.float = make_field(tonumber, is_valid_number, function(x) local str = tostring(x) - if str:match("^%d+$") then + if str:match("^[+-]?%d+$") then str = str .. ".0" end return str @@ -161,15 +161,17 @@ function make.enum(setting) local value = core.settings:get(setting.name) or setting.default self.resettable = core.settings:has(setting.name) + local labels = setting.option_labels or {} + local items = {} for i, option in ipairs(setting.values) do - items[i] = core.formspec_escape(option) + items[i] = core.formspec_escape(labels[option] or option) end local selected_idx = table.indexof(setting.values, value) local fs = "label[0,0.1;" .. get_label(setting) .. "]" - fs = fs .. ("dropdown[0,0.3;%f,0.8;%s;%s;%d]"):format( + fs = fs .. ("dropdown[0,0.3;%f,0.8;%s;%s;%d;true]"):format( avail_w, setting.name, table.concat(items, ","), selected_idx, value) return fs, 1.1 @@ -177,7 +179,8 @@ function make.enum(setting) on_submit = function(self, fields) local old_value = core.settings:get(setting.name) or setting.default - local value = fields[setting.name] + local idx = tonumber(fields[setting.name]) or 0 + local value = setting.values[idx] if value == nil or value == old_value then return false end @@ -199,7 +202,7 @@ function make.path(setting) self.resettable = core.settings:has(setting.name) local fs = ("field[0,0.3;%f,0.8;%s;%s;%s]"):format( - avail_w - 3, setting.name, get_label(setting), value) + avail_w - 3, setting.name, get_label(setting), core.formspec_escape(value)) fs = fs .. ("button[%f,0.3;1.5,0.8;%s;%s]"):format(avail_w - 3, "pick_" .. setting.name, fgettext("Browse")) fs = fs .. ("button[%f,0.3;1.5,0.8;%s;%s]"):format(avail_w - 1.5, "set_" .. setting.name, fgettext("Set")) @@ -365,6 +368,10 @@ local function noise_params(setting) setting = setting, get_formspec = function(self, avail_w) + -- The "defaults" noise parameter flag doesn't reset a noise + -- setting to its default value, so we offer a regular reset button. + self.resettable = core.settings:has(setting.name) + local fs = "label[0,0.4;" .. get_label(setting) .. "]" .. ("button[%f,0;2.5,0.8;%s;%s]"):format(avail_w - 2.5, "edit_" .. setting.name, fgettext("Edit")) return fs, 0.8 diff --git a/builtin/mainmenu/settings/dlg_change_mapgen_flags.lua b/builtin/mainmenu/settings/dlg_change_mapgen_flags.lua index 3b5ef26f416ec..570d184da63d2 100644 --- a/builtin/mainmenu/settings/dlg_change_mapgen_flags.lua +++ b/builtin/mainmenu/settings/dlg_change_mapgen_flags.lua @@ -49,8 +49,8 @@ local function get_formspec(dialogdata) -- Final formspec will be created at the end of this function -- Default values below, may be changed depending on setting type local width = 10 - local height = 3.5 - local description_height = 3 + local height = 2 + local description_height = 1.5 local t = get_current_np_group(setting) local dimension = 3 @@ -58,10 +58,6 @@ local function get_formspec(dialogdata) dimension = 2 end - -- More space for 3x3 fields - description_height = description_height - 1.5 - height = height - 1.5 - local fields = {} local function add_field(x, name, label, value) fields[#fields + 1] = ("field[%f,%f;3.3,1;%s;%s;%s]"):format( @@ -109,21 +105,21 @@ local function get_formspec(dialogdata) .. "checkbox[0.5," .. height - 0.6 .. ";cb_defaults;" --[[~ "defaults" is a noise parameter flag. It describes the default processing options - for noise settings in main menu -> "All Settings". ]] + for noise settings in the settings menu. ]] .. fgettext("defaults") .. ";" -- defaults .. tostring(flags["defaults"] == true) .. "]" -- to get false if nil .. "checkbox[5," .. height - 0.6 .. ";cb_eased;" --[[~ "eased" is a noise parameter flag. It is used to make the map smoother and can be enabled in noise settings in - main menu -> "All Settings". ]] + the settings menu. ]] .. fgettext("eased") .. ";" -- eased .. tostring(flags["eased"] == true) .. "]" .. "checkbox[5," .. height - 0.15 .. ";cb_absvalue;" --[[~ "absvalue" is a noise parameter flag. It is short for "absolute value". It can be enabled in noise settings in - main menu -> "All Settings". ]] + the settings menu. ]] .. fgettext("absvalue") .. ";" -- absvalue .. tostring(flags["absvalue"] == true) .. "]" @@ -204,7 +200,7 @@ local function buttonhandler(this, fields) checkboxes = {} if setting.type == "noise_params_2d" then - fields["te_spready"] = fields["te_spreadz"] + fields["te_spready"] = fields["te_spreadz"] end local new_value = { offset = fields["te_offset"], @@ -232,6 +228,13 @@ local function buttonhandler(this, fields) return true end + for name, value in pairs(fields) do + if name:sub(1, 3) == "cb_" then + checkboxes[name] = core.is_yes(value) + return false -- Don't update the formspec! + end + end + return false end diff --git a/builtin/mainmenu/settings/dlg_settings.lua b/builtin/mainmenu/settings/dlg_settings.lua index dc56e576705cf..0141adbc561a0 100644 --- a/builtin/mainmenu/settings/dlg_settings.lua +++ b/builtin/mainmenu/settings/dlg_settings.lua @@ -19,8 +19,8 @@ local component_funcs = dofile(core.get_mainmenu_path() .. DIR_DELIM .. "settings" .. DIR_DELIM .. "components.lua") -local quick_shader_component = dofile(core.get_mainmenu_path() .. DIR_DELIM .. - "settings" .. DIR_DELIM .. "shader_component.lua") +local shadows_component = dofile(core.get_mainmenu_path() .. DIR_DELIM .. + "settings" .. DIR_DELIM .. "shadows_component.lua") local full_settings = settingtypes.parse_config_file(false, true) @@ -49,6 +49,9 @@ end local change_keys = { query_text = "Change keys", + requires = { + keyboard_mouse = true, + }, get_formspec = function(self, avail_w) local btn_w = math.min(avail_w, 3) return ("button[0,0;%f,0.8;btn_change_keys;%s]"):format(btn_w, fgettext("Change keys")), 0.8 @@ -61,35 +64,12 @@ local change_keys = { } -add_page({ - id = "most_used", - title = gettext("Most Used"), - content = { - change_keys, - "language", - "fullscreen", - PLATFORM ~= "Android" and "autosave_screensize" or false, - "touchscreen_threshold", - { heading = gettext("Scaling") }, - "gui_scaling", - "hud_scaling", - { heading = gettext("Graphics / Performance") }, - "smooth_lighting", - "enable_particles", - "enable_3d_clouds", - "opaque_water", - "connected_glass", - "node_highlighting", - "leaves_style", - { heading = gettext("Shaders") }, - quick_shader_component, - }, -}) - add_page({ id = "accessibility", title = gettext("Accessibility"), content = { + "language", + { heading = gettext("General") }, "font_size", "chat_font_size", "gui_scaling", @@ -155,6 +135,11 @@ end load_settingtypes() table.insert(page_by_id.controls_keyboard_and_mouse.content, 1, change_keys) +do + local content = page_by_id.graphics_and_audio_shaders.content + local idx = table.indexof(content, "enable_dynamic_shadows") + table.insert(content, idx, shadows_component) +end local function get_setting_info(name) @@ -168,6 +153,72 @@ local function get_setting_info(name) end +-- These must not be translated, as they need to show in the local +-- language no matter the user's current language. +get_setting_info("language").option_labels = { + [""] = fgettext_ne("(Use system language)"), + --ar = " [ar]", blacklisted + be = "Беларуская [be]", + bg = "Български [bg]", + ca = "Català [ca]", + cs = "Česky [cs]", + cy = "Cymraeg [cy]", + da = "Dansk [da]", + de = "Deutsch [de]", + --dv = " [dv]", blacklisted + el = "Ελληνικά [el]", + en = "English [en]", + eo = "Esperanto [eo]", + es = "Español [es]", + et = "Eesti [et]", + eu = "Euskara [eu]", + fi = "Suomi [fi]", + fil = "Wikang Filipino [fil]", + fr = "Français [fr]", + gd = "Gàidhlig [gd]", + gl = "Galego [gl]", + --he = " [he]", blacklisted + --hi = " [hi]", blacklisted + hu = "Magyar [hu]", + id = "Bahasa Indonesia [id]", + it = "Italiano [it]", + ja = "日本語 [ja]", + jbo = "Lojban [jbo]", + kk = "Қазақша [kk]", + --kn = " [kn]", blacklisted + ko = "한국어 [ko]", + ky = "Kırgızca / Кыргызча [ky]", + lt = "Lietuvių [lt]", + lv = "Latviešu [lv]", + mn = "Монгол [mn]", + mr = "मराठी [mr]", + ms = "Bahasa Melayu [ms]", + --ms_Arab = " [ms_Arab]", blacklisted + nb = "Norsk Bokmål [nb]", + nl = "Nederlands [nl]", + nn = "Norsk Nynorsk [nn]", + oc = "Occitan [oc]", + pl = "Polski [pl]", + pt = "Português [pt]", + pt_BR = "Português do Brasil [pt_BR]", + ro = "Română [ro]", + ru = "Русский [ru]", + sk = "Slovenčina [sk]", + sl = "Slovenščina [sl]", + sr_Cyrl = "Српски [sr_Cyrl]", + sr_Latn = "Srpski (Latinica) [sr_Latn]", + sv = "Svenska [sv]", + sw = "Kiswahili [sw]", + --th = " [th]", blacklisted + tr = "Türkçe [tr]", + tt = "Tatarça [tt]", + uk = "Українська [uk]", + vi = "Tiếng Việt [vi]", + zh_CN = "中文 (简体) [zh_CN]", + zh_TW = "正體中文 (繁體) [zh_TW]", +} + + -- See if setting matches keywords local function get_setting_match_weight(entry, query_keywords) local setting_score = 0 @@ -193,7 +244,7 @@ end local function filter_page_content(page, query_keywords) if #query_keywords == 0 then - return page.content + return page.content, 0 end local retval = {} @@ -229,12 +280,6 @@ local function update_filtered_pages(query) filtered_pages = {} filtered_page_by_id = {} - if query == "" or query == nil then - filtered_pages = all_pages - filtered_page_by_id = page_by_id - return filtered_pages[1].id - end - local query_keywords = {} for word in query:lower():gmatch("%S+") do table.insert(query_keywords, word) @@ -245,7 +290,7 @@ local function update_filtered_pages(query) for _, page in ipairs(all_pages) do local content, page_weight = filter_page_content(page, query_keywords) - if #content > 0 then + if page_has_contents(content) then local new_page = table.copy(page) new_page.content = content @@ -263,28 +308,113 @@ local function update_filtered_pages(query) end +local function check_requirements(name, requires) + if requires == nil then + return true + end + + local video_driver = core.get_active_driver() + local shaders_support = video_driver == "opengl" or video_driver == "ogles2" + local special = { + android = PLATFORM == "Android", + desktop = PLATFORM ~= "Android", + touchscreen_gui = TOUCHSCREEN_GUI, + keyboard_mouse = not TOUCHSCREEN_GUI, + shaders_support = shaders_support, + shaders = core.settings:get_bool("enable_shaders") and shaders_support, + opengl = video_driver == "opengl", + gles = video_driver:sub(1, 5) == "ogles", + } + + for req_key, req_value in pairs(requires) do + if special[req_key] == nil then + local required_setting = get_setting_info(req_key) + if required_setting == nil then + core.log("warning", "Unknown setting " .. req_key .. " required by " .. name) + end + local actual_value = core.settings:get_bool(req_key, + required_setting and core.is_yes(required_setting.default)) + if actual_value ~= req_value then + return false + end + elseif special[req_key] ~= req_value then + return false + end + end + + return true +end + + +function page_has_contents(content) + for _, item in ipairs(content) do + if item == false or item.heading then --luacheck: ignore + -- skip + elseif type(item) == "string" then + local setting = get_setting_info(item) + assert(setting, "Unknown setting: " .. item) + if check_requirements(setting.name, setting.requires) then + return true + end + elseif item.get_formspec then + if check_requirements(item.id, item.requires) then + return true + end + else + error("Unknown content in page: " .. dump(item)) + end + end + + return false +end + + local function build_page_components(page) - local retval = {} - local j = 1 - for i, content in ipairs(page.content) do - if content == false then - -- false is used to disable components conditionally (ie: Android specific) - j = j - 1 - elseif type(content) == "string" then - local setting = get_setting_info(content) - assert(setting, "Unknown setting: " .. content) + -- Filter settings based on requirements + local content = {} + local last_heading + for _, item in ipairs(page.content) do + if item == false then --luacheck: ignore + -- skip + elseif item.heading then + last_heading = item + else + local name, requires + if type(item) == "string" then + local setting = get_setting_info(item) + assert(setting, "Unknown setting: " .. item) + name = setting.name + requires = setting.requires + elseif item.get_formspec then + name = item.id + requires = item.requires + else + error("Unknown content in page: " .. dump(item)) + end + if check_requirements(name, requires) then + if last_heading then + content[#content + 1] = last_heading + last_heading = nil + end + content[#content + 1] = item + end + end + end + + -- Create components + local retval = {} + for i, item in ipairs(content) do + if type(item) == "string" then + local setting = get_setting_info(item) local component_func = component_funcs[setting.type] assert(component_func, "Unknown setting type: " .. setting.type) - retval[j] = component_func(setting) - elseif content.get_formspec then - retval[j] = content - elseif content.heading then - retval[j] = component_funcs.heading(content.heading) - else - error("Unknown content in page: " .. dump(content)) + retval[i] = component_func(setting) + elseif item.get_formspec then + retval[i] = item + elseif item.heading then + retval[i] = component_funcs.heading(item.heading) end - j = j + 1 end return retval end @@ -363,7 +493,8 @@ local function get_formspec(dialogdata) local last_section = nil for _, other_page in ipairs(filtered_pages) do if other_page.section ~= last_section then - fs[#fs + 1] = ("label[0.1,%f;%s]"):format(y + 0.41, core.colorize("#ff0", fgettext(other_page.section))) + fs[#fs + 1] = ("label[0.1,%f;%s]"):format( + y + 0.41, core.colorize("#ff0", fgettext(other_page.section))) last_section = other_page.section y = y + 0.82 end @@ -507,10 +638,15 @@ local function buttonhandler(this, fields) for i, comp in ipairs(dialogdata.components) do if comp.on_submit and comp:on_submit(fields, this) then + -- Clear components so they regenerate + dialogdata.components = nil return true end if comp.setting and fields["reset_" .. i] then core.settings:remove(comp.setting.name) + + -- Clear components so they regenerate + dialogdata.components = nil return true end end diff --git a/builtin/mainmenu/settings/init.lua b/builtin/mainmenu/settings/init.lua index 09ea8b9c11716..c60f1ef18187e 100644 --- a/builtin/mainmenu/settings/init.lua +++ b/builtin/mainmenu/settings/init.lua @@ -25,4 +25,4 @@ dofile(path .. DIR_DELIM .. "dlg_settings.lua") -- For RUN_IN_PLACE the generated files may appear in the 'bin' folder. -- See comment and alternative line at the end of 'generate_from_settingtypes.lua'. ---assert(loadfile(path .. DIR_DELIM .. "generate_from_settingtypes.lua"))(parse_config_file(true, false)) +-- assert(loadfile(path .. DIR_DELIM .. "generate_from_settingtypes.lua"))(settingtypes.parse_config_file(true, false)) diff --git a/builtin/mainmenu/settings/settingtypes.lua b/builtin/mainmenu/settings/settingtypes.lua index 06389b085585d..891e89fcb977f 100644 --- a/builtin/mainmenu/settings/settingtypes.lua +++ b/builtin/mainmenu/settings/settingtypes.lua @@ -51,20 +51,16 @@ local function parse_setting_line(settings, line, read_all, base_level, allow_se line = line:gsub("\r", "") -- comment - local comment = line:match("^#" .. CHAR_CLASSES.SPACE .. "*(.*)$") - if comment then - if settings.current_comment == "" then - settings.current_comment = comment - else - settings.current_comment = settings.current_comment .. "\n" .. comment - end + local comment_match = line:match("^#" .. CHAR_CLASSES.SPACE .. "*(.*)$") + if comment_match then + settings.current_comment[#settings.current_comment + 1] = comment_match return end -- clear current_comment so only comments directly above a setting are bound to it -- but keep a local reference to it for variables in the current line local current_comment = settings.current_comment - settings.current_comment = "" + settings.current_comment = {} -- empty lines if line:match("^" .. CHAR_CLASSES.SPACE .. "*$") then @@ -103,11 +99,32 @@ local function parse_setting_line(settings, line, read_all, base_level, allow_se return "Tried to add \"secure.\" setting" end + local requires = {} + local last_line = #current_comment > 0 and current_comment[#current_comment]:trim() + if last_line and last_line:lower():sub(1, 9) == "requires:" then + local parts = last_line:sub(10):split(",") + current_comment[#current_comment] = nil + + for _, part in ipairs(parts) do + part = part:trim() + + local value = true + if part:sub(1, 1) == "!" then + value = false + part = part:sub(2):trim() + end + + requires[part] = value + end + end + if readable_name == "" then readable_name = nil end local remaining_line = line:sub(first_part:len() + 1) + local comment = table.concat(current_comment, "\n"):trim() + if setting_type == "int" then local default, min, max = remaining_line:match("^" -- first int is required, the last 2 are optional @@ -129,7 +146,8 @@ local function parse_setting_line(settings, line, read_all, base_level, allow_se default = default, min = min, max = max, - comment = current_comment, + requires = requires, + comment = comment, }) return end @@ -151,7 +169,8 @@ local function parse_setting_line(settings, line, read_all, base_level, allow_se readable_name = readable_name, type = setting_type, default = default, - comment = current_comment, + requires = requires, + comment = comment, }) return end @@ -179,7 +198,6 @@ local function parse_setting_line(settings, line, read_all, base_level, allow_se local flags = "" if index then flags = default:sub(index) - default = default:sub(1, index - 3) -- Make sure no flags in single-line format end table.insert(values, flags) @@ -203,7 +221,8 @@ local function parse_setting_line(settings, line, read_all, base_level, allow_se flags = values[10] }, values = values, - comment = current_comment, + requires = requires, + comment = comment, noise_params = true, flags = flags_to_table("defaults,eased,absvalue") }) @@ -220,7 +239,8 @@ local function parse_setting_line(settings, line, read_all, base_level, allow_se readable_name = readable_name, type = "bool", default = remaining_line, - comment = current_comment, + requires = requires, + comment = comment, }) return end @@ -246,7 +266,8 @@ local function parse_setting_line(settings, line, read_all, base_level, allow_se default = default, min = min, max = max, - comment = current_comment, + requires = requires, + comment = comment, }) return end @@ -268,7 +289,8 @@ local function parse_setting_line(settings, line, read_all, base_level, allow_se type = "enum", default = default, values = values:split(",", true), - comment = current_comment, + requires = requires, + comment = comment, }) return end @@ -285,7 +307,8 @@ local function parse_setting_line(settings, line, read_all, base_level, allow_se readable_name = readable_name, type = setting_type, default = default, - comment = current_comment, + requires = requires, + comment = comment, }) return end @@ -314,7 +337,8 @@ local function parse_setting_line(settings, line, read_all, base_level, allow_se type = "flags", default = default, possible = flags_to_table(possible), - comment = current_comment, + requires = requires, + comment = comment, }) return end @@ -324,7 +348,7 @@ end local function parse_single_file(file, filepath, read_all, result, base_level, allow_secure) -- store this helper variable in the table so it's easier to pass to parse_setting_line() - result.current_comment = "" + result.current_comment = {} local line = file:read("*line") while line do diff --git a/builtin/mainmenu/settings/shader_component.lua b/builtin/mainmenu/settings/shader_component.lua deleted file mode 100644 index 3db18dbc431bb..0000000000000 --- a/builtin/mainmenu/settings/shader_component.lua +++ /dev/null @@ -1,163 +0,0 @@ ---Minetest ---Copyright (C) 2013 sapier ---Copyright (C) 2021-2 x2048 ---Copyright (C) 2022-3 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 shadow_levels_labels = { - fgettext("Disabled"), - fgettext("Very Low"), - fgettext("Low"), - fgettext("Medium"), - fgettext("High"), - fgettext("Very High") -} - - -local function get_shadow_mapping_idx() - local level = tonumber(core.settings:get("shadow_levels")) - if level and level >= 0 and level < #shadow_levels_labels then - return level + 1 - end - return 1 -end - - -local function set_shadow_mapping_idx(v) - assert(v >= 1 and v <= #shadow_levels_labels) - core.settings:set("shadow_levels", tostring(v - 1)) -end - - -return { - query_text = "Shaders", - get_formspec = function(self, avail_w) - local fs = "" - - local video_driver = core.get_active_driver() - local shaders_enabled = core.settings:get_bool("enable_shaders") - if video_driver == "opengl" then - fs = fs .. - "checkbox[0,0.25;cb_shaders;" .. fgettext("Shaders") .. ";" - .. tostring(shaders_enabled) .. "]" - elseif video_driver == "ogles2" then - fs = fs .. - "checkbox[0,0.25;cb_shaders;" .. fgettext("Shaders (experimental)") .. ";" - .. tostring(shaders_enabled) .. "]" - else - core.settings:set_bool("enable_shaders", false) - shaders_enabled = false - fs = fs .. - "label[0.13,0.25;" .. core.colorize("#888888", - fgettext("Shaders (unavailable)")) .. "]" - end - - if shaders_enabled then - fs = fs .. - "container[0,0.75]" .. - "checkbox[0,0;cb_tonemapping;" .. fgettext("Tone mapping") .. ";" - .. tostring(core.settings:get_bool("tone_mapping")) .. "]" .. - "checkbox[0,0.5;cb_waving_water;" .. fgettext("Waving liquids") .. ";" - .. tostring(core.settings:get_bool("enable_waving_water")) .. "]" .. - "checkbox[0,1;cb_waving_leaves;" .. fgettext("Waving leaves") .. ";" - .. tostring(core.settings:get_bool("enable_waving_leaves")) .. "]" .. - "checkbox[0,1.5;cb_waving_plants;" .. fgettext("Waving plants") .. ";" - .. tostring(core.settings:get_bool("enable_waving_plants")) .. "]" - - if video_driver == "opengl" then - fs = fs .. - "label[0,2.2;" .. fgettext("Dynamic shadows") .. "]" .. - "dropdown[0,2.4;3,0.8;dd_shadows;" .. table.concat(shadow_levels_labels, ",") .. ";" .. - get_shadow_mapping_idx() .. ";true]" .. - "label[0,3.5;" .. core.colorize("#bbb", fgettext("(The game will need to enable shadows as well)")) .. "]" - else - fs = fs .. - "label[0,2.2;" .. core.colorize("#888888", fgettext("Dynamic shadows")) .. "]" - end - - fs = fs .. "container_end[]" - else - fs = fs .. - "container[0.35,0.75]" .. - "label[0,0;" .. core.colorize("#888888", - fgettext("Tone mapping")) .. "]" .. - "label[0,0.5;" .. core.colorize("#888888", - fgettext("Waving liquids")) .. "]" .. - "label[0,1;" .. core.colorize("#888888", - fgettext("Waving leaves")) .. "]" .. - "label[0,1.5;" .. core.colorize("#888888", - fgettext("Waving plants")) .. "]".. - "label[0,2;" .. core.colorize("#888888", - fgettext("Dynamic shadows")) .. "]" .. - "container_end[]" - end - return fs, 4.5 - end, - - on_submit = function(self, fields) - if fields.cb_shaders then - core.settings:set("enable_shaders", fields.cb_shaders) - return true - end - if fields.cb_tonemapping then - core.settings:set("tone_mapping", fields.cb_tonemapping) - return true - end - if fields.cb_waving_water then - core.settings:set("enable_waving_water", fields.cb_waving_water) - return true - end - if fields.cb_waving_leaves then - core.settings:set("enable_waving_leaves", fields.cb_waving_leaves) - return true - end - if fields.cb_waving_plants then - core.settings:set("enable_waving_plants", fields.cb_waving_plants) - return true - end - - if fields.dd_shadows then - local old_shadow_level_idx = get_shadow_mapping_idx() - local shadow_level_idx = tonumber(fields.dd_shadows) - if shadow_level_idx == nil or shadow_level_idx == old_shadow_level_idx then - return false - end - - set_shadow_mapping_idx(shadow_level_idx) - - local shadow_presets = { - [2] = { 62, 512, "true", 0, "false" }, - [3] = { 93, 1024, "true", 0, "false" }, - [4] = { 140, 2048, "true", 1, "false" }, - [5] = { 210, 4096, "true", 2, "true" }, - [6] = { 300, 8192, "true", 2, "true" }, - } - local preset = shadow_presets[shadow_level_idx] - if preset then - core.settings:set("enable_dynamic_shadows", "true") - core.settings:set("shadow_map_max_distance", preset[1]) - core.settings:set("shadow_map_texture_size", preset[2]) - core.settings:set("shadow_map_texture_32bit", preset[3]) - core.settings:set("shadow_filters", preset[4]) - core.settings:set("shadow_map_color", preset[5]) - else - core.settings:set("enable_dynamic_shadows", "false") - end - return true - end - end, -} diff --git a/builtin/mainmenu/settings/shadows_component.lua b/builtin/mainmenu/settings/shadows_component.lua new file mode 100644 index 0000000000000..93c071bf78e5a --- /dev/null +++ b/builtin/mainmenu/settings/shadows_component.lua @@ -0,0 +1,121 @@ +--Minetest +--Copyright (C) 2021-2 x2048 +--Copyright (C) 2022-3 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 shadow_levels_labels = { + fgettext("Disabled"), + fgettext("Very Low"), + fgettext("Low"), + fgettext("Medium"), + fgettext("High"), + fgettext("Very High"), + fgettext("Custom"), +} +local PRESET_DISABLED = 1 +local PRESET_CUSTOM = #shadow_levels_labels + + +-- max distance, texture size, texture_32bit, filters, map color +local shadow_presets = { + [2] = { 62, 512, true, 0, false }, + [3] = { 93, 1024, true, 0, false }, + [4] = { 140, 2048, true, 1, false }, + [5] = { 210, 4096, true, 2, true }, + [6] = { 300, 8192, true, 2, true }, +} + + +local function detect_mapping_idx() + if not core.settings:get_bool("enable_dynamic_shadows", false) then + return PRESET_DISABLED + end + + local shadow_map_max_distance = tonumber(core.settings:get("shadow_map_max_distance")) + local shadow_map_texture_size = tonumber(core.settings:get("shadow_map_texture_size")) + local shadow_map_texture_32bit = core.settings:get_bool("shadow_map_texture_32bit", false) + local shadow_filters = tonumber(core.settings:get("shadow_filters")) + local shadow_map_color = core.settings:get_bool("shadow_map_color", false) + + for i = 2, 6 do + local preset = shadow_presets[i] + if preset[1] == shadow_map_max_distance and + preset[2] == shadow_map_texture_size and + preset[3] == shadow_map_texture_32bit and + preset[4] == shadow_filters and + preset[5] == shadow_map_color then + return i + end + end + return PRESET_CUSTOM +end + + +local function apply_preset(preset) + if preset then + core.settings:set_bool("enable_dynamic_shadows", true) + core.settings:set("shadow_map_max_distance", preset[1]) + core.settings:set("shadow_map_texture_size", preset[2]) + core.settings:set_bool("shadow_map_texture_32bit", preset[3]) + core.settings:set("shadow_filters", preset[4]) + core.settings:set_bool("shadow_map_color", preset[5]) + else + core.settings:set_bool("enable_dynamic_shadows", false) + end +end + + +return { + query_text = "Shadows", + requires = { + shaders = true, + opengl = true, + }, + get_formspec = function(self, avail_w) + local labels = table.copy(shadow_levels_labels) + local idx = detect_mapping_idx() + + -- Remove "custom" if not already selected + if idx ~= PRESET_CUSTOM then + table.remove(labels, PRESET_CUSTOM) + end + + local fs = + "label[0,0.2;" .. fgettext("Dynamic shadows") .. "]" .. + "dropdown[0,0.4;3,0.8;dd_shadows;" .. table.concat(labels, ",") .. ";" .. idx .. ";true]" .. + "label[0,1.5;" .. core.colorize("#bbb", fgettext("(The game will need to enable shadows as well)")) .. "]" + return fs, 1.8 + end, + on_submit = function(self, fields) + if fields.dd_shadows then + local old_shadow_level_idx = detect_mapping_idx() + local shadow_level_idx = tonumber(fields.dd_shadows) + if shadow_level_idx == nil or shadow_level_idx == old_shadow_level_idx then + return false + end + + if shadow_level_idx == PRESET_CUSTOM then + core.settings:set_bool("enable_dynamic_shadows", true) + return true + end + + local preset = shadow_presets[shadow_level_idx] + apply_preset(preset) + return true + end + end, +} diff --git a/builtin/mainmenu/tab_about.lua b/builtin/mainmenu/tab_about.lua index e0d984a6c78ac..4eb1c8b559599 100644 --- a/builtin/mainmenu/tab_about.lua +++ b/builtin/mainmenu/tab_about.lua @@ -194,7 +194,7 @@ return { fs = fs .. "button[0.5,5.1;4.5,0.8;userdata;" .. fgettext("Open User Data Directory") .. "]" end - return fs, "size[15.5,7.1,false]real_coordinates[true]" + return fs end, cbf_button_handler = function(this, fields, name, tabdata) if fields.homepage then diff --git a/builtin/mainmenu/tab_content.lua b/builtin/mainmenu/tab_content.lua index 5e14d1902efcc..2a184cd2c07c1 100644 --- a/builtin/mainmenu/tab_content.lua +++ b/builtin/mainmenu/tab_content.lua @@ -16,77 +16,83 @@ --with this program; if not, write to the Free Software Foundation, Inc., --51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -local packages_raw -local packages +local packages_raw, packages --------------------------------------------------------------------------------- -local function get_formspec(tabview, name, tabdata) - if pkgmgr.global_mods == nil then +local function update_packages() + if not pkgmgr.global_mods then pkgmgr.refresh_globals() end - if pkgmgr.games == nil then + if not pkgmgr.games then pkgmgr.update_gamelist() end - if packages == nil then - packages_raw = {} - table.insert_all(packages_raw, pkgmgr.games) - table.insert_all(packages_raw, pkgmgr.get_texture_packs()) - table.insert_all(packages_raw, pkgmgr.global_mods:get_list()) + packages_raw = {} + table.insert_all(packages_raw, pkgmgr.games) + table.insert_all(packages_raw, pkgmgr.get_texture_packs()) + table.insert_all(packages_raw, pkgmgr.global_mods:get_list()) - local function get_data() - return packages_raw - end + local function get_data() + return packages_raw + end - local function is_equal(element, uid) --uid match - return (element.type == "game" and element.id == uid) or - element.name == uid - end + local function is_equal(element, uid) --uid match + return (element.type == "game" and element.id == uid) or + element.name == uid + end + + packages = filterlist.create(get_data, pkgmgr.compare_package, + is_equal, nil, {}) +end + +local function on_change(type) + if type == "ENTER" then + update_packages() + end +end - packages = filterlist.create(get_data, pkgmgr.compare_package, - is_equal, nil, {}) +local function get_formspec(tabview, name, tabdata) + if not packages then + update_packages() end - if tabdata.selected_pkg == nil then + if not tabdata.selected_pkg then tabdata.selected_pkg = 1 end local use_technical_names = core.settings:get_bool("show_technical_names") + local retval = { + "label[0.4,0.4;", fgettext("Installed Packages:"), "]", + "tablecolumns[color;tree;text]", + "table[0.4,0.8;6.3,4.8;pkglist;", + pkgmgr.render_packagelist(packages, use_technical_names), + ";", tabdata.selected_pkg, "]", - local retval = - "label[0.05,-0.25;".. fgettext("Installed Packages:") .. "]" .. - "tablecolumns[color;tree;text]" .. - "table[0,0.25;5.1,4.3;pkglist;" .. - pkgmgr.render_packagelist(packages, use_technical_names) .. - ";" .. tabdata.selected_pkg .. "]" .. - "button[0,4.85;5.25,0.5;btn_contentdb;".. fgettext("Browse online content") .. "]" - + "button[0.4,5.8;6.3,0.9;btn_contentdb;", fgettext("Browse online content"), "]" + } local selected_pkg if filterlist.size(packages) >= tabdata.selected_pkg then selected_pkg = packages:get_list()[tabdata.selected_pkg] end - if selected_pkg ~= nil then - --check for screenshot beeing available + if selected_pkg then + -- Check for screenshot being available local screenshotfilename = selected_pkg.path .. DIR_DELIM .. "screenshot.png" local screenshotfile, error = io.open(screenshotfilename, "r") local modscreenshot - if error == nil then + if not error then screenshotfile:close() modscreenshot = screenshotfilename - end - - if modscreenshot == nil then - modscreenshot = defaulttexturedir .. "no_screenshot.png" + else + modscreenshot = defaulttexturedir .. "no_screenshot.png" end local info = core.get_content_info(selected_pkg.path) local desc = fgettext("No package description available") if info.description and info.description:trim() ~= "" then - desc = info.description + desc = core.formspec_escape(info.description) end local title_and_name @@ -97,65 +103,64 @@ local function get_formspec(tabview, name, tabdata) core.colorize("#BFBFBF", selected_pkg.name) end - retval = retval .. - "image[5.5,0;3,2;" .. core.formspec_escape(modscreenshot) .. "]" .. - "label[8.25,0.6;" .. core.formspec_escape(title_and_name) .. "]" .. - "box[5.5,2.2;6.15,2.35;#000]" - - if selected_pkg.type == "mod" then - if selected_pkg.is_modpack then - retval = retval .. - "button[8.65,4.65;3.25,1;btn_mod_mgr_rename_modpack;" .. - fgettext("Rename") .. "]" + table.insert_all(retval, { + "image[7.1,0.2;3,2;", core.formspec_escape(modscreenshot), "]", + "label[10.5,1;", core.formspec_escape(title_and_name), "]", + "box[7.1,2.4;8,3.1;#000]" + }) + + if selected_pkg.is_modpack then + table.insert_all(retval, { + "button[11.1,5.8;4,0.9;btn_mod_mgr_rename_modpack;", + fgettext("Rename"), "]" + }) + elseif selected_pkg.type == "mod" then + -- Show dependencies for mods + desc = desc .. "\n\n" + local toadd_hard = table.concat(info.depends or {}, "\n") + local toadd_soft = table.concat(info.optional_depends or {}, "\n") + if toadd_hard == "" and toadd_soft == "" then + desc = desc .. fgettext("No dependencies.") else - --show dependencies - desc = desc .. "\n\n" - local toadd_hard = table.concat(info.depends or {}, "\n") - local toadd_soft = table.concat(info.optional_depends or {}, "\n") - if toadd_hard == "" and toadd_soft == "" then - desc = desc .. fgettext("No dependencies.") - else + if toadd_hard ~= "" then + desc = desc ..fgettext("Dependencies:") .. + "\n" .. toadd_hard + end + if toadd_soft ~= "" then if toadd_hard ~= "" then - desc = desc ..fgettext("Dependencies:") .. - "\n" .. toadd_hard - end - if toadd_soft ~= "" then - if toadd_hard ~= "" then - desc = desc .. "\n\n" - end - desc = desc .. fgettext("Optional dependencies:") .. - "\n" .. toadd_soft + desc = desc .. "\n\n" end + desc = desc .. fgettext("Optional dependencies:") .. + "\n" .. toadd_soft end end - - else - if selected_pkg.type == "txp" then - if selected_pkg.enabled then - retval = retval .. - "button[8.65,4.65;3.25,1;btn_mod_mgr_disable_txp;" .. - fgettext("Disable Texture Pack") .. "]" - else - retval = retval .. - "button[8.65,4.65;3.25,1;btn_mod_mgr_use_txp;" .. - fgettext("Use Texture Pack") .. "]" - end + elseif selected_pkg.type == "txp" then + if selected_pkg.enabled then + table.insert_all(retval, { + "button[11.1,5.8;4,0.9;btn_mod_mgr_disable_txp;", + fgettext("Disable Texture Pack"), "]" + }) + else + table.insert_all(retval, { + "button[11.1,5.8;4,0.9;btn_mod_mgr_use_txp;", + fgettext("Use Texture Pack"), "]" + }) end end - retval = retval .. "textarea[5.85,2.2;6.35,2.9;;" .. - fgettext("Information:") .. ";" .. desc .. "]" + table.insert_all(retval, {"textarea[7.1,2.4;8,3.1;;;", desc, "]"}) if core.may_modify_path(selected_pkg.path) then - retval = retval .. - "button[5.5,4.65;3.25,1;btn_mod_mgr_delete_mod;" .. - fgettext("Uninstall Package") .. "]" + table.insert_all(retval, { + "button[7.1,5.8;4,0.9;btn_mod_mgr_delete_mod;", + fgettext("Uninstall Package"), "]" + }) end end - return retval + + return table.concat(retval) end --------------------------------------------------------------------------------- local function handle_doubleclick(pkg) if pkg.type == "txp" then if core.settings:get("texture_path") == pkg.path then @@ -170,10 +175,10 @@ local function handle_doubleclick(pkg) end end --------------------------------------------------------------------------------- local function handle_buttons(tabview, fields, tabname, tabdata) - if fields["pkglist"] ~= nil then - local event = core.explode_table_event(fields["pkglist"]) + + if fields.pkglist then + local event = core.explode_table_event(fields.pkglist) tabdata.selected_pkg = event.row if event.type == "DCL" then handle_doubleclick(packages:get_list()[tabdata.selected_pkg]) @@ -181,7 +186,7 @@ local function handle_buttons(tabview, fields, tabname, tabdata) return true end - if fields["btn_contentdb"] ~= nil then + if fields.btn_contentdb then local dlg = create_store_dlg() dlg:set_parent(tabview) tabview:hide() @@ -190,7 +195,7 @@ local function handle_buttons(tabview, fields, tabname, tabdata) return true end - if fields["btn_mod_mgr_rename_modpack"] ~= nil then + if fields.btn_mod_mgr_rename_modpack then local mod = packages:get_list()[tabdata.selected_pkg] local dlg_renamemp = create_rename_modpack_dlg(mod) dlg_renamemp:set_parent(tabview) @@ -200,7 +205,7 @@ local function handle_buttons(tabview, fields, tabname, tabdata) return true end - if fields["btn_mod_mgr_delete_mod"] ~= nil then + if fields.btn_mod_mgr_delete_mod then local mod = packages:get_list()[tabdata.selected_pkg] local dlg_delmod = create_delete_content_dlg(mod) dlg_delmod:set_parent(tabview) @@ -227,11 +232,10 @@ local function handle_buttons(tabview, fields, tabname, tabdata) return false end --------------------------------------------------------------------------------- return { name = "content", caption = fgettext("Content"), cbf_formspec = get_formspec, cbf_button_handler = handle_buttons, - on_change = pkgmgr.update_gamelist + on_change = on_change } diff --git a/builtin/mainmenu/tab_local.lua b/builtin/mainmenu/tab_local.lua index de121e65bc3fd..efa7667da6acb 100644 --- a/builtin/mainmenu/tab_local.lua +++ b/builtin/mainmenu/tab_local.lua @@ -23,10 +23,30 @@ local valid_disabled_settings = { ["enable_server"]=true, } +-- Name and port stored to persist when updating the formspec +local current_name = core.settings:get("name") +local current_port = core.settings:get("port") + -- Currently chosen game in gamebar for theming and filtering function current_game() - local last_game_id = core.settings:get("menu_last_game") - local game = pkgmgr.find_by_gameid(last_game_id) + local gameid = core.settings:get("menu_last_game") + local game = gameid and pkgmgr.find_by_gameid(gameid) + -- Fall back to first game installed if one exists. + if not game and #pkgmgr.games > 0 then + + -- If devtest is the first game in the list and there is another + -- game available, pick the other game instead. + local picked_game + if pkgmgr.games[1].id == "devtest" and #pkgmgr.games > 1 then + picked_game = 2 + else + picked_game = 1 + end + + game = pkgmgr.games[picked_game] + gameid = game.id + core.settings:set("menu_last_game", gameid) + end return game end @@ -59,16 +79,12 @@ function singleplayer_refresh_gamebar() old_bar:delete() end - local function game_buttonbar_button_handler(fields) - if fields.game_open_cdb then - local maintab = ui.find_by_name("maintab") - local dlg = create_store_dlg("game") - dlg:set_parent(maintab) - maintab:hide() - dlg:show() - return true - end + -- Hide gamebar if no games are installed + if #pkgmgr.games == 0 then + return false + end + local function game_buttonbar_button_handler(fields) for _, game in ipairs(pkgmgr.games) do if fields["game_btnbar_" .. game.id] then apply_game(game) @@ -77,9 +93,8 @@ function singleplayer_refresh_gamebar() end end - local btnbar = buttonbar_create("game_button_bar", - game_buttonbar_button_handler, - {x=-0.3,y=5.9}, "horizontal", {x=12.4,y=1.15}) + local btnbar = buttonbar_create("game_button_bar", {x = 0, y = 7.475}, + {x = 15.5, y = 1.25}, "#000000", game_buttonbar_button_handler) for _, game in ipairs(pkgmgr.games) do local btn_name = "game_btnbar_" .. game.id @@ -105,6 +120,7 @@ function singleplayer_refresh_gamebar() local plus_image = core.formspec_escape(defaulttexturedir .. "plus.png") btnbar:add_button("game_open_cdb", "", plus_image, fgettext("Install games from ContentDB")) + return true end local function get_disabled_settings(game) @@ -134,6 +150,15 @@ local function get_disabled_settings(game) end local function get_formspec(tabview, name, tabdata) + + -- Point the player to ContentDB when no games are found + if #pkgmgr.games == 0 then + return table.concat({ + "style[label_button;border=false]", + "button[2.75,1.5;10,1;label_button;", fgettext("You have no games installed."), "]", + "button[5.25,3.5;5,1.2;game_open_cdb;", fgettext("Install a game"), "]"}) + end + local retval = "" local index = filterlist.get_current_index(menudata.worldlist, @@ -151,8 +176,8 @@ local function get_formspec(tabview, name, tabdata) local creative, damage, host = "", "", "" -- Y offsets for game settings checkboxes - local y = -0.2 - local yo = 0.45 + local y = 0.2 + local yo = 0.5625 if disabled_settings["creative_mode"] == nil then creative = "checkbox[0,"..y..";cb_creative_mode;".. fgettext("Creative Mode") .. ";" .. @@ -171,41 +196,60 @@ local function get_formspec(tabview, name, tabdata) end retval = retval .. - "button[3.9,3.8;2.8,1;world_delete;".. fgettext("Delete") .. "]" .. - "button[6.55,3.8;2.8,1;world_configure;".. fgettext("Select Mods") .. "]" .. - "button[9.2,3.8;2.8,1;world_create;".. fgettext("New") .. "]" .. - "label[3.9,-0.05;".. fgettext("Select World:") .. "]".. + "container[5.25,4.875]" .. + "button[0,0;3.225,0.8;world_delete;".. fgettext("Delete") .. "]" .. + "button[3.325,0;3.225,0.8;world_configure;".. fgettext("Select Mods") .. "]" .. + "button[6.65,0;3.225,0.8;world_create;".. fgettext("New") .. "]" .. + "container_end[]" .. + "container[0.375,0.375]" .. creative .. damage .. host .. - "textlist[3.9,0.4;7.9,3.45;sp_worlds;" .. + "container_end[]" .. + "container[5.25,0.375]" .. + "label[0,0.2;".. fgettext("Select World:") .. "]".. + "textlist[0,0.5;9.875,3.9;sp_worlds;" .. menu_render_worldlist() .. - ";" .. index .. "]" + ";" .. index .. "]" .. + "container_end[]" if core.settings:get_bool("enable_server") and disabled_settings["enable_server"] == nil then retval = retval .. - "button[7.9,4.75;4.1,1;play;".. fgettext("Host Game") .. "]" .. + "button[10.1875,5.925;4.9375,0.8;play;".. fgettext("Host Game") .. "]" .. + "container[0.375,0.375]" .. "checkbox[0,"..y..";cb_server_announce;" .. fgettext("Announce Server") .. ";" .. - dump(core.settings:get_bool("server_announce")) .. "]" .. - "field[0.3,2.85;3.8,0.5;te_playername;" .. fgettext("Name") .. ";" .. - core.formspec_escape(core.settings:get("name")) .. "]" .. - "pwdfield[0.3,4.05;3.8,0.5;te_passwd;" .. fgettext("Password") .. "]" + dump(core.settings:get_bool("server_announce")) .. "]" + + -- Reset y so that the text fields always start at the same position, + -- regardless of whether some of the checkboxes are hidden. + y = 0.2 + 4 * yo + 0.35 + + retval = retval .. "field[0," .. y .. ";4.5,0.75;te_playername;" .. fgettext("Name") .. ";" .. + core.formspec_escape(current_name) .. "]" + + y = y + 1.15 + 0.25 + + retval = retval .. "pwdfield[0," .. y .. ";4.5,0.75;te_passwd;" .. fgettext("Password") .. "]" + + y = y + 1.15 + 0.25 local bind_addr = core.settings:get("bind_address") if bind_addr ~= nil and bind_addr ~= "" then retval = retval .. - "field[0.3,5.25;2.5,0.5;te_serveraddr;" .. fgettext("Bind Address") .. ";" .. + "field[0," .. y .. ";3,0.75;te_serveraddr;" .. fgettext("Bind Address") .. ";" .. core.formspec_escape(core.settings:get("bind_address")) .. "]" .. - "field[2.85,5.25;1.25,0.5;te_serverport;" .. fgettext("Port") .. ";" .. - core.formspec_escape(core.settings:get("port")) .. "]" + "field[3.25," .. y .. ";1.25,0.75;te_serverport;" .. fgettext("Port") .. ";" .. + core.formspec_escape(current_port) .. "]" else retval = retval .. - "field[0.3,5.25;3.8,0.5;te_serverport;" .. fgettext("Server Port") .. ";" .. - core.formspec_escape(core.settings:get("port")) .. "]" + "field[0," .. y .. ";4.5,0.75;te_serverport;" .. fgettext("Server Port") .. ";" .. + core.formspec_escape(current_port) .. "]" end + + retval = retval .. "container_end[]" else retval = retval .. - "button[7.9,4.75;4.1,1;play;" .. fgettext("Play Game") .. "]" + "button[10.1875,5.925;4.9375,0.8;play;" .. fgettext("Play Game") .. "]" end return retval @@ -215,12 +259,29 @@ local function main_button_handler(this, fields, name, tabdata) assert(name == "local") + if fields.game_open_cdb then + local maintab = ui.find_by_name("maintab") + local dlg = create_store_dlg("game") + dlg:set_parent(maintab) + maintab:hide() + dlg:show() + return true + end + if this.dlg_create_world_closed_at == nil then this.dlg_create_world_closed_at = 0 end local world_doubleclick = false + if fields["te_playername"] then + current_name = fields["te_playername"] + end + + if fields["te_serverport"] then + current_port = fields["te_serverport"] + end + if fields["sp_worlds"] ~= nil then local event = core.explode_textlist_event(fields["sp_worlds"]) local selected = core.get_textlist_index("sp_worlds") @@ -381,8 +442,9 @@ local function on_change(type, old_tab, new_tab) apply_game(game) end - singleplayer_refresh_gamebar() - ui.find_by_name("game_button_bar"):show() + if singleplayer_refresh_gamebar() then + ui.find_by_name("game_button_bar"):show() + end else menudata.worldlist:set_filtercriteria(nil) local gamebar = ui.find_by_name("game_button_bar") diff --git a/builtin/mainmenu/tab_online.lua b/builtin/mainmenu/tab_online.lua index 1b1a0a71a1116..8a0de4edacff2 100644 --- a/builtin/mainmenu/tab_online.lua +++ b/builtin/mainmenu/tab_online.lua @@ -180,7 +180,7 @@ local function get_formspec(tabview, name, tabdata) retval = retval .. ";0]" end - return retval, "size[15.5,7.1,false]real_coordinates[true]" + return retval end -------------------------------------------------------------------------------- diff --git a/builtin/settingtypes.txt b/builtin/settingtypes.txt index f7553ec242ad2..f116d30dde108 100644 --- a/builtin/settingtypes.txt +++ b/builtin/settingtypes.txt @@ -57,6 +57,25 @@ # Comments and (Readable name) are handled by gettext. # Comments should be complete sentences that describe the setting and possibly # give the user additional useful insight. +# The last line of a comment can contain the requirements for the setting, ie: +# +# # This is a comment +# # +# # Requires: shaders, enable_dynamic_shadows, !touchscreen_gui +# name (Readable name) type type_args +# +# A requirement can be the name of a boolean setting or an engine-defined value. +# These requirements may be: +# +# * The value of a boolean setting, such as enable_dynamic_shadows +# * An engine-defined value: +# * shaders_support (a video driver that supports shaders, may not be enabled) +# * shaders (both enable_shaders and shaders_support) +# * desktop / android +# * touchscreen_gui / keyboard_mouse +# * opengl / gles +# * You can negate any requirement by prepending with ! +# # Sections are marked by a single line in the format: [Section Name] # Sub-section are marked by adding * in front of the section name: [*Sub-section] # Sub-sub-sections have two * etc. @@ -72,6 +91,8 @@ 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 Change Keys. +# +# 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. @@ -91,44 +112,68 @@ always_fly_fast (Always fly fast) bool true # The time in seconds it takes between repeated node placements when holding # the place button. +# +# Requires: keyboard_mouse repeat_place_time (Place repetition interval) float 0.25 0.16 2 # Automatically jump up single-node obstacles. autojump (Automatic jumping) bool false -# Prevent digging and placing from repeating when holding the mouse buttons. +# Prevent digging and placing from repeating when holding the respective buttons. # Enable this when you dig or place too often by accident. +# On touchscreens, this only affects digging. safe_dig_and_place (Safe digging and placing) bool false [*Keyboard and Mouse] # Invert vertical mouse movement. +# +# Requires: keyboard_mouse invert_mouse (Invert mouse) bool false # Mouse sensitivity multiplier. +# +# Requires: keyboard_mouse mouse_sensitivity (Mouse sensitivity) float 0.2 0.001 10.0 # Enable mouse wheel (scroll) for item selection in hotbar. +# +# 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: keyboard_mouse invert_hotbar_mouse_wheel (Hotbar: Invert mouse wheel direction) bool false [*Touchscreen] -# The length in pixels it takes for touch screen interaction to start. -touchscreen_threshold (Touch screen threshold) int 20 0 100 +# The length in pixels it takes for touchscreen interaction to start. +# +# Requires: touchscreen_gui +touchscreen_threshold (Touchscreen threshold) int 20 0 100 + +# Touchscreen sensitivity multiplier. +# +# Requires: touchscreen_gui +touchscreen_sensitivity (Touchscreen sensitivity) float 0.2 0.001 10.0 # Use crosshair to select object instead of whole screen. # If enabled, a crosshair will be shown and will be used for selecting object. +# +# Requires: touchscreen_gui touch_use_crosshair (Use crosshair for touch screen) bool false -# (Android) Fixes the position of virtual joystick. +# Fixes the position of virtual joystick. # If disabled, virtual joystick will center to first-touch's position. +# +# Requires: touchscreen_gui fixed_virtual_joystick (Fixed virtual joystick) bool false -# (Android) Use virtual joystick to trigger "Aux1" button. +# Use virtual joystick to trigger "Aux1" button. # If enabled, virtual joystick will also tap "Aux1" button when out of main circle. +# +# Requires: touchscreen_gui virtual_joystick_triggers_aux1 (Virtual joystick triggers Aux1 button) bool false @@ -139,25 +184,37 @@ virtual_joystick_triggers_aux1 (Virtual joystick triggers Aux1 button) bool fals [**Screen] # Width component of the initial window size. +# +# Requires: desktop screen_w (Screen width) int 1024 1 65535 # Height component of the initial window size. +# +# Requires: desktop screen_h (Screen height) int 600 1 65535 # Whether the window is maximized. +# +# Requires: desktop window_maximized (Window maximized) bool false # Save window size automatically when modified. # If true, screen size is saved in screen_w and screen_h, and whether the window # is maximized is stored in window_maximized. # (Autosaving window_maximized only works if compiled with SDL.) +# +# Requires: desktop autosave_screensize (Remember screen size) bool true # Fullscreen mode. +# +# Requires: desktop fullscreen (Full screen) bool false # Open the pause menu when the window's focus is lost. Does not pause if a formspec is # open. +# +# Requires: desktop pause_on_lost_focus (Pause on lost window focus) bool false [**FPS] @@ -258,14 +315,20 @@ ambient_occlusion_gamma (Ambient occlusion gamma) float 1.8 0.25 4.0 # Path to save screenshots at. Can be an absolute or relative path. # The folder will be created if it doesn't already exist. +# +# Requires: desktop screenshot_path (Screenshot folder) path screenshots # Format of screenshots. +# +# Requires: desktop screenshot_format (Screenshot format) enum png png,jpg # Screenshot quality. Only used for JPEG format. # 1 means worst quality; 100 means best quality. # Use 0 for default quality. +# +# Requires: desktop screenshot_quality (Screenshot quality) int 0 0 100 [**Node and Entity Highlighting] @@ -297,9 +360,13 @@ crosshair_alpha (Crosshair alpha) int 255 0 255 enable_fog (Fog) bool true # Make fog and sky colors depend on daytime (dawn/sunset) and view direction. +# +# Requires: enable_fog directional_colored_fog (Colored fog) bool true # Fraction of the visible distance at which fog starts to be rendered +# +# Requires: enable_fog fog_start (Fog start) float 0.4 0.0 0.99 [**Clouds] @@ -308,39 +375,27 @@ fog_start (Fog start) float 0.4 0.0 0.99 enable_clouds (Clouds) bool true # Use 3D cloud look instead of flat. +# +# Requires: enable_clouds enable_3d_clouds (3D clouds) bool true [**Filtering and Antialiasing] -# Use mipmapping to scale textures. May slightly increase performance, +# Use mipmaps when scaling textures down. May slightly increase performance, # especially when using a high resolution texture pack. -# Gamma correct downscaling is not supported. +# Gamma-correct downscaling is not supported. mip_map (Mipmapping) bool false -# Use anisotropic filtering when viewing at textures from an angle. -anisotropic_filter (Anisotropic filtering) bool false - -# Use bilinear filtering when scaling textures. +# Use bilinear filtering when scaling textures down. bilinear_filter (Bilinear filtering) bool false -# Use trilinear filtering when scaling textures. +# Use trilinear filtering when scaling textures down. +# If both bilinear and trilinear filtering are enabled, trilinear filtering +# is applied. trilinear_filter (Trilinear filtering) bool false -# Filtered textures can blend RGB values with fully-transparent neighbors, -# which PNG optimizers usually discard, often resulting in dark or -# light edges to transparent textures. Apply a filter to clean that up -# at texture load time. This is automatically enabled if mipmapping is enabled. -texture_clean_transparent (Clean transparent textures) bool false - -# When using bilinear/trilinear/anisotropic filters, low-resolution textures -# can be blurred, so automatically upscale them with nearest-neighbor -# interpolation to preserve crisp pixels. This sets the minimum texture size -# for the upscaled textures; higher values look sharper, but require more -# memory. Powers of 2 are recommended. This setting is ONLY applied if -# bilinear/trilinear/anisotropic filtering is enabled. -# This is also used as the base node texture size for world-aligned -# texture autoscaling. -texture_min_size (Minimum texture size) int 64 1 32768 +# Use anisotropic filtering when looking at textures from an angle. +anisotropic_filter (Anisotropic filtering) bool false # Select the antialiasing method to apply. # @@ -386,84 +441,111 @@ enable_raytraced_culling (Enable Raytraced Culling) bool true # Shaders allow advanced visual effects and may increase performance on some video # cards. # This only works with the OpenGL video backend. +# +# Requires: shaders_support enable_shaders (Shaders) bool true [**Waving Nodes] # Set to true to enable waving leaves. -# Requires shaders to be enabled. +# +# Requires: shaders enable_waving_leaves (Waving leaves) bool false # Set to true to enable waving plants. -# Requires shaders to be enabled. +# +# Requires: shaders enable_waving_plants (Waving plants) bool false # Set to true to enable waving liquids (like water). -# Requires shaders to be enabled. +# +# Requires: shaders enable_waving_water (Waving liquids) bool false # The maximum height of the surface of waving liquids. # 4.0 = Wave height is two nodes. # 0.0 = Wave doesn't move at all. # Default is 1.0 (1/2 node). -# Requires waving liquids to be enabled. +# +# Requires: shaders, enable_waving_water water_wave_height (Waving liquids wave height) float 1.0 0.0 4.0 # Length of liquid waves. -# Requires waving liquids to be enabled. +# +# Requires: shaders, enable_waving_water water_wave_length (Waving liquids wavelength) float 20.0 0.1 # How fast liquid waves will move. Higher = faster. # If negative, liquid waves will move backwards. -# Requires waving liquids to be enabled. +# +# Requires: shaders, enable_waving_water water_wave_speed (Waving liquids wave speed) float 5.0 [**Dynamic shadows] # Set to true to enable Shadow Mapping. -# Requires shaders to be enabled. +# +# Requires: shaders, opengl enable_dynamic_shadows (Dynamic shadows) bool false # Set the shadow strength gamma. # Adjusts the intensity of in-game dynamic shadows. # Lower value means lighter shadows, higher value means darker shadows. +# +# Requires: shaders, enable_dynamic_shadows, opengl shadow_strength_gamma (Shadow strength gamma) float 1.0 0.1 10.0 # Maximum distance to render shadows. -shadow_map_max_distance (Shadow map max distance in nodes to render shadows) float 200.0 10.0 1000.0 +# +# Requires: shaders, enable_dynamic_shadows, opengl +shadow_map_max_distance (Shadow map max distance in nodes to render shadows) float 140.0 10.0 1000.0 # Texture size to render the shadow map on. # This must be a power of two. # Bigger numbers create better shadows but it is also more expensive. +# +# Requires: shaders, enable_dynamic_shadows, opengl shadow_map_texture_size (Shadow map texture size) int 2048 128 8192 # Sets shadow texture quality to 32 bits. # On false, 16 bits texture will be used. # This can cause much more artifacts in the shadow. +# +# Requires: shaders, enable_dynamic_shadows, opengl shadow_map_texture_32bit (Shadow map texture in 32 bits) bool true # Enable Poisson disk filtering. # On true uses Poisson disk to make "soft shadows". Otherwise uses PCF filtering. +# +# Requires: shaders, enable_dynamic_shadows, opengl shadow_poisson_filter (Poisson filtering) bool true -# Define shadow filtering quality. -# This simulates the soft shadows effect by applying a PCF or Poisson disk -# but also uses more resources. +# Define shadow filtering quality. +# This simulates the soft shadows effect by applying a PCF or Poisson disk +# but also uses more resources. +# +# Requires: shaders, enable_dynamic_shadows, opengl shadow_filters (Shadow filter quality) enum 1 0,1,2 # Enable colored shadows. # On true translucent nodes cast colored shadows. This is expensive. +# +# Requires: shaders, enable_dynamic_shadows, opengl shadow_map_color (Colored shadows) bool false # Spread a complete update of shadow map over given amount of frames. # Higher values might make shadows laggy, lower values # will consume more resources. # Minimum value: 1; maximum value: 16 +# +# Requires: shaders, enable_dynamic_shadows, opengl shadow_update_frames (Map shadows update frames) int 8 1 16 # Set the soft shadow radius size. # Lower values mean sharper shadows, bigger values mean softer shadows. # Minimum value: 1.0; maximum value: 15.0 +# +# Requires: shaders, enable_dynamic_shadows, opengl shadow_soft_radius (Soft shadow radius) float 5.0 1.0 15.0 [**Post Processing] @@ -472,43 +554,59 @@ shadow_soft_radius (Soft shadow radius) float 5.0 1.0 15.0 # Simulates the tone curve of photographic film and how this approximates the # appearance of high dynamic range images. Mid-range contrast is slightly # enhanced, highlights and shadows are gradually compressed. +# +# Requires: shaders tone_mapping (Filmic tone mapping) bool false # Enable automatic exposure correction # When enabled, the post-processing engine will # automatically adjust to the brightness of the scene, # simulating the behavior of human eye. +# +# Requires: shaders enable_auto_exposure (Enable Automatic Exposure) bool false # Set the exposure compensation in EV units. # Value of 0.0 (default) means no exposure compensation. # Range: from -1 to 1.0 +# +# Requires: shaders, enable_auto_exposure exposure_compensation (Exposure compensation) float 0.0 -1.0 1.0 [**Bloom] # Set to true to enable bloom effect. # Bright colors will bleed over the neighboring objects. +# +# Requires: shaders 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_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_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_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_bloom bloom_radius (Bloom Radius) float 1 0.1 8 @@ -1660,6 +1758,8 @@ ignore_world_load_errors (Ignore world errors) bool false [**Graphics] # Path to shader directory. If no path is defined, default location will be used. +# +# Requires: shaders shader_path (Shader path) path # The rendering back-end. @@ -1694,7 +1794,6 @@ mesh_generation_interval (Mapblock mesh generation delay) int 0 0 50 # Value of 0 (default) will let Minetest autodetect the number of available threads. mesh_generation_threads (Mapblock mesh generation threads) int 0 0 8 - # Size of the MapBlock cache of the mesh generator. Increasing this will # increase the cache hit %, reducing the data being copied from the main # thread, thus reducing jitter. @@ -1721,6 +1820,9 @@ world_aligned_mode (World-aligned textures mode) enum enable disable,enable,forc # Warning: This option is EXPERIMENTAL! autoscale_mode (Autoscaling mode) enum disable disable,enable,force +# The base node texture size used for world-aligned texture autoscaling. +texture_min_size (Base texture size) int 64 1 32768 + # Side length of a cube of map blocks that the client will consider together # when generating meshes. # Larger values increase the utilization of the GPU by reducing the number of @@ -2085,10 +2187,6 @@ address (Server address) string # Note that the port field in the main menu overrides this setting. remote_port (Remote port) int 30000 1 65535 -# Default game when creating a new world. -# This will be overridden when creating a world from the main menu. -default_game (Default game) string minetest - # Enable players getting damage and dying. enable_damage (Damage) bool false diff --git a/doc/compiling/linux.md b/doc/compiling/linux.md index c595b17b21465..d977cfe20b708 100644 --- a/doc/compiling/linux.md +++ b/doc/compiling/linux.md @@ -4,7 +4,7 @@ | Dependency | Version | Commentary | | ---------- | ------- | ---------- | -| GCC | 7.5+ | or Clang 6.0+ | +| GCC | 7.5+ | or Clang 7.0.1+ | | CMake | 3.5+ | | | IrrlichtMt | - | Custom version of Irrlicht, see https://github.com/minetest/irrlicht | | Freetype | 2.0+ | | @@ -24,6 +24,10 @@ For Fedora users: sudo dnf install make automake gcc gcc-c++ kernel-devel cmake libcurl-devel openal-soft-devel libpng-devel libjpeg-devel libvorbis-devel libXi-devel libogg-devel freetype-devel mesa-libGL-devel zlib-devel jsoncpp-devel gmp-devel sqlite-devel luajit-devel leveldb-devel ncurses-devel spatialindex-devel libzstd-devel gettext +For openSUSE users: + + sudo zypper install gcc cmake libjpeg8-devel libpng16-devel openal-soft-devel libcurl-devel sqlite3-devel luajit-devel libzstd-devel + For Arch users: sudo pacman -S base-devel libcurl-gnutls cmake libxi libpng sqlite libogg libvorbis openal freetype2 jsoncpp gmp luajit leveldb ncurses zstd gettext diff --git a/doc/fst_api.txt b/doc/fst_api.txt index 294642cc3f176..9ad06362d2b75 100644 --- a/doc/fst_api.txt +++ b/doc/fst_api.txt @@ -75,7 +75,7 @@ methods: ^ handler: function(tabview,fields) --> returns true to finish button processing false to continue - set_parent(parent) ^ set parent to attach tabview to. TV's with parent are hidden if their parent - is hidden and they don't set their specified size. + is hidden and they don't set their specified size. ^ parent: component to attach to - show() ^ show tabview @@ -85,6 +85,12 @@ methods: ^ delete tabview - set_fixed_size(state) ^ true/false set to fixed size, variable size +- set_end_button(info) + ^ info is a table with: + * name: button name + * label: tooltip text + * icon: path to icon + * on_click(tabview): callback function File: fst/dialog.lua --------------------- @@ -126,30 +132,27 @@ members: File: fst/buttonbar.lua ----------------------- -buttonbar_create(name, cbf_buttonhandler, pos, orientation, size) +buttonbar_create(name, pos, size, bgcolor, cbf_buttonhandler) ^ create a buttonbar ^ name: name of component (unique per ui) -^ cbf_buttonhandler: function to be called on button pressed - function(buttonbar,buttonname,buttondata) ^ pos: position relative to upper left of current shown formspec { x, y } -^ orientation: "vertical" or "horizontal" ^ size: size of bar { - width, - height + x, + y } +^ bgcolor: background color as `ColorString` +^ cbf_buttonhandler: function to be called on button pressed + function(fields) Class reference buttonbar: methods: -- add_button(btn_id, caption, button_image) -- set_parent(parent) - ^ set parent to attach a buttonbar to - ^ parent: component to attach to +- add_button(name, caption, image, tooltip) - show() ^ show buttonbar - hide() diff --git a/doc/lua_api.md b/doc/lua_api.md index 7a6c780d21262..c4476128d9235 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -259,7 +259,18 @@ time, if necessary. (See [`Settings`]) Media files (textures, sounds, whatever) that will be transferred to the client and will be available for use by the mod and translation files for -the clients (see [Translations]). +the clients (see [Translations]). Accepted characters for names are: + + a-zA-Z0-9_.- + +Accepted formats are: + + images: .png, .jpg, .bmp, (deprecated) .tga + sounds: .ogg vorbis + models: .x, .b3d, .obj + +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) It is suggested to use the folders for the purpose they are thought for, eg. put textures into `textures`, translation files into `locale`, @@ -6443,6 +6454,7 @@ This allows you easy interoperability for delegating work to jobs. ### List of APIs available in an async environment Classes: +* `AreaStore` * `ItemStack` * `PerlinNoise` * `PerlinNoiseMap` @@ -7457,7 +7469,7 @@ child will follow movement and rotation of that bone. * `get_wield_list()`: returns the name of the inventory list the wielded item is in. * `get_wield_index()`: returns the wield list index of the wielded item (starting with 1) -* `get_wielded_item()`: returns the wielded item as an `ItemStack` +* `get_wielded_item()`: returns a copy of the wielded item as an `ItemStack` * `set_wielded_item(item)`: replaces the wielded item, returns `true` if successful. * `get_armor_groups()`: @@ -7484,8 +7496,8 @@ child will follow movement and rotation of that bone. * `frame_blend`: number, default: `0.0` * `frame_loop`: If `true`, animation will loop. If false, it will play once * default: `true` -* `get_animation()`: returns current animation parameters set by `set_animaition`: - * `range`, `frame_speed`, `frame_blend`, `frame_loop`. +* `get_animation()`: returns current animation parameters set by `set_animation`: + * `frame_range`, `frame_speed`, `frame_blend`, `frame_loop`. * `set_animation_frame_speed(frame_speed)` * Sets the frame speed of the object's animation * Unlike `set_animation`, this will not restart the animation @@ -7681,16 +7693,44 @@ child will follow movement and rotation of that bone. * 9 - zoom * Returns `0` (no bits set) if the object is not a player. * `set_physics_override(override_table)` + * Overrides the physics attributes of the player * `override_table` is a table with the following fields: - * `speed`: multiplier to default walking speed value (default: `1`) + * `speed`: multiplier to default movement speed and acceleration values (default: `1`) * `jump`: multiplier to default jump value (default: `1`) * `gravity`: multiplier to default gravity value (default: `1`) + * `speed_climb`: multiplier to default climb speed value (default: `1`) + * Note: The actual climb speed is the product of `speed` and `speed_climb` + * `speed_crouch`: multiplier to default sneak speed value (default: `1`) + * Note: The actual sneak speed is the product of `speed` and `speed_crouch` + * `liquid_fluidity`: multiplier to liquid movement resistance value + (for nodes with `liquid_move_physics`); the higher this value, the lower the + resistance to movement. At `math.huge`, the resistance is zero and you can + move through any liquid like air. (default: `1`) + * Warning: Values below 1 are currently unsupported. + * `liquid_fluidity_smooth`: multiplier to default maximum liquid resistance value + (for nodes with `liquid_move_physics`); controls deceleration when entering + node at high speed. At higher values you come to a halt more quickly + (default: `1`) + * `liquid_sink`: multiplier to default liquid sinking speed value; + (for nodes with `liquid_move_physics`) (default: `1`) + * `acceleration_default`: multiplier to horizontal and vertical acceleration + on ground or when climbing (default: `1`) + * Note: The actual acceleration is the product of `speed` and `acceleration_default` + * `acceleration_air`: multiplier to acceleration + when jumping or falling (default: `1`) + * Note: The actual acceleration is the product of `speed` and `acceleration_air` * `sneak`: whether player can sneak (default: `true`) * `sneak_glitch`: whether player can use the new move code replications of the old sneak side-effects: sneak ladders and 2 node sneak jump (default: `false`) * `new_move`: use new move/sneak code. When `false` the exact old code is used for the specific old sneak behavior (default: `true`) + * Note: All numeric fields above modify a corresponding `movement_*` setting. + * For games, we recommend for simpler code to first modify the `movement_*` + settings (e.g. via the game's `minetest.conf`) to set a global base value + for all players and only use `set_physics_override` when you need to change + from the base value on a per-player basis + * `get_physics_override()`: returns the table given to `set_physics_override` * `hud_add(hud definition)`: add a HUD element described by HUD def, returns ID number on success @@ -8774,7 +8814,11 @@ Used by `minetest.register_node`. -- Only when `paramtype2` supports palettes. post_effect_color = "#00000000", - -- Screen tint if player is inside node, see "ColorSpec" + -- Screen tint if a player is inside this node, see `ColorSpec`. + -- Color is alpha-blended over the screen. + + post_effect_color_shaded = false, + -- Determines whether `post_effect_color` is affected by lighting. paramtype = "none", -- See "Nodes" diff --git a/doc/mkdocs/build.sh b/doc/mkdocs/build.sh index 33e28acee8458..12266441cbd64 100755 --- a/doc/mkdocs/build.sh +++ b/doc/mkdocs/build.sh @@ -1,9 +1,5 @@ #!/bin/sh -e -# Patch Python-Markdown -MARKDOWN_FILE=$(pip show markdown | awk '/Location/ { print $2 }')/markdown/extensions/codehilite.py -patch -N -r - $MARKDOWN_FILE lua_highlight.patch || true - # Split lua_api.md on top level headings cat ../lua_api.md | csplit -sz -f docs/section - '/^=/-1' '{*}' diff --git a/doc/mkdocs/lua_highlight.patch b/doc/mkdocs/lua_highlight.patch deleted file mode 100644 index e231081d60552..0000000000000 --- a/doc/mkdocs/lua_highlight.patch +++ /dev/null @@ -1,9 +0,0 @@ -@@ -75,7 +75,7 @@ - css_class="codehilite", lang=None, style='default', - noclasses=False, tab_length=4, hl_lines=None, use_pygments=True): - self.src = src -- self.lang = lang -+ self.lang = "lua" - self.linenums = linenums - self.guess_lang = guess_lang - self.css_class = css_class diff --git a/doc/mkdocs/requirements.txt b/doc/mkdocs/requirements.txt index e162653126ea1..522de3aa7c9e9 100644 --- a/doc/mkdocs/requirements.txt +++ b/doc/mkdocs/requirements.txt @@ -1,2 +1,2 @@ -mkdocs~=1.3.0 -pygments~=2.12.0 +mkdocs~=1.4.3 +pygments~=2.15.1 diff --git a/games/devtest/mods/basenodes/init.lua b/games/devtest/mods/basenodes/init.lua index aca117f255383..249c7fbd84bc6 100644 --- a/games/devtest/mods/basenodes/init.lua +++ b/games/devtest/mods/basenodes/init.lua @@ -147,6 +147,7 @@ minetest.register_node("basenodes:water_source", { liquid_alternative_source = "basenodes:water_source", liquid_viscosity = WATER_VISC, post_effect_color = {a = 64, r = 100, g = 100, b = 200}, + post_effect_color_shaded = true, groups = {water = 3, liquid = 3}, }) @@ -177,6 +178,7 @@ minetest.register_node("basenodes:water_flowing", { liquid_alternative_source = "basenodes:water_source", liquid_viscosity = WATER_VISC, post_effect_color = {a = 64, r = 100, g = 100, b = 200}, + post_effect_color_shaded = true, groups = {water = 3, liquid = 3}, }) @@ -206,6 +208,7 @@ minetest.register_node("basenodes:river_water_source", { liquid_renewable = false, liquid_range = 2, post_effect_color = {a = 103, r = 30, g = 76, b = 90}, + post_effect_color_shaded = true, groups = {water = 3, liquid = 3, }, }) @@ -238,6 +241,7 @@ minetest.register_node("basenodes:river_water_flowing", { liquid_renewable = false, liquid_range = 2, post_effect_color = {a = 103, r = 30, g = 76, b = 90}, + post_effect_color_shaded = true, groups = {water = 3, liquid = 3, }, }) diff --git a/games/devtest/mods/testnodes/properties.lua b/games/devtest/mods/testnodes/properties.lua index 6271f0add924d..e75cc8b69c370 100644 --- a/games/devtest/mods/testnodes/properties.lua +++ b/games/devtest/mods/testnodes/properties.lua @@ -588,3 +588,36 @@ minetest.register_node("testnodes:drowning_1", { groups = {dig_immediate=3}, }) +-- post_effect_color_shaded + +minetest.register_node("testnodes:post_effect_color_shaded_false", { + description = S("\"post_effect_color_shaded = false\" Node"), + + drawtype = "allfaces", + tiles = {"testnodes_post_effect_color_shaded_false.png"}, + use_texture_alpha = "blend", + paramtype = "light", + sunlight_propagates = true, + post_effect_color = {a = 128, r = 255, g = 255, b = 255}, + post_effect_color_shaded = false, + + walkable = false, + is_ground_content = false, + groups = {dig_immediate=3}, +}) + +minetest.register_node("testnodes:post_effect_color_shaded_true", { + description = S("\"post_effect_color_shaded = true\" Node"), + + drawtype = "allfaces", + tiles = {"testnodes_post_effect_color_shaded_true.png"}, + use_texture_alpha = "blend", + paramtype = "light", + sunlight_propagates = true, + post_effect_color = {a = 128, r = 255, g = 255, b = 255}, + post_effect_color_shaded = true, + + walkable = false, + is_ground_content = false, + groups = {dig_immediate=3}, +}) diff --git a/games/devtest/mods/testnodes/textures/testnodes_post_effect_color_shaded_false.png b/games/devtest/mods/testnodes/textures/testnodes_post_effect_color_shaded_false.png new file mode 100644 index 0000000000000..a14713a070baf Binary files /dev/null and b/games/devtest/mods/testnodes/textures/testnodes_post_effect_color_shaded_false.png differ diff --git a/games/devtest/mods/testnodes/textures/testnodes_post_effect_color_shaded_true.png b/games/devtest/mods/testnodes/textures/testnodes_post_effect_color_shaded_true.png new file mode 100644 index 0000000000000..448cf91c8c220 Binary files /dev/null and b/games/devtest/mods/testnodes/textures/testnodes_post_effect_color_shaded_true.png differ diff --git a/games/devtest/mods/unittests/inside_async_env.lua b/games/devtest/mods/unittests/inside_async_env.lua index 4ed0fccd23aa8..7228d383dbec8 100644 --- a/games/devtest/mods/unittests/inside_async_env.lua +++ b/games/devtest/mods/unittests/inside_async_env.lua @@ -10,10 +10,17 @@ local function do_tests() assert(not core.object_refs) -- stuff that should be here assert(ItemStack) + local meta = ItemStack():get_meta() + assert(type(meta) == "userdata") + assert(type(meta.set_tool_capabilities) == "function") assert(core.registered_items[""]) + assert(next(core.registered_nodes) ~= nil) + assert(core.registered_craftitems["unittests:stick"]) -- alias handling assert(core.registered_items["unittests:steel_ingot_alias"].name == "unittests:steel_ingot") + -- fallback to item defaults + assert(core.registered_items["unittests:description_test"].on_place == true) end function unittests.async_test() diff --git a/minetest.conf.example b/minetest.conf.example index 02433473d666d..8efd3670cb48e 100644 --- a/minetest.conf.example +++ b/minetest.conf.example @@ -433,7 +433,7 @@ # Maximum distance to render shadows. # type: float min: 10 max: 1000 -# shadow_map_max_distance = 200.0 +# shadow_map_max_distance = 140.0 # Texture size to render the shadow map on. # This must be a power of two. @@ -3179,11 +3179,6 @@ # type: int min: 1 max: 65535 # remote_port = 30000 -# Default game when creating a new world. -# This will be overridden when creating a world from the main menu. -# type: string -# default_game = minetest - # Enable players getting damage and dying. # type: bool # enable_damage = false diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index 2e934ba29c710..5ec61795a169c 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -2,8 +2,15 @@ set(sound_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/sound.cpp) if(USE_SOUND) set(sound_SRCS ${sound_SRCS} - ${CMAKE_CURRENT_SOURCE_DIR}/sound_openal.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/sound_openal_internal.cpp) + ${CMAKE_CURRENT_SOURCE_DIR}/sound/al_helpers.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/sound/ogg_file.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/sound/playing_sound.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/sound/proxy_sound_manager.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/sound/sound_data.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/sound/sound_manager.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/sound/sound_openal.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/sound/sound_singleton.cpp + ) set(SOUND_INCLUDE_DIRS ${OPENAL_INCLUDE_DIR} ${VORBIS_INCLUDE_DIR} diff --git a/src/client/client.cpp b/src/client/client.cpp index 744c5eb9084b1..839ed391b3389 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -1317,9 +1317,7 @@ void Client::sendChatMessage(const std::wstring &message) m_chat_message_allowance -= 1.0f; NetworkPacket pkt(TOSERVER_CHAT_MESSAGE, 2 + message.size() * sizeof(u16)); - pkt << message; - Send(&pkt); } else if (m_out_chat_queue.size() < (u16) max_queue_size || max_queue_size < 0) { m_out_chat_queue.push(message); @@ -1701,8 +1699,11 @@ void Client::typeChatMessage(const std::wstring &message) if (message.empty()) return; + auto message_utf8 = wide_to_utf8(message); + infostream << "Typed chat message: \"" << message_utf8 << "\"" << std::endl; + // If message was consumed by script API, don't send it to server - if (m_mods_loaded && m_script->on_sending_message(wide_to_utf8(message))) + if (m_mods_loaded && m_script->on_sending_message(message_utf8)) return; // Send to others diff --git a/src/client/clientenvironment.cpp b/src/client/clientenvironment.cpp index d71b0ec338c59..633a2896c69ae 100644 --- a/src/client/clientenvironment.cpp +++ b/src/client/clientenvironment.cpp @@ -208,7 +208,7 @@ void ClientEnvironment::step(float dtime) !lplayer->swimming_vertical && !lplayer->swimming_pitch) // HACK the factor 2 for gravity is arbitrary and should be removed eventually - lplayer->gravity = 2 * lplayer->movement_liquid_sink; + lplayer->gravity = 2 * lplayer->movement_liquid_sink * lplayer->physics_override.liquid_sink; // Movement resistance if (lplayer->move_resistance > 0) { @@ -218,20 +218,22 @@ void ClientEnvironment::step(float dtime) // between 0 and 1. Should match the scale at which liquid_viscosity // increase affects other liquid attributes. static const f32 resistance_factor = 0.3f; + float fluidity = lplayer->movement_liquid_fluidity; + fluidity *= MYMAX(1.0f, lplayer->physics_override.liquid_fluidity); + fluidity = MYMAX(0.001f, fluidity); // prevent division by 0 + float fluidity_smooth = lplayer->movement_liquid_fluidity_smooth; + fluidity_smooth *= lplayer->physics_override.liquid_fluidity_smooth; + fluidity_smooth = MYMAX(0.0f, fluidity_smooth); v3f d_wanted; bool in_liquid_stable = lplayer->in_liquid_stable || lplayer->in_liquid; - if (in_liquid_stable) { - d_wanted = -speed / lplayer->movement_liquid_fluidity; - } else { + if (in_liquid_stable) + d_wanted = -speed / fluidity; + else d_wanted = -speed / BS; - } f32 dl = d_wanted.getLength(); - if (in_liquid_stable) { - if (dl > lplayer->movement_liquid_fluidity_smooth) - dl = lplayer->movement_liquid_fluidity_smooth; - } - + if (in_liquid_stable) + dl = MYMIN(dl, fluidity_smooth); dl *= (lplayer->move_resistance * resistance_factor) + (1 - resistance_factor); v3f d = d_wanted.normalize() * (dl * dtime_part * 100.0f); diff --git a/src/client/clientlauncher.cpp b/src/client/clientlauncher.cpp index 72452f9c289f1..d8384bfcbf52d 100644 --- a/src/client/clientlauncher.cpp +++ b/src/client/clientlauncher.cpp @@ -36,7 +36,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "network/networkexceptions.h" #if USE_SOUND - #include "sound_openal.h" + #include "sound/sound_openal.h" #endif /* mainmenumanager.h @@ -277,9 +277,8 @@ bool ClientLauncher::run(GameStartData &start_data, const Settings &cmd_args) #ifdef NDEBUG catch (std::exception &e) { - std::string error_message = "Some exception: \""; - error_message += e.what(); - error_message += "\""; + error_message = "Some exception: "; + error_message.append(debug_describe_exc(e)); errorstream << error_message << std::endl; } #endif @@ -398,11 +397,6 @@ bool ClientLauncher::launch_game(std::string &error_message, spec.path = start_data.world_path; spec.gameid = getWorldGameId(spec.path, true); spec.name = _("[--world parameter]"); - - if (spec.gameid.empty()) { // Create new - spec.gameid = g_settings->get("default_game"); - spec.name += " [new]"; - } } /* Show the GUI menu diff --git a/src/client/clientmap.cpp b/src/client/clientmap.cpp index 00218cc55cca6..b831724b36956 100644 --- a/src/client/clientmap.cpp +++ b/src/client/clientmap.cpp @@ -19,6 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "clientmap.h" #include "client.h" +#include "client/mesh.h" #include "mapblock_mesh.h" #include #include @@ -124,7 +125,7 @@ ClientMap::~ClientMap() g_settings->deregisterChangedCallback("enable_raytraced_culling", on_settings_changed, this); } -void ClientMap::updateCamera(v3f pos, v3f dir, f32 fov, v3s16 offset) +void ClientMap::updateCamera(v3f pos, v3f dir, f32 fov, v3s16 offset, video::SColor light_color) { v3s16 previous_node = floatToInt(m_camera_position, BS) + m_camera_offset; v3s16 previous_block = getContainerPos(previous_node, MAP_BLOCKSIZE); @@ -133,6 +134,7 @@ void ClientMap::updateCamera(v3f pos, v3f dir, f32 fov, v3s16 offset) m_camera_direction = dir; m_camera_fov = fov; m_camera_offset = offset; + m_camera_light_color = light_color; v3s16 current_node = floatToInt(m_camera_position, BS) + m_camera_offset; v3s16 current_block = getContainerPos(current_node, MAP_BLOCKSIZE); @@ -843,7 +845,7 @@ void ClientMap::renderMap(video::IVideoDriver* driver, s32 pass) // Apply filter settings material.forEachTexture([this] (auto &tex) { - tex.setFiltersMinetest(m_cache_bilinear_filter, m_cache_trilinear_filter, + setMaterialFilters(tex, m_cache_bilinear_filter, m_cache_trilinear_filter, m_cache_anistropic_filter); }); material.Wireframe = m_control.show_wireframe; @@ -1056,21 +1058,30 @@ void ClientMap::renderPostFx(CameraMode cam_mode) MapNode n = getNode(floatToInt(m_camera_position, BS)); const ContentFeatures& features = m_nodedef->get(n); - video::SColor post_effect_color = features.post_effect_color; + video::SColor post_color = features.post_effect_color; + + if (features.post_effect_color_shaded) { + auto apply_light = [] (u32 color, u32 light) { + return core::clamp(core::round32(color * light / 255.0f), 0, 255); + }; + post_color.setRed(apply_light(post_color.getRed(), m_camera_light_color.getRed())); + post_color.setGreen(apply_light(post_color.getGreen(), m_camera_light_color.getGreen())); + post_color.setBlue(apply_light(post_color.getBlue(), m_camera_light_color.getBlue())); + } // If the camera is in a solid node, make everything black. // (first person mode only) if (features.solidness == 2 && cam_mode == CAMERA_MODE_FIRST && - !m_control.allow_noclip) { - post_effect_color = video::SColor(255, 0, 0, 0); + !m_control.allow_noclip) { + post_color = video::SColor(255, 0, 0, 0); } - if (post_effect_color.getAlpha() != 0) { + if (post_color.getAlpha() != 0) { // Draw a full-screen rectangle video::IVideoDriver* driver = SceneManager->getVideoDriver(); v2u32 ss = driver->getScreenSize(); core::rect rect(0,0, ss.X, ss.Y); - driver->draw2DRectangle(post_effect_color, rect); + driver->draw2DRectangle(post_color, rect); } } @@ -1187,8 +1198,14 @@ void ClientMap::renderMapShadows(video::IVideoDriver *driver, // override some material properties video::SMaterial local_material = buf->getMaterial(); local_material.MaterialType = material.MaterialType; - local_material.BackfaceCulling = material.BackfaceCulling; - local_material.FrontfaceCulling = material.FrontfaceCulling; + // do not override culling if the original material renders both back + // and front faces in solid mode (e.g. plantlike) + // Transparent plants would still render shadows only from one side, + // but this conflicts with water which occurs much more frequently + if (is_transparent_pass || local_material.BackfaceCulling || local_material.FrontfaceCulling) { + local_material.BackfaceCulling = material.BackfaceCulling; + local_material.FrontfaceCulling = material.FrontfaceCulling; + } local_material.BlendOperation = material.BlendOperation; local_material.Lighting = false; driver->setMaterial(local_material); diff --git a/src/client/clientmap.h b/src/client/clientmap.h index 4d565ec9fa274..13c320d9a2bf3 100644 --- a/src/client/clientmap.h +++ b/src/client/clientmap.h @@ -88,7 +88,7 @@ class ClientMap : public Map, public scene::ISceneNode ISceneNode::drop(); // calls destructor } - void updateCamera(v3f pos, v3f dir, f32 fov, v3s16 offset); + void updateCamera(v3f pos, v3f dir, f32 fov, v3s16 offset, video::SColor light_color); /* Forcefully get a sector from somewhere @@ -201,6 +201,7 @@ class ClientMap : public Map, public scene::ISceneNode v3f m_camera_direction = v3f(0,0,1); f32 m_camera_fov = M_PI; v3s16 m_camera_offset; + video::SColor m_camera_light_color = video::SColor(0xFFFFFFFF); bool m_needs_update_transparent_meshes = true; std::map m_drawlist; diff --git a/src/client/content_cao.cpp b/src/client/content_cao.cpp index 28f9d18cbadd9..ad316209a3c42 100644 --- a/src/client/content_cao.cpp +++ b/src/client/content_cao.cpp @@ -357,7 +357,6 @@ bool GenericCAO::collideWithObjects() const void GenericCAO::initialize(const std::string &data) { - infostream<<"GenericCAO: Got init data"<getBool("enable_shaders"); @@ -393,8 +392,7 @@ void GenericCAO::processInitData(const std::string &data) } const u8 num_messages = readU8(is); - - for (int i = 0; i < num_messages; i++) { + for (u8 i = 0; i < num_messages; i++) { std::string message = deSerializeString32(is); processMessage(message); } @@ -1355,7 +1353,7 @@ void GenericCAO::updateTextures(std::string mod) } material.forEachTexture([=] (auto &tex) { - tex.setFiltersMinetest(use_bilinear_filter, use_trilinear_filter, + setMaterialFilters(tex, use_bilinear_filter, use_trilinear_filter, use_anisotropic_filter); }); } @@ -1383,15 +1381,8 @@ void GenericCAO::updateTextures(std::string mod) material.Lighting = true; material.BackfaceCulling = m_prop.backface_culling; - // don't filter low-res textures, makes them look blurry - // player models have a res of 64 - const core::dimension2d &size = texture->getOriginalSize(); - const u32 res = std::min(size.Height, size.Width); - use_trilinear_filter &= res > 64; - use_bilinear_filter &= res > 64; - material.forEachTexture([=] (auto &tex) { - tex.setFiltersMinetest(use_bilinear_filter, use_trilinear_filter, + setMaterialFilters(tex, use_bilinear_filter, use_trilinear_filter, use_anisotropic_filter); }); } @@ -1438,7 +1429,7 @@ void GenericCAO::updateTextures(std::string mod) } material.forEachTexture([=] (auto &tex) { - tex.setFiltersMinetest(use_bilinear_filter, use_trilinear_filter, + setMaterialFilters(tex, use_bilinear_filter, use_trilinear_filter, use_anisotropic_filter); }); } @@ -1463,7 +1454,7 @@ void GenericCAO::updateTextures(std::string mod) } material.forEachTexture([=] (auto &tex) { - tex.setFiltersMinetest(use_bilinear_filter, use_trilinear_filter, + setMaterialFilters(tex, use_bilinear_filter, use_trilinear_filter, use_anisotropic_filter); }); } @@ -1492,7 +1483,7 @@ void GenericCAO::updateTextures(std::string mod) } material.forEachTexture([=] (auto &tex) { - tex.setFiltersMinetest(use_bilinear_filter, use_trilinear_filter, + setMaterialFilters(tex, use_bilinear_filter, use_trilinear_filter, use_anisotropic_filter); }); } @@ -1711,8 +1702,6 @@ void GenericCAO::processMessage(const std::string &data) if (expire_visuals) { expireVisuals(); } else { - infostream << "GenericCAO: properties updated but expiring visuals" - << " not necessary" << std::endl; if (textures_changed) { // don't update while punch texture modifier is active if (m_reset_textures_timer < 0) @@ -1782,6 +1771,23 @@ void GenericCAO::processMessage(const std::string &data) bool sneak_glitch = !readU8(is); bool new_move = !readU8(is); + float override_speed_climb = readF32(is); + float override_speed_crouch = readF32(is); + float override_liquid_fluidity = readF32(is); + float override_liquid_fluidity_smooth = readF32(is); + float override_liquid_sink = readF32(is); + float override_acceleration_default = readF32(is); + float override_acceleration_air = readF32(is); + // fallback for new overrides (since 5.8.0) + if (is.eof()) { + override_speed_climb = 1.0f; + override_speed_crouch = 1.0f; + override_liquid_fluidity = 1.0f; + override_liquid_fluidity_smooth = 1.0f; + override_liquid_sink = 1.0f; + override_acceleration_default = 1.0f; + override_acceleration_air = 1.0f; + } if (m_is_local_player) { auto &phys = m_env->getLocalPlayer()->physics_override; @@ -1791,6 +1797,13 @@ void GenericCAO::processMessage(const std::string &data) phys.sneak = sneak; phys.sneak_glitch = sneak_glitch; phys.new_move = new_move; + phys.speed_climb = override_speed_climb; + phys.speed_crouch = override_speed_crouch; + phys.liquid_fluidity = override_liquid_fluidity; + phys.liquid_fluidity_smooth = override_liquid_fluidity_smooth; + phys.liquid_sink = override_liquid_sink; + phys.acceleration_default = override_acceleration_default; + phys.acceleration_air = override_acceleration_air; } } else if (cmd == AO_CMD_SET_ANIMATION) { // TODO: change frames send as v2s32 value diff --git a/src/client/content_cao.h b/src/client/content_cao.h index 69c7cfe08a486..62d090a6ff11d 100644 --- a/src/client/content_cao.h +++ b/src/client/content_cao.h @@ -144,7 +144,7 @@ class GenericCAO : public ClientActiveObject return new GenericCAO(client, env); } - inline ActiveObjectType getType() const + inline ActiveObjectType getType() const override { return ACTIVEOBJECT_TYPE_GENERIC; } @@ -152,15 +152,15 @@ class GenericCAO : public ClientActiveObject { return m_armor_groups; } - void initialize(const std::string &data); + void initialize(const std::string &data) override; void processInitData(const std::string &data); - bool getCollisionBox(aabb3f *toset) const; + bool getCollisionBox(aabb3f *toset) const override; - bool collideWithObjects() const; + bool collideWithObjects() const override; - virtual bool getSelectionBox(aabb3f *toset) const; + virtual bool getSelectionBox(aabb3f *toset) const override; const v3f getPosition() const override final; @@ -172,9 +172,9 @@ class GenericCAO : public ClientActiveObject inline const ObjectProperties &getProperties() const { return m_prop; } - scene::ISceneNode *getSceneNode() const; + scene::ISceneNode *getSceneNode() const override; - scene::IAnimatedMeshSceneNode *getAnimatedMeshSceneNode() const; + scene::IAnimatedMeshSceneNode *getAnimatedMeshSceneNode() const override; // m_matrixnode controls the position and rotation of the child node // for all scene nodes, as a workaround for an Irrlicht problem with @@ -201,7 +201,7 @@ class GenericCAO : public ClientActiveObject return m_prop.stepheight; } - inline bool isLocalPlayer() const + inline bool isLocalPlayer() const override { return m_is_local_player; } @@ -218,28 +218,28 @@ class GenericCAO : public ClientActiveObject void setChildrenVisible(bool toset); void setAttachment(int parent_id, const std::string &bone, v3f position, - v3f rotation, bool force_visible); + v3f rotation, bool force_visible) override; void getAttachment(int *parent_id, std::string *bone, v3f *position, - v3f *rotation, bool *force_visible) const; - void clearChildAttachments(); - void clearParentAttachment(); - void addAttachmentChild(int child_id); - void removeAttachmentChild(int child_id); - ClientActiveObject *getParent() const; - const std::unordered_set &getAttachmentChildIds() const + v3f *rotation, bool *force_visible) const override; + void clearChildAttachments() override; + void clearParentAttachment() override; + void addAttachmentChild(int child_id) override; + void removeAttachmentChild(int child_id) override; + ClientActiveObject *getParent() const override; + const std::unordered_set &getAttachmentChildIds() const override { return m_attachment_child_ids; } - void updateAttachments(); + void updateAttachments() override; - void removeFromScene(bool permanent); + void removeFromScene(bool permanent) override; - void addToScene(ITextureSource *tsrc, scene::ISceneManager *smgr); + void addToScene(ITextureSource *tsrc, scene::ISceneManager *smgr) override; inline void expireVisuals() { m_visuals_expired = true; } - void updateLight(u32 day_night_ratio); + void updateLight(u32 day_night_ratio) override; void setNodeLight(const video::SColor &light); @@ -254,7 +254,7 @@ class GenericCAO : public ClientActiveObject void updateNodePos(); - void step(float dtime, ClientEnvironment *env); + void step(float dtime, ClientEnvironment *env) override; void updateTexturePos(); @@ -268,14 +268,14 @@ class GenericCAO : public ClientActiveObject void updateBonePosition(); - void processMessage(const std::string &data); + void processMessage(const std::string &data) override; bool directReportPunch(v3f dir, const ItemStack *punchitem=NULL, - float time_from_last_punch=1000000); + float time_from_last_punch=1000000) override; - std::string debugInfoText(); + std::string debugInfoText() override; - std::string infoText() + std::string infoText() override { return m_prop.infotext; } diff --git a/src/client/game.cpp b/src/client/game.cpp index 32787cad3486d..b28cb70603bb4 100644 --- a/src/client/game.cpp +++ b/src/client/game.cpp @@ -31,6 +31,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "client/keys.h" #include "client/joystick_controller.h" #include "client/mapblock_mesh.h" +#include "client/sound.h" #include "clientmap.h" #include "clouds.h" #include "config.h" @@ -75,10 +76,9 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "clientdynamicinfo.h" #if USE_SOUND - #include "client/sound_openal.h" -#else - #include "client/sound.h" + #include "client/sound/sound_openal.h" #endif + /* Text input system */ @@ -578,7 +578,7 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter m_saturation_pixel.set(&saturation, services); } - void onSetMaterial(const video::SMaterial &material) + void onSetMaterial(const video::SMaterial &material) override { video::ITexture *texture = material.getTexture(0); if (texture) { @@ -972,7 +972,7 @@ class Game { bool m_first_loop_after_window_activation = false; bool m_camera_offset_changed = false; - bool m_game_focused; + bool m_game_focused = false; bool m_does_lost_focus_pause_game = false; @@ -1951,7 +1951,7 @@ void Game::processUserInput(f32 dtime) { // Reset input if window not active or some menu is active if (!device->isWindowActive() || isMenuActive() || guienv->hasFocus(gui_chat_console)) { - if(m_game_focused) { + if (m_game_focused) { m_game_focused = false; infostream << "Game lost focus" << std::endl; input->releaseAllKeys(); @@ -2161,6 +2161,14 @@ void Game::processItemSelection(u16 *new_playeritem) } } +#ifdef HAVE_TOUCHSCREENGUI + if (g_touchscreengui) { + std::optional selection = g_touchscreengui->getHotbarSelection(); + if (selection) + *new_playeritem = *selection; + } +#endif + // Clamp selection again in case it wasn't changed but max_item was *new_playeritem = MYMIN(*new_playeritem, max_item); } @@ -3162,7 +3170,7 @@ void Game::updateCamera(f32 dtime) v3f camera_direction = camera->getDirection(); client->getEnv().getClientMap().updateCamera(camera_position, - camera_direction, camera_fov, camera_offset); + camera_direction, camera_fov, camera_offset, player->light_color); if (m_camera_offset_changed) { client->updateCameraOffset(camera_offset); @@ -3619,10 +3627,10 @@ bool Game::nodePlacement(const ItemDefinition &selected_def, // Compare core.item_place_node() for what the server does with param2 MapNode predicted_node(id, 0, 0); - const u8 place_param2 = selected_def.place_param2; + const auto place_param2 = selected_def.place_param2; if (place_param2) { - predicted_node.setParam2(place_param2); + predicted_node.setParam2(*place_param2); } else if (predicted_f.param_type_2 == CPT2_WALLMOUNTED || predicted_f.param_type_2 == CPT2_COLORED_WALLMOUNTED) { v3s16 dir = nodepos - neighborpos; diff --git a/src/client/gameui.cpp b/src/client/gameui.cpp index d448eadd6147d..a4ab44e60c892 100644 --- a/src/client/gameui.cpp +++ b/src/client/gameui.cpp @@ -266,8 +266,7 @@ void GameUI::updateProfiler() os << " Profiler page " << (int)m_profiler_current_page << ", elapsed: " << g_profiler->getElapsedMs() << " ms)" << std::endl; - int lines = g_profiler->print(os, m_profiler_current_page, m_profiler_max_page); - ++lines; + g_profiler->print(os, m_profiler_current_page, m_profiler_max_page); EnrichedString str(utf8_to_wide(os.str())); str.setBackground(video::SColor(120, 0, 0, 0)); diff --git a/src/client/hud.cpp b/src/client/hud.cpp index f05131e1f4629..5d3de7bfbf670 100644 --- a/src/client/hud.cpp +++ b/src/client/hud.cpp @@ -223,16 +223,12 @@ void Hud::drawItem(const ItemStack &item, const core::rect& rect, client, selected ? IT_ROT_SELECTED : IT_ROT_NONE); } -//NOTE: selectitem = 0 -> no selected; selectitem 1-based +// NOTE: selectitem = 0 -> no selected; selectitem is 1-based // mainlist can be NULL, but draw the frame anyway. void Hud::drawItems(v2s32 upperleftpos, v2s32 screen_offset, s32 itemcount, - s32 inv_offset, InventoryList *mainlist, u16 selectitem, u16 direction) + s32 inv_offset, InventoryList *mainlist, u16 selectitem, u16 direction, + bool is_hotbar) { -#ifdef HAVE_TOUCHSCREENGUI - if (g_touchscreengui && inv_offset == 0) - g_touchscreengui->resetHud(); -#endif - s32 height = m_hotbar_imagesize + m_padding * 2; s32 width = (itemcount - inv_offset) * (m_hotbar_imagesize + m_padding * 2); @@ -292,11 +288,13 @@ void Hud::drawItems(v2s32 upperleftpos, v2s32 screen_offset, s32 itemcount, break; } - drawItem(mainlist->getItem(i), (imgrect + pos + steppos), (i + 1) == selectitem); + core::rect item_rect = imgrect + pos + steppos; + + drawItem(mainlist->getItem(i), item_rect, (i + 1) == selectitem); #ifdef HAVE_TOUCHSCREENGUI - if (g_touchscreengui) - g_touchscreengui->registerHudItem(i, (imgrect + pos + steppos)); + if (is_hotbar && g_touchscreengui) + g_touchscreengui->registerHotbarRect(i, item_rect); #endif } } @@ -406,7 +404,7 @@ void Hud::drawLuaElements(const v3s16 &camera_offset) if (!inv) warningstream << "HUD: Unknown inventory list. name=" << e->text << std::endl; drawItems(pos, v2s32(e->offset.X, e->offset.Y), e->number, 0, - inv, e->item, e->dir); + inv, e->item, e->dir, false); break; } case HUD_ELEM_WAYPOINT: { if (!calculateScreenPos(camera_offset, e, &pos)) @@ -739,16 +737,21 @@ void Hud::drawStatbar(v2s32 pos, u16 corner, u16 drawdir, } -void Hud::drawHotbar(u16 playeritem) { - - v2s32 centerlowerpos(m_displaycenter.X, m_screensize.Y); +void Hud::drawHotbar(u16 playeritem) +{ +#ifdef HAVE_TOUCHSCREENGUI + if (g_touchscreengui) + g_touchscreengui->resetHotbarRects(); +#endif InventoryList *mainlist = inventory->getList("main"); if (mainlist == NULL) { - //silently ignore this we may not be initialized completely + // Silently ignore this. We may not be initialized completely. return; } + v2s32 centerlowerpos(m_displaycenter.X, m_screensize.Y); + s32 hotbar_itemcount = player->hud_hotbar_itemcount; s32 width = hotbar_itemcount * (m_hotbar_imagesize + m_padding * 2); v2s32 pos = centerlowerpos - v2s32(width / 2, m_hotbar_imagesize + m_padding * 3); @@ -757,7 +760,7 @@ void Hud::drawHotbar(u16 playeritem) { if ((float) width / (float) window_size.X <= g_settings->getFloat("hud_hotbar_max_width")) { if (player->hud_flags & HUD_FLAG_HOTBAR_VISIBLE) { - drawItems(pos, v2s32(0, 0), hotbar_itemcount, 0, mainlist, playeritem + 1, 0); + drawItems(pos, v2s32(0, 0), hotbar_itemcount, 0, mainlist, playeritem + 1, 0, true); } } else { pos.X += width/4; @@ -767,9 +770,9 @@ void Hud::drawHotbar(u16 playeritem) { if (player->hud_flags & HUD_FLAG_HOTBAR_VISIBLE) { drawItems(pos, v2s32(0, 0), hotbar_itemcount / 2, 0, - mainlist, playeritem + 1, 0); + mainlist, playeritem + 1, 0, true); drawItems(secondpos, v2s32(0, 0), hotbar_itemcount, - hotbar_itemcount / 2, mainlist, playeritem + 1, 0); + hotbar_itemcount / 2, mainlist, playeritem + 1, 0, true); } } } @@ -777,44 +780,52 @@ void Hud::drawHotbar(u16 playeritem) { void Hud::drawCrosshair() { + auto draw_image_crosshair = [this] (video::ITexture *tex) { + core::dimension2di orig_size(tex->getOriginalSize()); + core::dimension2di scaled_size( + core::round32(orig_size.Width * m_scale_factor), + core::round32(orig_size.Height * m_scale_factor)); + + core::rect src_rect(orig_size); + core::position2d pos(m_displaycenter.X - scaled_size.Width / 2, + m_displaycenter.Y - scaled_size.Height / 2); + core::rect dest_rect(pos, scaled_size); + + video::SColor colors[] = { crosshair_argb, crosshair_argb, + crosshair_argb, crosshair_argb }; + + draw2DImageFilterScaled(driver, tex, dest_rect, src_rect, + nullptr, colors, true); + }; + if (pointing_at_object) { if (use_object_crosshair_image) { - video::ITexture *object_crosshair = tsrc->getTexture("object_crosshair.png"); - v2u32 size = object_crosshair->getOriginalSize(); - v2s32 lsize = v2s32(m_displaycenter.X - (size.X / 2), - m_displaycenter.Y - (size.Y / 2)); - driver->draw2DImage(object_crosshair, lsize, - core::rect(0, 0, size.X, size.Y), - nullptr, crosshair_argb, true); + draw_image_crosshair(tsrc->getTexture("object_crosshair.png")); } else { + s32 line_size = core::round32(OBJECT_CROSSHAIR_LINE_SIZE * m_scale_factor); + driver->draw2DLine( - m_displaycenter - v2s32(OBJECT_CROSSHAIR_LINE_SIZE, - OBJECT_CROSSHAIR_LINE_SIZE), - m_displaycenter + v2s32(OBJECT_CROSSHAIR_LINE_SIZE, - OBJECT_CROSSHAIR_LINE_SIZE), crosshair_argb); + m_displaycenter - v2s32(line_size, line_size), + m_displaycenter + v2s32(line_size, line_size), + crosshair_argb); driver->draw2DLine( - m_displaycenter + v2s32(OBJECT_CROSSHAIR_LINE_SIZE, - -OBJECT_CROSSHAIR_LINE_SIZE), - m_displaycenter + v2s32(-OBJECT_CROSSHAIR_LINE_SIZE, - OBJECT_CROSSHAIR_LINE_SIZE), crosshair_argb); + m_displaycenter + v2s32(line_size, -line_size), + m_displaycenter + v2s32(-line_size, line_size), + crosshair_argb); } return; } if (use_crosshair_image) { - video::ITexture *crosshair = tsrc->getTexture("crosshair.png"); - v2u32 size = crosshair->getOriginalSize(); - v2s32 lsize = v2s32(m_displaycenter.X - (size.X / 2), - m_displaycenter.Y - (size.Y / 2)); - driver->draw2DImage(crosshair, lsize, - core::rect(0, 0, size.X, size.Y), - nullptr, crosshair_argb, true); + draw_image_crosshair(tsrc->getTexture("crosshair.png")); } else { - driver->draw2DLine(m_displaycenter - v2s32(CROSSHAIR_LINE_SIZE, 0), - m_displaycenter + v2s32(CROSSHAIR_LINE_SIZE, 0), crosshair_argb); - driver->draw2DLine(m_displaycenter - v2s32(0, CROSSHAIR_LINE_SIZE), - m_displaycenter + v2s32(0, CROSSHAIR_LINE_SIZE), crosshair_argb); + s32 line_size = core::round32(CROSSHAIR_LINE_SIZE * m_scale_factor); + + driver->draw2DLine(m_displaycenter - v2s32(line_size, 0), + m_displaycenter + v2s32(line_size, 0), crosshair_argb); + driver->draw2DLine(m_displaycenter - v2s32(0, line_size), + m_displaycenter + v2s32(0, line_size), crosshair_argb); } } diff --git a/src/client/hud.h b/src/client/hud.h index b6ff84243c3c9..303feb78375c5 100644 --- a/src/client/hud.h +++ b/src/client/hud.h @@ -101,7 +101,7 @@ class Hud void drawItems(v2s32 upperleftpos, v2s32 screen_offset, s32 itemcount, s32 inv_offset, InventoryList *mainlist, u16 selectitem, - u16 direction); + u16 direction, bool is_hotbar); void drawItem(const ItemStack &item, const core::rect &rect, bool selected); diff --git a/src/client/keycode.h b/src/client/keycode.h index 7036705d16a48..1abaa97bc79b3 100644 --- a/src/client/keycode.h +++ b/src/client/keycode.h @@ -38,7 +38,9 @@ class KeyPress bool operator==(const KeyPress &o) const { - return (Char > 0 && Char == o.Char) || (valid_kcode(Key) && Key == o.Key); + if (valid_kcode(Key) && valid_kcode(o.Key)) + return Key == o.Key; + return Char > 0 && Char == o.Char; } const char *sym() const; diff --git a/src/client/localplayer.cpp b/src/client/localplayer.cpp index a7bcc968950c2..6e5458c8a52d9 100644 --- a/src/client/localplayer.cpp +++ b/src/client/localplayer.cpp @@ -557,7 +557,7 @@ void LocalPlayer::applyControl(float dtime, Environment *env) speedV.Y = -movement_speed_walk; swimming_vertical = true; } else if (is_climbing && !m_disable_descend) { - speedV.Y = -movement_speed_climb; + speedV.Y = -movement_speed_climb * physics_override.speed_climb; } else { // If not free movement but fast is allowed, aux1 is // "Turbo button" @@ -595,7 +595,7 @@ void LocalPlayer::applyControl(float dtime, Environment *env) if (fast_climb) speedV.Y = -movement_speed_fast; else - speedV.Y = -movement_speed_climb; + speedV.Y = -movement_speed_climb * physics_override.speed_climb; } } } @@ -647,7 +647,7 @@ void LocalPlayer::applyControl(float dtime, Environment *env) if (fast_climb) speedV.Y = movement_speed_fast; else - speedV.Y = movement_speed_climb; + speedV.Y = movement_speed_climb * physics_override.speed_climb; } } @@ -656,7 +656,7 @@ void LocalPlayer::applyControl(float dtime, Environment *env) ((in_liquid || in_liquid_stable) && fast_climb)) speedH = speedH.normalize() * movement_speed_fast; else if (control.sneak && !free_move && !in_liquid && !in_liquid_stable) - speedH = speedH.normalize() * movement_speed_crouch; + speedH = speedH.normalize() * movement_speed_crouch * physics_override.speed_crouch; else speedH = speedH.normalize() * movement_speed_walk; @@ -671,13 +671,13 @@ void LocalPlayer::applyControl(float dtime, Environment *env) if (superspeed || (fast_move && control.aux1)) incH = movement_acceleration_fast * BS * dtime; else - incH = movement_acceleration_air * BS * dtime; + incH = movement_acceleration_air * physics_override.acceleration_air * BS * dtime; incV = 0.0f; // No vertical acceleration in air } else if (superspeed || (is_climbing && fast_climb) || ((in_liquid || in_liquid_stable) && fast_climb)) { incH = incV = movement_acceleration_fast * BS * dtime; } else { - incH = incV = movement_acceleration_default * BS * dtime; + incH = incV = movement_acceleration_default * physics_override.acceleration_default * BS * dtime; } float slip_factor = 1.0f; diff --git a/src/client/mapblock_mesh.cpp b/src/client/mapblock_mesh.cpp index 4545b635048c1..09ca5262b9e9c 100644 --- a/src/client/mapblock_mesh.cpp +++ b/src/client/mapblock_mesh.cpp @@ -409,36 +409,36 @@ void getNodeTile(MapNode mn, const v3s16 &p, const v3s16 &dir, MeshMakeData *dat u8 tile; TileRotation rotation; } dir_to_tile[24][8] = { - // 0 +X +Y +Z -Z -Y -X -> value=tile,rotation - 0,R0, 2,R0 , 0,R0 , 4,R0 , 0,R0, 5,R0 , 1,R0 , 3,R0 , // rotate around y+ 0 - 3 - 0,R0, 4,R0 , 0,R3 , 3,R0 , 0,R0, 2,R0 , 1,R1 , 5,R0 , - 0,R0, 3,R0 , 0,R2 , 5,R0 , 0,R0, 4,R0 , 1,R2 , 2,R0 , - 0,R0, 5,R0 , 0,R1 , 2,R0 , 0,R0, 3,R0 , 1,R3 , 4,R0 , - - 0,R0, 2,R3 , 5,R0 , 0,R2 , 0,R0, 1,R0 , 4,R2 , 3,R1 , // rotate around z+ 4 - 7 - 0,R0, 4,R3 , 2,R0 , 0,R1 , 0,R0, 1,R1 , 3,R2 , 5,R1 , - 0,R0, 3,R3 , 4,R0 , 0,R0 , 0,R0, 1,R2 , 5,R2 , 2,R1 , - 0,R0, 5,R3 , 3,R0 , 0,R3 , 0,R0, 1,R3 , 2,R2 , 4,R1 , - - 0,R0, 2,R1 , 4,R2 , 1,R2 , 0,R0, 0,R0 , 5,R0 , 3,R3 , // rotate around z- 8 - 11 - 0,R0, 4,R1 , 3,R2 , 1,R3 , 0,R0, 0,R3 , 2,R0 , 5,R3 , - 0,R0, 3,R1 , 5,R2 , 1,R0 , 0,R0, 0,R2 , 4,R0 , 2,R3 , - 0,R0, 5,R1 , 2,R2 , 1,R1 , 0,R0, 0,R1 , 3,R0 , 4,R3 , - - 0,R0, 0,R3 , 3,R3 , 4,R1 , 0,R0, 5,R3 , 2,R3 , 1,R3 , // rotate around x+ 12 - 15 - 0,R0, 0,R2 , 5,R3 , 3,R1 , 0,R0, 2,R3 , 4,R3 , 1,R0 , - 0,R0, 0,R1 , 2,R3 , 5,R1 , 0,R0, 4,R3 , 3,R3 , 1,R1 , - 0,R0, 0,R0 , 4,R3 , 2,R1 , 0,R0, 3,R3 , 5,R3 , 1,R2 , - - 0,R0, 1,R1 , 2,R1 , 4,R3 , 0,R0, 5,R1 , 3,R1 , 0,R1 , // rotate around x- 16 - 19 - 0,R0, 1,R2 , 4,R1 , 3,R3 , 0,R0, 2,R1 , 5,R1 , 0,R0 , - 0,R0, 1,R3 , 3,R1 , 5,R3 , 0,R0, 4,R1 , 2,R1 , 0,R3 , - 0,R0, 1,R0 , 5,R1 , 2,R3 , 0,R0, 3,R1 , 4,R1 , 0,R2 , - - 0,R0, 3,R2 , 1,R2 , 4,R2 , 0,R0, 5,R2 , 0,R2 , 2,R2 , // rotate around y- 20 - 23 - 0,R0, 5,R2 , 1,R3 , 3,R2 , 0,R0, 2,R2 , 0,R1 , 4,R2 , - 0,R0, 2,R2 , 1,R0 , 5,R2 , 0,R0, 4,R2 , 0,R0 , 3,R2 , - 0,R0, 4,R2 , 1,R1 , 2,R2 , 0,R0, 3,R2 , 0,R3 , 5,R2 + // 0 +X +Y +Z -Z -Y -X -> value=tile,rotation + {{0,R0}, {2,R0}, {0,R0}, {4,R0}, {0,R0}, {5,R0}, {1,R0}, {3,R0}}, // rotate around y+ 0 - 3 + {{0,R0}, {4,R0}, {0,R3}, {3,R0}, {0,R0}, {2,R0}, {1,R1}, {5,R0}}, + {{0,R0}, {3,R0}, {0,R2}, {5,R0}, {0,R0}, {4,R0}, {1,R2}, {2,R0}}, + {{0,R0}, {5,R0}, {0,R1}, {2,R0}, {0,R0}, {3,R0}, {1,R3}, {4,R0}}, + + {{0,R0}, {2,R3}, {5,R0}, {0,R2}, {0,R0}, {1,R0}, {4,R2}, {3,R1}}, // rotate around z+ 4 - 7 + {{0,R0}, {4,R3}, {2,R0}, {0,R1}, {0,R0}, {1,R1}, {3,R2}, {5,R1}}, + {{0,R0}, {3,R3}, {4,R0}, {0,R0}, {0,R0}, {1,R2}, {5,R2}, {2,R1}}, + {{0,R0}, {5,R3}, {3,R0}, {0,R3}, {0,R0}, {1,R3}, {2,R2}, {4,R1}}, + + {{0,R0}, {2,R1}, {4,R2}, {1,R2}, {0,R0}, {0,R0}, {5,R0}, {3,R3}}, // rotate around z- 8 - 11 + {{0,R0}, {4,R1}, {3,R2}, {1,R3}, {0,R0}, {0,R3}, {2,R0}, {5,R3}}, + {{0,R0}, {3,R1}, {5,R2}, {1,R0}, {0,R0}, {0,R2}, {4,R0}, {2,R3}}, + {{0,R0}, {5,R1}, {2,R2}, {1,R1}, {0,R0}, {0,R1}, {3,R0}, {4,R3}}, + + {{0,R0}, {0,R3}, {3,R3}, {4,R1}, {0,R0}, {5,R3}, {2,R3}, {1,R3}}, // rotate around x+ 12 - 15 + {{0,R0}, {0,R2}, {5,R3}, {3,R1}, {0,R0}, {2,R3}, {4,R3}, {1,R0}}, + {{0,R0}, {0,R1}, {2,R3}, {5,R1}, {0,R0}, {4,R3}, {3,R3}, {1,R1}}, + {{0,R0}, {0,R0}, {4,R3}, {2,R1}, {0,R0}, {3,R3}, {5,R3}, {1,R2}}, + + {{0,R0}, {1,R1}, {2,R1}, {4,R3}, {0,R0}, {5,R1}, {3,R1}, {0,R1}}, // rotate around x- 16 - 19 + {{0,R0}, {1,R2}, {4,R1}, {3,R3}, {0,R0}, {2,R1}, {5,R1}, {0,R0}}, + {{0,R0}, {1,R3}, {3,R1}, {5,R3}, {0,R0}, {4,R1}, {2,R1}, {0,R3}}, + {{0,R0}, {1,R0}, {5,R1}, {2,R3}, {0,R0}, {3,R1}, {4,R1}, {0,R2}}, + + {{0,R0}, {3,R2}, {1,R2}, {4,R2}, {0,R0}, {5,R2}, {0,R2}, {2,R2}}, // rotate around y- 20 - 23 + {{0,R0}, {5,R2}, {1,R3}, {3,R2}, {0,R0}, {2,R2}, {0,R1}, {4,R2}}, + {{0,R0}, {2,R2}, {1,R0}, {5,R2}, {0,R0}, {4,R2}, {0,R0}, {3,R2}}, + {{0,R0}, {4,R2}, {1,R1}, {2,R2}, {0,R0}, {3,R2}, {0,R3}, {5,R2}} }; getNodeTileN(mn, p, dir_to_tile[facedir][dir_i].tile, data, tile); tile.rotation = tile.world_aligned ? TileRotation::None : dir_to_tile[facedir][dir_i].rotation; diff --git a/src/client/mesh.cpp b/src/client/mesh.cpp index 6cdcb0b78bdb6..7fff2b3146d49 100644 --- a/src/client/mesh.cpp +++ b/src/client/mesh.cpp @@ -337,6 +337,8 @@ bool checkMeshNormals(scene::IMesh *mesh) for (u32 i = 0; i < buffer_count; i++) { scene::IMeshBuffer *buffer = mesh->getMeshBuffer(i); + if (!buffer->getVertexCount()) + continue; // Here we intentionally check only first normal, assuming that if buffer // has it valid, then most likely all other ones are fine too. We can @@ -503,3 +505,18 @@ scene::IMesh* convertNodeboxesToMesh(const std::vector &boxes, } return dst_mesh; } + +void setMaterialFilters(video::SMaterialLayer &tex, bool bilinear, bool trilinear, bool anisotropic) { + if (trilinear) + tex.MinFilter = video::ETMINF_LINEAR_MIPMAP_LINEAR; + else if (bilinear) + tex.MinFilter = video::ETMINF_LINEAR_MIPMAP_NEAREST; + else + tex.MinFilter = video::ETMINF_NEAREST_MIPMAP_NEAREST; + + // "We don't want blurriness after all." ~ Desour, #13108 + // (because of pixel art) + tex.MagFilter = video::ETMAGF_NEAREST; + + tex.AnisotropicFilter = anisotropic ? 0xFF : 0; +} diff --git a/src/client/mesh.h b/src/client/mesh.h index 1ed753c013410..0c3e8942e0c05 100644 --- a/src/client/mesh.h +++ b/src/client/mesh.h @@ -19,6 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once +#include "SMaterialLayer.h" #include "irrlichttypes_extrabloated.h" #include "nodedef.h" @@ -133,3 +134,10 @@ void recalculateBoundingBox(scene::IMesh *src_mesh); We assume normal to be valid when it's 0 < length < Inf. and not NaN */ bool checkMeshNormals(scene::IMesh *mesh); + +/* + Set the MinFilter, MagFilter and AnisotropicFilter properties of a texture + layer according to the three relevant boolean values found in the Minetest + settings. +*/ +void setMaterialFilters(video::SMaterialLayer &tex, bool bilinear, bool trilinear, bool anisotropic); diff --git a/src/client/minimap.cpp b/src/client/minimap.cpp index 0e6c61d608c4e..8ebe2fb03cbe9 100644 --- a/src/client/minimap.cpp +++ b/src/client/minimap.cpp @@ -248,9 +248,6 @@ Minimap::~Minimap() driver->removeTexture(data->texture); driver->removeTexture(data->heightmap_texture); - driver->removeTexture(data->minimap_overlay_round); - driver->removeTexture(data->minimap_overlay_square); - driver->removeTexture(data->object_marker_red); for (MinimapMarker *m : m_markers) delete m; diff --git a/src/client/renderingengine.cpp b/src/client/renderingengine.cpp index 59522f43da2cf..e94de87bbaa27 100644 --- a/src/client/renderingengine.cpp +++ b/src/client/renderingengine.cpp @@ -37,6 +37,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "gettext.h" #include "filesys.h" #include "../gui/guiSkin.h" +#include "irrlicht_changes/static_text.h" #include "irr_ptr.h" RenderingEngine *RenderingEngine::s_singleton = nullptr; @@ -123,7 +124,7 @@ RenderingEngine::RenderingEngine(IEventReceiver *receiver) // bpp, fsaa, vsync bool vsync = g_settings->getBool("vsync"); bool enable_fsaa = g_settings->get("antialiasing") == "fsaa"; - u16 fsaa = enable_fsaa ? g_settings->getU16("fsaa") : 0; + u16 fsaa = enable_fsaa ? MYMAX(2, g_settings->getU16("fsaa")) : 0; // Determine driver auto driverType = chooseVideoDriver(); @@ -139,7 +140,6 @@ RenderingEngine::RenderingEngine(IEventReceiver *receiver) params.Stencilbuffer = false; params.Vsync = vsync; params.EventReceiver = receiver; - params.HighPrecisionFPU = true; #ifdef __ANDROID__ params.PrivateData = porting::app_global; #endif @@ -195,16 +195,7 @@ void RenderingEngine::cleanupMeshCache() bool RenderingEngine::setupTopLevelWindow() { - // FIXME: It would make more sense for there to be a switch of some - // sort here that would call the correct toplevel setup methods for - // the environment Minetest is running in. - - /* Setting general properties for the top level window */ - verbosestream << "Client: Configuring general top level window properties" - << std::endl; - bool result = setWindowIcon(); - - return result; + return setWindowIcon(); } bool RenderingEngine::setWindowIcon() @@ -235,7 +226,7 @@ void RenderingEngine::draw_load_screen(const std::wstring &text, core::rect textrect(center - textsize / 2, center + textsize / 2); gui::IGUIStaticText *guitext = - guienv->addStaticText(text.c_str(), textrect, false, false); + gui::StaticText::add(guienv, text, textrect, false, false); guitext->setTextAlignment(gui::EGUIA_CENTER, gui::EGUIA_UPPERLEFT); if (sky && g_settings->getBool("menu_clouds")) { diff --git a/src/client/shader.cpp b/src/client/shader.cpp index 7465692236e1f..0fcdebf60f784 100644 --- a/src/client/shader.cpp +++ b/src/client/shader.cpp @@ -233,8 +233,6 @@ class MainShaderConstantSetter : public IShaderConstantSetter CachedVertexShaderSetting m_world_view; // Texture matrix CachedVertexShaderSetting m_texture; - // Normal matrix - CachedVertexShaderSetting m_normal; public: MainShaderConstantSetter() : @@ -256,7 +254,6 @@ class MainShaderConstantSetter : public IShaderConstantSetter , m_perspective_zbias_pixel("zPerspectiveBias") , m_world_view("mWorldView") , m_texture("mTexture") - , m_normal("mNormal") {} ~MainShaderConstantSetter() = default; @@ -283,16 +280,6 @@ class MainShaderConstantSetter : public IShaderConstantSetter core::matrix4 texture = driver->getTransform(video::ETS_TEXTURE_0); m_world_view.set(*reinterpret_cast(worldView.pointer()), services); m_texture.set(*reinterpret_cast(texture.pointer()), services); - - core::matrix4 normal; - worldView.getTransposed(normal); - sanity_check(normal.makeInverse()); - float m[9] = { - normal[0], normal[1], normal[2], - normal[4], normal[5], normal[6], - normal[8], normal[9], normal[10], - }; - m_normal.set(m, services); } // Set uniforms for Shadow shader @@ -488,8 +475,6 @@ u32 ShaderSource::getShader(const std::string &name, u32 ShaderSource::getShaderIdDirect(const std::string &name, MaterialType material_type, NodeDrawType drawtype) { - //infostream<<"getShaderIdDirect(): name=\""<setTransform(video::ETS_PROJECTION, light.getProjectionMatrix()); for (const auto &shadow_node : m_shadow_node_array) { - // we only take care of the shadow casters - if (shadow_node.shadowMode == ESM_RECEIVE) + // we only take care of the shadow casters and only visible nodes cast shadows + if (shadow_node.shadowMode == ESM_RECEIVE || !shadow_node.node->isVisible()) continue; // render other objects diff --git a/src/client/sound/al_helpers.cpp b/src/client/sound/al_helpers.cpp new file mode 100644 index 0000000000000..3db3c6f614599 --- /dev/null +++ b/src/client/sound/al_helpers.cpp @@ -0,0 +1,57 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +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; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "al_helpers.h" + +namespace sound { + +/* + * RAIIALSoundBuffer + */ + +RAIIALSoundBuffer &RAIIALSoundBuffer::operator=(RAIIALSoundBuffer &&other) noexcept +{ + if (&other != this) + reset(other.release()); + return *this; +} + +void RAIIALSoundBuffer::reset(ALuint buf) noexcept +{ + if (m_buffer != 0) { + alDeleteBuffers(1, &m_buffer); + warn_if_al_error("Failed to free sound buffer"); + } + + m_buffer = buf; +} + +RAIIALSoundBuffer RAIIALSoundBuffer::generate() noexcept +{ + ALuint buf; + alGenBuffers(1, &buf); + return RAIIALSoundBuffer(buf); +} + +} // namespace sound diff --git a/src/client/sound/al_helpers.h b/src/client/sound/al_helpers.h new file mode 100644 index 0000000000000..5f0ba00890962 --- /dev/null +++ b/src/client/sound/al_helpers.h @@ -0,0 +1,122 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +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; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include "log.h" +#include "util/basic_macros.h" +#include "irr_v3d.h" + +#if defined(_WIN32) + #include + #include + //#include +#elif defined(__APPLE__) + #define OPENAL_DEPRECATED + #include + #include + //#include +#else + #include + #include + #include +#endif + +#include + +namespace sound { + +inline const char *getAlErrorString(ALenum err) noexcept +{ + switch (err) { + case AL_NO_ERROR: + return "no error"; + case AL_INVALID_NAME: + return "invalid name"; + case AL_INVALID_ENUM: + return "invalid enum"; + case AL_INVALID_VALUE: + return "invalid value"; + case AL_INVALID_OPERATION: + return "invalid operation"; + case AL_OUT_OF_MEMORY: + return "out of memory"; + default: + return ""; + } +} + +inline ALenum warn_if_al_error(const char *desc) +{ + ALenum err = alGetError(); + if (err == AL_NO_ERROR) + return err; + warningstream << "[OpenAL Error] " << desc << ": " << getAlErrorString(err) + << std::endl; + return err; +} + +/** + * Transforms vectors from a left-handed coordinate system to a right-handed one + * and vice-versa. + * (Needed because Minetest uses a left-handed one and OpenAL a right-handed one.) + */ +inline v3f swap_handedness(v3f v) noexcept +{ + return v3f(-v.X, v.Y, v.Z); +} + +/** + * RAII wrapper for openal sound buffers. + */ +struct RAIIALSoundBuffer final +{ + RAIIALSoundBuffer() noexcept = default; + explicit RAIIALSoundBuffer(ALuint buffer) noexcept : m_buffer(buffer) {}; + + ~RAIIALSoundBuffer() noexcept { reset(0); } + + DISABLE_CLASS_COPY(RAIIALSoundBuffer) + + RAIIALSoundBuffer(RAIIALSoundBuffer &&other) noexcept : m_buffer(other.release()) {} + RAIIALSoundBuffer &operator=(RAIIALSoundBuffer &&other) noexcept; + + ALuint get() noexcept { return m_buffer; } + + ALuint release() noexcept { return std::exchange(m_buffer, 0); } + + void reset(ALuint buf) noexcept; + + static RAIIALSoundBuffer generate() noexcept; + +private: + // According to openal specification: + // > Deleting buffer name 0 is a legal NOP. + // + // and: + // > [...] the NULL buffer (i.e., 0) which can always be queued. + ALuint m_buffer = 0; +}; + +} // namespace sound diff --git a/src/client/sound/ogg_file.cpp b/src/client/sound/ogg_file.cpp new file mode 100644 index 0000000000000..46c3427defaae --- /dev/null +++ b/src/client/sound/ogg_file.cpp @@ -0,0 +1,183 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +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; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "ogg_file.h" + +#include // memcpy + +namespace sound { + +/* + * OggVorbisBufferSource struct + */ + +size_t OggVorbisBufferSource::read_func(void *ptr, size_t size, size_t nmemb, + void *datasource) noexcept +{ + OggVorbisBufferSource *s = (OggVorbisBufferSource *)datasource; + size_t copied_size = MYMIN(s->buf.size() - s->cur_offset, size); + memcpy(ptr, s->buf.data() + s->cur_offset, copied_size); + s->cur_offset += copied_size; + return copied_size; +} + +int OggVorbisBufferSource::seek_func(void *datasource, ogg_int64_t offset, int whence) noexcept +{ + OggVorbisBufferSource *s = (OggVorbisBufferSource *)datasource; + if (whence == SEEK_SET) { + if (offset < 0 || (size_t)offset > s->buf.size()) { + // offset out of bounds + return -1; + } + s->cur_offset = offset; + return 0; + } else if (whence == SEEK_CUR) { + if ((size_t)MYMIN(-offset, 0) > s->cur_offset + || s->cur_offset + offset > s->buf.size()) { + // offset out of bounds + return -1; + } + s->cur_offset += offset; + return 0; + } else if (whence == SEEK_END) { + if (offset > 0 || (size_t)-offset > s->buf.size()) { + // offset out of bounds + return -1; + } + s->cur_offset = s->buf.size() - offset; + return 0; + } + return -1; +} + +int OggVorbisBufferSource::close_func(void *datasource) noexcept +{ + auto s = reinterpret_cast(datasource); + delete s; + return 0; +} + +long OggVorbisBufferSource::tell_func(void *datasource) noexcept +{ + OggVorbisBufferSource *s = (OggVorbisBufferSource *)datasource; + return s->cur_offset; +} + +const ov_callbacks OggVorbisBufferSource::s_ov_callbacks = { + &OggVorbisBufferSource::read_func, + &OggVorbisBufferSource::seek_func, + &OggVorbisBufferSource::close_func, + &OggVorbisBufferSource::tell_func +}; + +/* + * RAIIOggFile struct + */ + +std::optional RAIIOggFile::getDecodeInfo(const std::string &filename_for_logging) +{ + OggFileDecodeInfo ret; + + vorbis_info *pInfo = ov_info(&m_file, -1); + if (!pInfo) + return std::nullopt; + + ret.name_for_logging = filename_for_logging; + + if (pInfo->channels == 1) { + ret.is_stereo = false; + ret.format = AL_FORMAT_MONO16; + ret.bytes_per_sample = 2; + } else if (pInfo->channels == 2) { + ret.is_stereo = true; + ret.format = AL_FORMAT_STEREO16; + ret.bytes_per_sample = 4; + } else { + warningstream << "Audio: Can't decode. Sound is neither mono nor stereo: " + << ret.name_for_logging << std::endl; + return std::nullopt; + } + + ret.freq = pInfo->rate; + + ret.length_samples = static_cast(ov_pcm_total(&m_file, -1)); + ret.length_seconds = static_cast(ov_time_total(&m_file, -1)); + + return ret; +} + +RAIIALSoundBuffer RAIIOggFile::loadBuffer(const OggFileDecodeInfo &decode_info, + ALuint pcm_start, ALuint pcm_end) +{ + constexpr int endian = 0; // 0 for Little-Endian, 1 for Big-Endian + constexpr int word_size = 2; // we use s16 samples + constexpr int word_signed = 1; // ^ + + // seek + if (ov_pcm_tell(&m_file) != pcm_start) { + if (ov_pcm_seek(&m_file, pcm_start) != 0) { + warningstream << "Audio: Error decoding (could not seek) " + << decode_info.name_for_logging << std::endl; + return RAIIALSoundBuffer(); + } + } + + const size_t size = static_cast(pcm_end - pcm_start) + * decode_info.bytes_per_sample; + + std::unique_ptr snd_buffer(new char[size]); + + // read size bytes + size_t read_count = 0; + int bitStream; + while (read_count < size) { + // Read up to a buffer's worth of decoded sound data + long num_bytes = ov_read(&m_file, &snd_buffer[read_count], size - read_count, + endian, word_size, word_signed, &bitStream); + + if (num_bytes <= 0) { + warningstream << "Audio: Error decoding " + << decode_info.name_for_logging << std::endl; + return RAIIALSoundBuffer(); + } + + read_count += num_bytes; + } + + // load buffer to openal + RAIIALSoundBuffer snd_buffer_id = RAIIALSoundBuffer::generate(); + alBufferData(snd_buffer_id.get(), decode_info.format, &(snd_buffer[0]), size, + decode_info.freq); + + ALenum error = alGetError(); + if (error != AL_NO_ERROR) { + warningstream << "Audio: OpenAL error: " << getAlErrorString(error) + << "preparing sound buffer for sound \"" + << decode_info.name_for_logging << "\"" << std::endl; + } + + return snd_buffer_id; +} + +} // namespace sound diff --git a/src/client/sound/ogg_file.h b/src/client/sound/ogg_file.h new file mode 100644 index 0000000000000..177357337a09e --- /dev/null +++ b/src/client/sound/ogg_file.h @@ -0,0 +1,98 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +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; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include "al_helpers.h" +#include +#include +#include + +namespace sound { + +/** + * For vorbisfile to read from our buffer instead of from a file. + */ +struct OggVorbisBufferSource { + std::string buf; + size_t cur_offset = 0; + + static size_t read_func(void *ptr, size_t size, size_t nmemb, void *datasource) noexcept; + static int seek_func(void *datasource, ogg_int64_t offset, int whence) noexcept; + static int close_func(void *datasource) noexcept; + static long tell_func(void *datasource) noexcept; + + static const ov_callbacks s_ov_callbacks; +}; + +/** + * Metadata of an Ogg-Vorbis file, used for decoding. + * We query this information once and store it in this struct. + */ +struct OggFileDecodeInfo { + std::string name_for_logging; + bool is_stereo; + ALenum format; // AL_FORMAT_MONO16 or AL_FORMAT_STEREO16 + size_t bytes_per_sample; + ALsizei freq; + ALuint length_samples = 0; + f32 length_seconds = 0.0f; +}; + +/** + * RAII wrapper for OggVorbis_File. + */ +struct RAIIOggFile { + bool m_needs_clear = false; + OggVorbis_File m_file; + + RAIIOggFile() = default; + + DISABLE_CLASS_COPY(RAIIOggFile) + + ~RAIIOggFile() noexcept + { + if (m_needs_clear) + ov_clear(&m_file); + } + + OggVorbis_File *get() { return &m_file; } + + std::optional getDecodeInfo(const std::string &filename_for_logging); + + /** + * Main function for loading ogg vorbis sounds. + * Loads exactly the specified interval of PCM-data, and creates an OpenAL + * buffer with it. + * + * @param decode_info Cached meta information of the file. + * @param pcm_start First sample in the interval. + * @param pcm_end One after last sample of the interval (=> exclusive). + * @return An AL sound buffer, or a 0-buffer on failure. + */ + RAIIALSoundBuffer loadBuffer(const OggFileDecodeInfo &decode_info, ALuint pcm_start, + ALuint pcm_end); +}; + +} // namespace sound diff --git a/src/client/sound/playing_sound.cpp b/src/client/sound/playing_sound.cpp new file mode 100644 index 0000000000000..11aee80e3735d --- /dev/null +++ b/src/client/sound/playing_sound.cpp @@ -0,0 +1,245 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +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; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "playing_sound.h" + +#include "debug.h" +#include +#include + +namespace sound { + +PlayingSound::PlayingSound(ALuint source_id, std::shared_ptr data, + bool loop, f32 volume, f32 pitch, f32 start_time, + const std::optional> &pos_vel_opt) + : m_source_id(source_id), m_data(std::move(data)), m_looping(loop), + m_is_positional(pos_vel_opt.has_value()) +{ + // Calculate actual start_time (see lua_api.txt for specs) + f32 len_seconds = m_data->m_decode_info.length_seconds; + f32 len_samples = m_data->m_decode_info.length_samples; + if (!m_looping) { + if (start_time < 0.0f) { + start_time = std::fmax(start_time + len_seconds, 0.0f); + } else if (start_time >= len_seconds) { + // No sound + m_next_sample_pos = len_samples; + return; + } + } else { + // Modulo offset to be within looping time + start_time = start_time - std::floor(start_time / len_seconds) * len_seconds; + } + + // Queue first buffers + + m_next_sample_pos = std::min((start_time / len_seconds) * len_samples, len_samples); + + if (m_looping && m_next_sample_pos == len_samples) + m_next_sample_pos = 0; + + if (!m_data->isStreaming()) { + // If m_next_sample_pos >= len_samples, buf will be 0, and setting it as + // AL_BUFFER is a NOP (source stays AL_UNDETERMINED). => No sound will be + // played. + + auto [buf, buf_end, offset_in_buf] = m_data->getOrLoadBufferAt(m_next_sample_pos); + m_next_sample_pos = buf_end; + + alSourcei(m_source_id, AL_BUFFER, buf); + alSourcei(m_source_id, AL_SAMPLE_OFFSET, offset_in_buf); + + alSourcei(m_source_id, AL_LOOPING, m_looping ? AL_TRUE : AL_FALSE); + + warn_if_al_error("when creating non-streaming sound"); + + } else { + // Start with 2 buffers + ALuint buf_ids[2]; + + // If m_next_sample_pos >= len_samples (happens only if not looped), one + // or both of buf_ids will be 0. Queuing 0 is a NOP. + + auto [buf0, buf0_end, offset_in_buf0] = m_data->getOrLoadBufferAt(m_next_sample_pos); + buf_ids[0] = buf0; + m_next_sample_pos = buf0_end; + + if (m_looping && m_next_sample_pos == len_samples) + m_next_sample_pos = 0; + + auto [buf1, buf1_end, offset_in_buf1] = m_data->getOrLoadBufferAt(m_next_sample_pos); + buf_ids[1] = buf1; + m_next_sample_pos = buf1_end; + assert(offset_in_buf1 == 0); + + alSourceQueueBuffers(m_source_id, 2, buf_ids); + alSourcei(m_source_id, AL_SAMPLE_OFFSET, offset_in_buf0); + + // We can't use AL_LOOPING because more buffers are queued later + // looping is therefore done manually + + m_stopped_means_dead = false; + + warn_if_al_error("when creating streaming sound"); + } + + // Set initial pos, volume, pitch + if (m_is_positional) { + updatePosVel(pos_vel_opt->first, pos_vel_opt->second); + } else { + // Make position-less + alSourcei(m_source_id, AL_SOURCE_RELATIVE, true); + alSource3f(m_source_id, AL_POSITION, 0.0f, 0.0f, 0.0f); + alSource3f(m_source_id, AL_VELOCITY, 0.0f, 0.0f, 0.0f); + warn_if_al_error("PlayingSound::PlayingSound at making position-less"); + } + setGain(volume); + setPitch(pitch); +} + +bool PlayingSound::stepStream() +{ + if (isDead()) + return false; + + // unqueue finished buffers + ALint num_unqueued_bufs = 0; + alGetSourcei(m_source_id, AL_BUFFERS_PROCESSED, &num_unqueued_bufs); + if (num_unqueued_bufs == 0) + return true; + // We always have 2 buffers enqueued at most + SANITY_CHECK(num_unqueued_bufs <= 2); + ALuint unqueued_buffer_ids[2]; + alSourceUnqueueBuffers(m_source_id, num_unqueued_bufs, unqueued_buffer_ids); + + // Fill up again + for (ALint i = 0; i < num_unqueued_bufs; ++i) { + if (m_next_sample_pos == m_data->m_decode_info.length_samples) { + // Reached end + if (m_looping) { + m_next_sample_pos = 0; + } else { + m_stopped_means_dead = true; + return false; + } + } + + auto [buf, buf_end, offset_in_buf] = m_data->getOrLoadBufferAt(m_next_sample_pos); + m_next_sample_pos = buf_end; + assert(offset_in_buf == 0); + + alSourceQueueBuffers(m_source_id, 1, &buf); + + // Start again if queue was empty and resulted in stop + if (getState() == AL_STOPPED) { + play(); + warningstream << "PlayingSound::stepStream: Sound queue ran empty for \"" + << m_data->m_decode_info.name_for_logging << "\"" << std::endl; + } + } + + return true; +} + +bool PlayingSound::fade(f32 step, f32 target_gain) noexcept +{ + bool already_fading = m_fade_state.has_value(); + + target_gain = MYMAX(target_gain, 0.0f); // 0.0f if nan + step = target_gain - getGain() > 0.0f ? std::abs(step) : -std::abs(step); + + m_fade_state = FadeState{step, target_gain}; + + return !already_fading; +} + +bool PlayingSound::doFade(f32 dtime) noexcept +{ + if (!m_fade_state || isDead()) + return false; + + FadeState &fade = *m_fade_state; + assert(fade.step != 0.0f); + + f32 current_gain = getGain(); + current_gain += fade.step * dtime; + + if (fade.step < 0.0f) + current_gain = std::max(current_gain, fade.target_gain); + else + current_gain = std::min(current_gain, fade.target_gain); + + if (current_gain <= 0.0f) { + // stop sound + m_stopped_means_dead = true; + alSourceStop(m_source_id); + + m_fade_state = std::nullopt; + return false; + } + + setGain(current_gain); + + if (current_gain == fade.target_gain) { + m_fade_state = std::nullopt; + return false; + } else { + return true; + } +} + +void PlayingSound::updatePosVel(const v3f &pos, const v3f &vel) noexcept +{ + alSourcei(m_source_id, AL_SOURCE_RELATIVE, false); + alSource3f(m_source_id, AL_POSITION, pos.X, pos.Y, pos.Z); + alSource3f(m_source_id, AL_VELOCITY, vel.X, vel.Y, vel.Z); + // Using alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED) and setting reference + // distance to clamp gain at <1 node distance avoids excessive volume when + // closer. + alSourcef(m_source_id, AL_REFERENCE_DISTANCE, 1.0f); + + warn_if_al_error("PlayingSound::updatePosVel"); +} + +void PlayingSound::setGain(f32 gain) noexcept +{ + // AL_REFERENCE_DISTANCE was once reduced from 3 nodes to 1 node. + // We compensate this by multiplying the volume by 3. + if (m_is_positional) + gain *= 3.0f; + + alSourcef(m_source_id, AL_GAIN, gain); +} + +f32 PlayingSound::getGain() noexcept +{ + ALfloat gain; + alGetSourcef(m_source_id, AL_GAIN, &gain); + // Same as above, but inverse. + if (m_is_positional) + gain *= 1.0f/3.0f; + return gain; +} + +} // namespace sound diff --git a/src/client/sound/playing_sound.h b/src/client/sound/playing_sound.h new file mode 100644 index 0000000000000..1fbf37e3ecab6 --- /dev/null +++ b/src/client/sound/playing_sound.h @@ -0,0 +1,111 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +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; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include "sound_data.h" + +namespace sound { + +/** + * A sound that is currently played. + * Can be streaming. + * Can be fading. + */ +class PlayingSound final +{ + struct FadeState { + f32 step; + f32 target_gain; + }; + + ALuint m_source_id; + std::shared_ptr m_data; + ALuint m_next_sample_pos = 0; + bool m_looping; + bool m_is_positional; + bool m_stopped_means_dead = true; + std::optional m_fade_state = std::nullopt; + +public: + PlayingSound(ALuint source_id, std::shared_ptr data, bool loop, + f32 volume, f32 pitch, f32 start_time, + const std::optional> &pos_vel_opt); + + ~PlayingSound() noexcept + { + alDeleteSources(1, &m_source_id); + } + + DISABLE_CLASS_COPY(PlayingSound) + + // return false means streaming finished + bool stepStream(); + + // retruns true if it wasn't fading already + bool fade(f32 step, f32 target_gain) noexcept; + + // returns true if more fade is needed later + bool doFade(f32 dtime) noexcept; + + void updatePosVel(const v3f &pos, const v3f &vel) noexcept; + + void setGain(f32 gain) noexcept; + + f32 getGain() noexcept; + + void setPitch(f32 pitch) noexcept { alSourcef(m_source_id, AL_PITCH, pitch); } + + bool isStreaming() const noexcept { return m_data->isStreaming(); } + + void play() noexcept { alSourcePlay(m_source_id); } + + // returns one of AL_INITIAL, AL_PLAYING, AL_PAUSED, AL_STOPPED + ALint getState() noexcept + { + ALint state; + alGetSourcei(m_source_id, AL_SOURCE_STATE, &state); + return state; + } + + bool isDead() noexcept + { + // streaming sounds can (but should not) stop because the queue runs empty + return m_stopped_means_dead && getState() == AL_STOPPED; + } + + void pause() noexcept + { + // this is a NOP if state != AL_PLAYING + alSourcePause(m_source_id); + } + + void resume() noexcept + { + if (getState() == AL_PAUSED) + play(); + } +}; + +} // namespace sound diff --git a/src/client/sound/proxy_sound_manager.cpp b/src/client/sound/proxy_sound_manager.cpp new file mode 100644 index 0000000000000..ede603e678050 --- /dev/null +++ b/src/client/sound/proxy_sound_manager.cpp @@ -0,0 +1,167 @@ +/* +Minetest +Copyright (C) 2023 DS + +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; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "proxy_sound_manager.h" + +#include "filesys.h" + +namespace sound { + +ProxySoundManager::MsgResult ProxySoundManager::handleMsg(SoundManagerMsgToProxy &&msg) +{ + using namespace sound_manager_messages_to_proxy; + + return std::visit([&](auto &&msg) { + using T = std::decay_t; + + if constexpr (std::is_same_v) + return MsgResult::Empty; + else if constexpr (std::is_same_v) + reportRemovedSound(msg.id); + else if constexpr (std::is_same_v) + return MsgResult::Stopped; + + return MsgResult::Ok; + }, + std::move(msg)); +} + +ProxySoundManager::~ProxySoundManager() +{ + if (m_sound_manager.isRunning()) { + send(sound_manager_messages_to_mgr::PleaseStop{}); + + // recv until it stopped + auto recv = [&] { + return m_sound_manager.m_queue_to_proxy.pop_frontNoEx(); + }; + + while (true) { + if (handleMsg(recv()) == MsgResult::Stopped) + break; + } + + // join + m_sound_manager.stop(); + SANITY_CHECK(m_sound_manager.wait()); + } +} + +void ProxySoundManager::step(f32 dtime) +{ + auto recv = [&] { + return m_sound_manager.m_queue_to_proxy.pop_frontNoEx(0); + }; + + while (true) { + MsgResult res = handleMsg(recv()); + if (res == MsgResult::Empty) + break; + else if (res == MsgResult::Stopped) + throw std::runtime_error("OpenALSoundManager stopped unexpectedly"); + } +} + +void ProxySoundManager::pauseAll() +{ + send(sound_manager_messages_to_mgr::PauseAll{}); +} + +void ProxySoundManager::resumeAll() +{ + send(sound_manager_messages_to_mgr::ResumeAll{}); +} + +void ProxySoundManager::updateListener(const v3f &pos_, const v3f &vel_, + const v3f &at_, const v3f &up_) +{ + send(sound_manager_messages_to_mgr::UpdateListener{pos_, vel_, at_, up_}); +} + +void ProxySoundManager::setListenerGain(f32 gain) +{ + send(sound_manager_messages_to_mgr::SetListenerGain{gain}); +} + +bool ProxySoundManager::loadSoundFile(const std::string &name, + const std::string &filepath) +{ + // do not add twice + if (m_known_sound_names.count(name) != 0) + return false; + + // coarse check + if (!fs::IsFile(filepath)) + return false; + + send(sound_manager_messages_to_mgr::LoadSoundFile{name, filepath}); + + m_known_sound_names.insert(name); + return true; +} + +bool ProxySoundManager::loadSoundData(const std::string &name, std::string &&filedata) +{ + // do not add twice + if (m_known_sound_names.count(name) != 0) + return false; + + send(sound_manager_messages_to_mgr::LoadSoundData{name, std::move(filedata)}); + + m_known_sound_names.insert(name); + return true; +} + +void ProxySoundManager::addSoundToGroup(const std::string &sound_name, + const std::string &group_name) +{ + send(sound_manager_messages_to_mgr::AddSoundToGroup{sound_name, group_name}); +} + +void ProxySoundManager::playSound(sound_handle_t id, const SoundSpec &spec) +{ + if (id == 0) + id = allocateId(1); + send(sound_manager_messages_to_mgr::PlaySound{id, spec}); +} + +void ProxySoundManager::playSoundAt(sound_handle_t id, const SoundSpec &spec, const v3f &pos_, + const v3f &vel_) +{ + if (id == 0) + id = allocateId(1); + send(sound_manager_messages_to_mgr::PlaySoundAt{id, spec, pos_, vel_}); +} + +void ProxySoundManager::stopSound(sound_handle_t sound) +{ + send(sound_manager_messages_to_mgr::StopSound{sound}); +} + +void ProxySoundManager::fadeSound(sound_handle_t soundid, f32 step, f32 target_gain) +{ + send(sound_manager_messages_to_mgr::FadeSound{soundid, step, target_gain}); +} + +void ProxySoundManager::updateSoundPosVel(sound_handle_t sound, const v3f &pos_, const v3f &vel_) +{ + send(sound_manager_messages_to_mgr::UpdateSoundPosVel{sound, pos_, vel_}); +} + +} // namespace sound diff --git a/src/client/sound/proxy_sound_manager.h b/src/client/sound/proxy_sound_manager.h new file mode 100644 index 0000000000000..53299e0452966 --- /dev/null +++ b/src/client/sound/proxy_sound_manager.h @@ -0,0 +1,75 @@ +/* +Minetest +Copyright (C) 2023 DS + +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; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include "sound_manager.h" + +namespace sound { + +/* + * The public ISoundManager interface + */ + +class ProxySoundManager final : public ISoundManager +{ + OpenALSoundManager m_sound_manager; + // sound names from loadSoundData and loadSoundFile + std::unordered_set m_known_sound_names; + + void send(SoundManagerMsgToMgr msg) + { + m_sound_manager.m_queue_to_mgr.push_back(std::move(msg)); + } + + enum class MsgResult { Ok, Empty, Stopped}; + MsgResult handleMsg(SoundManagerMsgToProxy &&msg); + +public: + ProxySoundManager(SoundManagerSingleton *smg, + std::unique_ptr fallback_path_provider) : + m_sound_manager(smg, std::move(fallback_path_provider)) + { + m_sound_manager.start(); + } + + ~ProxySoundManager() override; + + /* Interface */ + + void step(f32 dtime) override; + void pauseAll() override; + void resumeAll() override; + + void updateListener(const v3f &pos_, const v3f &vel_, const v3f &at_, const v3f &up_) override; + void setListenerGain(f32 gain) override; + + bool loadSoundFile(const std::string &name, const std::string &filepath) override; + bool loadSoundData(const std::string &name, std::string &&filedata) override; + void addSoundToGroup(const std::string &sound_name, const std::string &group_name) override; + + void playSound(sound_handle_t id, const SoundSpec &spec) override; + void playSoundAt(sound_handle_t id, const SoundSpec &spec, const v3f &pos_, + const v3f &vel_) override; + void stopSound(sound_handle_t sound) override; + void fadeSound(sound_handle_t soundid, f32 step, f32 target_gain) override; + void updateSoundPosVel(sound_handle_t sound, const v3f &pos_, const v3f &vel_) override; +}; + +} // namespace sound diff --git a/src/client/sound/sound_constants.h b/src/client/sound/sound_constants.h new file mode 100644 index 0000000000000..94d74b86c1f0e --- /dev/null +++ b/src/client/sound/sound_constants.h @@ -0,0 +1,123 @@ +/* +Minetest +Copyright (C) 2022 DS + +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; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +/* + * + * The coordinate space for sounds (sound-space): + * ---------------------------------------------- + * + * * The functions from ISoundManager (see sound.h) take spatial vectors in node-space. + * * All other `v3f`s here are, if not told otherwise, in sound-space, which is + * defined as node-space mirrored along the x-axis. + * (This is needed because OpenAL uses a right-handed coordinate system.) + * * Use `swap_handedness()` from `al_helpers.h` to convert between those two + * coordinate spaces. + * + * + * How sounds are loaded: + * ---------------------- + * + * * Step 1: + * `loadSoundFile` or `loadSoundFile` is called. This adds an unopen sound with + * the given name to `m_sound_datas_unopen`. + * Unopen / lazy sounds (`ISoundDataUnopen`) are ogg-vorbis files that we did not yet + * start to decode. (Decoding an unopen sound does not fail under normal circumstances + * (because we check whether the file exists at least), if it does fail anyways, + * we should notify the user.) + * * Step 2: + * `addSoundToGroup` is called, to add the name from step 1 to a group. If the + * group does not yet exist, a new one is created. A group can later be played. + * (The mapping is stored in `m_sound_groups`.) + * * Step 3: + * `playSound` or `playSoundAt` is called. + * * Step 3.1: + * If the group with the name `spec.name` does not exist, and `spec.use_local_fallback` + * is true, a new group is created using the user's sound-pack. + * * Step 3.2: + * We choose one random sound name from the given group. + * * Step 3.3: + * We open the sound (see `openSingleSound`). + * If the sound is already open (in `m_sound_datas_open`), we take that one. + * Otherwise we open it by calling `ISoundDataUnopen::open`. We choose (by + * sound length), whether it's a single-buffer (`SoundDataOpenBuffer`) or + * streamed (`SoundDataOpenStream`) sound. + * Single-buffer sounds are always completely loaded. Streamed sounds can be + * partially loaded. + * The sound is erased from `m_sound_datas_unopen` and added to `m_sound_datas_open`. + * Open sounds are kept forever. + * * Step 3.4: + * We create the new `PlayingSound`. It has a `shared_ptr` to its open sound. + * If the open sound is streaming, the playing sound needs to be stepped using + * `PlayingSound::stepStream` for enqueuing buffers. For this purpose, the sound + * is added to `m_sounds_streaming` (as `weak_ptr`). + * If the sound is fading, it is added to `m_sounds_fading` for regular fade-stepping. + * The sound is also added to `m_sounds_playing`, so that one can access it + * via its sound handle. + * * Step 4: + * Streaming sounds are updated. For details see [Streaming of sounds]. + * * Step 5: + * At deinitialization, we can just let the destructors do their work. + * Sound sources are deleted (and with this also stopped) by ~PlayingSound. + * Buffers can't be deleted while sound sources using them exist, because + * PlayingSound has a shared_ptr to its ISoundData. + * + * + * Streaming of sounds: + * -------------------- + * + * In each "bigstep", all streamed sounds are stepStream()ed. This means a + * sound can be stepped at any point in time in the bigstep's interval. + * + * In the worst case, a sound is stepped at the start of one bigstep and in the + * end of the next bigstep. So between two stepStream()-calls lie at most + * 2 * STREAM_BIGSTEP_TIME seconds. + * As there are always 2 sound buffers enqueued, at least one untouched full buffer + * is still available after the first stepStream(). + * If we take a MIN_STREAM_BUFFER_LENGTH > 2 * STREAM_BIGSTEP_TIME, we can hence + * not run into an empty queue. + * + * The MIN_STREAM_BUFFER_LENGTH needs to be a little bigger because of dtime jitter, + * other sounds that may have taken long to stepStream(), and sounds being played + * faster due to Doppler effect. + * + */ + +namespace sound { + +// constants + +// in seconds +constexpr f32 REMOVE_DEAD_SOUNDS_INTERVAL = 2.0f; +// maximum length in seconds that a sound can have without being streamed +constexpr f32 SOUND_DURATION_MAX_SINGLE = 3.0f; +// minimum time in seconds of a single buffer in a streamed sound +constexpr f32 MIN_STREAM_BUFFER_LENGTH = 1.0f; +// duration in seconds of one bigstep +constexpr f32 STREAM_BIGSTEP_TIME = 0.3f; +// step duration for the OpenALSoundManager thread, in seconds +constexpr f32 SOUNDTHREAD_DTIME = 0.016f; + +static_assert(MIN_STREAM_BUFFER_LENGTH > STREAM_BIGSTEP_TIME * 2.0f, + "See [Streaming of sounds]."); +static_assert(SOUND_DURATION_MAX_SINGLE >= MIN_STREAM_BUFFER_LENGTH * 2.0f, + "There's no benefit in streaming if we can't queue more than 2 buffers."); + +} // namespace sound diff --git a/src/client/sound/sound_data.cpp b/src/client/sound/sound_data.cpp new file mode 100644 index 0000000000000..f0cbc6dbf2a6e --- /dev/null +++ b/src/client/sound/sound_data.cpp @@ -0,0 +1,235 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +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; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "sound_data.h" + +#include "sound_constants.h" + +namespace sound { + +/* + * ISoundDataOpen struct + */ + +std::shared_ptr ISoundDataOpen::fromOggFile(std::unique_ptr oggfile, + const std::string &filename_for_logging) +{ + // Get some information about the OGG file + std::optional decode_info = oggfile->getDecodeInfo(filename_for_logging); + if (!decode_info.has_value()) { + warningstream << "Audio: Error decoding " + << filename_for_logging << std::endl; + return nullptr; + } + + // use duration (in seconds) to decide whether to load all at once or to stream + if (decode_info->length_seconds <= SOUND_DURATION_MAX_SINGLE) { + return std::make_shared(std::move(oggfile), *decode_info); + } else { + return std::make_shared(std::move(oggfile), *decode_info); + } +} + +/* + * SoundDataUnopenBuffer struct + */ + +std::shared_ptr SoundDataUnopenBuffer::open(const std::string &sound_name) && +{ + // load from m_buffer + + auto oggfile = std::make_unique(); + + auto buffer_source = std::make_unique(); + buffer_source->buf = std::move(m_buffer); + + oggfile->m_needs_clear = true; + if (ov_open_callbacks(buffer_source.release(), oggfile->get(), nullptr, 0, + OggVorbisBufferSource::s_ov_callbacks) != 0) { + warningstream << "Audio: Error opening " << sound_name << " for decoding" + << std::endl; + return nullptr; + } + + return ISoundDataOpen::fromOggFile(std::move(oggfile), sound_name); +} + +/* + * SoundDataUnopenFile struct + */ + +std::shared_ptr SoundDataUnopenFile::open(const std::string &sound_name) && +{ + // load from file at m_path + + auto oggfile = std::make_unique(); + + if (ov_fopen(m_path.c_str(), oggfile->get()) != 0) { + warningstream << "Audio: Error opening " << m_path << " for decoding" + << std::endl; + return nullptr; + } + oggfile->m_needs_clear = true; + + return ISoundDataOpen::fromOggFile(std::move(oggfile), sound_name); +} + +/* + * SoundDataOpenBuffer struct + */ + +SoundDataOpenBuffer::SoundDataOpenBuffer(std::unique_ptr oggfile, + const OggFileDecodeInfo &decode_info) : ISoundDataOpen(decode_info) +{ + m_buffer = oggfile->loadBuffer(m_decode_info, 0, m_decode_info.length_samples); + if (m_buffer.get() == 0) { + warningstream << "SoundDataOpenBuffer: Failed to load sound \"" + << m_decode_info.name_for_logging << "\"" << std::endl; + return; + } +} + +/* + * SoundDataOpenStream struct + */ + +SoundDataOpenStream::SoundDataOpenStream(std::unique_ptr oggfile, + const OggFileDecodeInfo &decode_info) : + ISoundDataOpen(decode_info), m_oggfile(std::move(oggfile)) +{ + // do nothing here. buffers are loaded at getOrLoadBufferAt +} + +std::tuple SoundDataOpenStream::getOrLoadBufferAt(ALuint offset) +{ + if (offset >= m_decode_info.length_samples) + return {0, m_decode_info.length_samples, 0}; + + // find the right-most ContiguousBuffers, such that `m_start <= offset` + // equivalent: the first element from the right such that `!(m_start > offset)` + // (from the right, `offset` is a lower bound to the `m_start`s) + auto lower_rit = std::lower_bound(m_bufferss.rbegin(), m_bufferss.rend(), offset, + [](const ContiguousBuffers &bufs, ALuint offset) { + return bufs.m_start > offset; + }); + + if (lower_rit != m_bufferss.rend()) { + std::vector &bufs = lower_rit->m_buffers; + // find the left-most SoundBufferUntil, such that `m_end > offset` + // equivalent: the first element from the left such that `m_end > offset` + // (returns first element where comp gives true) + auto upper_it = std::upper_bound(bufs.begin(), bufs.end(), offset, + [](ALuint offset, const SoundBufferUntil &buf) { + return offset < buf.m_end; + }); + + if (upper_it != bufs.end()) { + ALuint start = upper_it == bufs.begin() ? lower_rit->m_start + : (upper_it - 1)->m_end; + return {upper_it->m_buffer.get(), upper_it->m_end, offset - start}; + } + } + + // no loaded buffer starts before or at `offset` + // or no loaded buffer (that starts before or at `offset`) ends after `offset` + + // lower_rit, but not reverse and 1 farther + auto after_it = m_bufferss.begin() + (m_bufferss.rend() - lower_rit); + + return loadBufferAt(offset, after_it); +} + +std::tuple SoundDataOpenStream::loadBufferAt(ALuint offset, + std::vector::iterator after_it) +{ + bool has_before = after_it != m_bufferss.begin(); + bool has_after = after_it != m_bufferss.end(); + + ALuint end_before = has_before ? (after_it - 1)->m_buffers.back().m_end : 0; + ALuint start_after = has_after ? after_it->m_start : m_decode_info.length_samples; + + const ALuint min_buf_len_samples = m_decode_info.freq * MIN_STREAM_BUFFER_LENGTH; + + // + // 1) Find the actual start and end of the new buffer + // + + ALuint new_buf_start = offset; + ALuint new_buf_end = offset + min_buf_len_samples; + + // Don't load into next buffer, or past the end + if (new_buf_end > start_after) { + new_buf_end = start_after; + // Also move start (for min buf size) (but not *into* previous buffer) + if (new_buf_end - new_buf_start < min_buf_len_samples) { + new_buf_start = std::max( + end_before, + new_buf_end < min_buf_len_samples ? 0 + : new_buf_end - min_buf_len_samples + ); + } + } + + // Widen if space to right or left is smaller than min buf size + if (new_buf_start - end_before < min_buf_len_samples) + new_buf_start = end_before; + if (start_after - new_buf_end < min_buf_len_samples) + new_buf_end = start_after; + + // + // 2) Load [new_buf_start, new_buf_end) + // + + // If it fails, we get a 0-buffer. we store it and won't try loading again + RAIIALSoundBuffer new_buf = m_oggfile->loadBuffer(m_decode_info, new_buf_start, + new_buf_end); + + // + // 3) Insert before after_it + // + + // Choose ContiguousBuffers to add the new SoundBufferUntil into: + // * `after_it - 1` (=before) if existent and if there's no space between its + // last buffer and the new buffer + // * A new ContiguousBuffers otherwise + auto it = has_before && new_buf_start == end_before ? after_it - 1 + : m_bufferss.insert(after_it, ContiguousBuffers{new_buf_start, {}}); + + // Add the new SoundBufferUntil + size_t new_buf_i = it->m_buffers.size(); + it->m_buffers.push_back(SoundBufferUntil{new_buf_end, std::move(new_buf)}); + + if (has_after && new_buf_end == start_after) { + // Merge after into my ContiguousBuffers + auto &bufs = it->m_buffers; + auto &bufs_after = (it + 1)->m_buffers; + bufs.insert(bufs.end(), std::make_move_iterator(bufs_after.begin()), + std::make_move_iterator(bufs_after.end())); + it = m_bufferss.erase(it + 1) - 1; + } + + return {it->m_buffers[new_buf_i].m_buffer.get(), new_buf_end, offset - new_buf_start}; +} + +} // namespace sound diff --git a/src/client/sound/sound_data.h b/src/client/sound/sound_data.h new file mode 100644 index 0000000000000..36052c1617488 --- /dev/null +++ b/src/client/sound/sound_data.h @@ -0,0 +1,177 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +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; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include "ogg_file.h" +#include +#include + +namespace sound { + +/** + * Stores sound pcm data buffers. + */ +struct ISoundDataOpen +{ + OggFileDecodeInfo m_decode_info; + + explicit ISoundDataOpen(const OggFileDecodeInfo &decode_info) : + m_decode_info(decode_info) {} + + virtual ~ISoundDataOpen() = default; + + /** + * Iff the data is streaming, there is more than one buffer. + * @return Whether it's streaming data. + */ + virtual bool isStreaming() const noexcept = 0; + + /** + * Load a buffer containing data starting at the given offset. Or just get it + * if it was already loaded. + * + * This function returns multiple values: + * * `buffer`: The OpenAL buffer. + * * `buffer_end`: The offset (in the file) where `buffer` ends (exclusive). + * * `offset_in_buffer`: Offset relative to `buffer`'s start where the requested + * `offset` is. + * `offset_in_buffer == 0` is guaranteed if some loaded buffer ends at + * `offset`. + * + * @param offset The start of the buffer. + * @return `{buffer, buffer_end, offset_in_buffer}` or `{0, sound_data_end, 0}` + * if `offset` is invalid. + */ + virtual std::tuple getOrLoadBufferAt(ALuint offset) = 0; + + static std::shared_ptr fromOggFile(std::unique_ptr oggfile, + const std::string &filename_for_logging); +}; + +/** + * Will be opened lazily when first used. + */ +struct ISoundDataUnopen +{ + virtual ~ISoundDataUnopen() = default; + + // Note: The ISoundDataUnopen is moved (see &&). It is not meant to be kept + // after opening. + virtual std::shared_ptr open(const std::string &sound_name) && = 0; +}; + +/** + * Sound file is in a memory buffer. + */ +struct SoundDataUnopenBuffer final : ISoundDataUnopen +{ + std::string m_buffer; + + explicit SoundDataUnopenBuffer(std::string &&buffer) : m_buffer(std::move(buffer)) {} + + std::shared_ptr open(const std::string &sound_name) && override; +}; + +/** + * Sound file is in file system. + */ +struct SoundDataUnopenFile final : ISoundDataUnopen +{ + std::string m_path; + + explicit SoundDataUnopenFile(const std::string &path) : m_path(path) {} + + std::shared_ptr open(const std::string &sound_name) && override; +}; + +/** + * Non-streaming opened sound data. + * All data is completely loaded in one buffer. + */ +struct SoundDataOpenBuffer final : ISoundDataOpen +{ + RAIIALSoundBuffer m_buffer; + + SoundDataOpenBuffer(std::unique_ptr oggfile, + const OggFileDecodeInfo &decode_info); + + bool isStreaming() const noexcept override { return false; } + + std::tuple getOrLoadBufferAt(ALuint offset) override + { + if (offset >= m_decode_info.length_samples) + return {0, m_decode_info.length_samples, 0}; + return {m_buffer.get(), m_decode_info.length_samples, offset}; + } +}; + +/** + * Streaming opened sound data. + * + * Uses a sorted list of contiguous sound data regions (`ContiguousBuffers`s) for + * efficient seeking. + */ +struct SoundDataOpenStream final : ISoundDataOpen +{ + /** + * An OpenAL buffer that goes until `m_end` (exclusive). + */ + struct SoundBufferUntil final + { + ALuint m_end; + RAIIALSoundBuffer m_buffer; + }; + + /** + * A sorted non-empty vector of contiguous buffers. + * The start (inclusive) of each buffer is the end of its predecessor, or + * `m_start` for the first buffer. + */ + struct ContiguousBuffers final + { + ALuint m_start; + std::vector m_buffers; + }; + + std::unique_ptr m_oggfile; + // A sorted vector of non-overlapping, non-contiguous `ContiguousBuffers`s. + std::vector m_bufferss; + + SoundDataOpenStream(std::unique_ptr oggfile, + const OggFileDecodeInfo &decode_info); + + bool isStreaming() const noexcept override { return true; } + + std::tuple getOrLoadBufferAt(ALuint offset) override; + +private: + // offset must be before after_it's m_start and after (after_it-1)'s last m_end + // new buffer will be inserted into m_bufferss before after_it + // returns same as getOrLoadBufferAt + std::tuple loadBufferAt(ALuint offset, + std::vector::iterator after_it); +}; + +} // namespace sound diff --git a/src/client/sound/sound_manager.cpp b/src/client/sound/sound_manager.cpp new file mode 100644 index 0000000000000..fcc3559a51213 --- /dev/null +++ b/src/client/sound/sound_manager.cpp @@ -0,0 +1,527 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +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; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "sound_manager.h" + +#include "sound_singleton.h" +#include "util/numeric.h" // myrand() +#include "filesys.h" +#include "porting.h" + +namespace sound { + +void OpenALSoundManager::stepStreams(f32 dtime) +{ + // spread work across steps + const size_t num_issued_sounds = std::min( + m_sounds_streaming_current_bigstep.size(), + (size_t)std::ceil(m_sounds_streaming_current_bigstep.size() + * dtime / m_stream_timer) + ); + + for (size_t i = 0; i < num_issued_sounds; ++i) { + auto wptr = std::move(m_sounds_streaming_current_bigstep.back()); + m_sounds_streaming_current_bigstep.pop_back(); + + std::shared_ptr snd = wptr.lock(); + if (!snd) + continue; + + if (!snd->stepStream()) + continue; + + // sound still lives and needs more stream-stepping => add to next bigstep + m_sounds_streaming_next_bigstep.push_back(std::move(wptr)); + } + + m_stream_timer -= dtime; + if (m_stream_timer <= 0.0f) { + m_stream_timer = STREAM_BIGSTEP_TIME; + using std::swap; + swap(m_sounds_streaming_current_bigstep, m_sounds_streaming_next_bigstep); + } +} + +void OpenALSoundManager::doFades(f32 dtime) +{ + for (size_t i = 0; i < m_sounds_fading.size();) { + std::shared_ptr snd = m_sounds_fading[i].lock(); + if (snd) { + if (snd->doFade(dtime)) { + // needs more fading later, keep in m_sounds_fading + ++i; + continue; + } + } + + // sound no longer needs to be faded + m_sounds_fading[i] = std::move(m_sounds_fading.back()); + m_sounds_fading.pop_back(); + // continue with same i + } +} + +std::shared_ptr OpenALSoundManager::openSingleSound(const std::string &sound_name) +{ + // if already open, nothing to do + auto it = m_sound_datas_open.find(sound_name); + if (it != m_sound_datas_open.end()) + return it->second; + + // find unopened data + auto it_unopen = m_sound_datas_unopen.find(sound_name); + if (it_unopen == m_sound_datas_unopen.end()) + return nullptr; + std::unique_ptr unopn_snd = std::move(it_unopen->second); + m_sound_datas_unopen.erase(it_unopen); + + // open + std::shared_ptr opn_snd = std::move(*unopn_snd).open(sound_name); + if (!opn_snd) + return nullptr; + m_sound_datas_open.emplace(sound_name, opn_snd); + return opn_snd; +} + +std::string OpenALSoundManager::getLoadedSoundNameFromGroup(const std::string &group_name) +{ + std::string chosen_sound_name = ""; + + auto it_groups = m_sound_groups.find(group_name); + if (it_groups == m_sound_groups.end()) + return ""; + + std::vector &group_sounds = it_groups->second; + while (!group_sounds.empty()) { + // choose one by random + int j = myrand() % group_sounds.size(); + chosen_sound_name = group_sounds[j]; + + // find chosen one + std::shared_ptr snd = openSingleSound(chosen_sound_name); + if (snd) + return chosen_sound_name; + + // it doesn't exist + // remove it from the group and try again + group_sounds[j] = std::move(group_sounds.back()); + group_sounds.pop_back(); + } + + return ""; +} + +std::string OpenALSoundManager::getOrLoadLoadedSoundNameFromGroup(const std::string &group_name) +{ + std::string sound_name = getLoadedSoundNameFromGroup(group_name); + if (!sound_name.empty()) + return sound_name; + + // load + std::vector paths = m_fallback_path_provider + ->getLocalFallbackPathsForSoundname(group_name); + for (const std::string &path : paths) { + if (loadSoundFile(path, path)) + addSoundToGroup(path, group_name); + } + return getLoadedSoundNameFromGroup(group_name); +} + +std::shared_ptr OpenALSoundManager::createPlayingSound( + const std::string &sound_name, bool loop, f32 volume, f32 pitch, + f32 start_time, const std::optional> &pos_vel_opt) +{ + infostream << "OpenALSoundManager: Creating playing sound \"" << sound_name + << "\"" << std::endl; + warn_if_al_error("before createPlayingSound"); + + std::shared_ptr lsnd = openSingleSound(sound_name); + if (!lsnd) { + // does not happen because of the call to getLoadedSoundNameFromGroup + errorstream << "OpenALSoundManager::createPlayingSound: Sound \"" + << sound_name << "\" disappeared." << std::endl; + return nullptr; + } + + if (lsnd->m_decode_info.is_stereo && pos_vel_opt.has_value()) { + warningstream << "OpenALSoundManager::createPlayingSound: " + << "Creating positional stereo sound \"" << sound_name << "\"." + << std::endl; + } + + ALuint source_id; + alGenSources(1, &source_id); + if (warn_if_al_error("createPlayingSound (alGenSources)") != AL_NO_ERROR) { + // happens ie. if there are too many sources (out of memory) + return nullptr; + } + + auto sound = std::make_shared(source_id, std::move(lsnd), loop, + volume, pitch, start_time, pos_vel_opt); + + sound->play(); + if (m_is_paused) + sound->pause(); + warn_if_al_error("createPlayingSound"); + return sound; +} + +void OpenALSoundManager::playSoundGeneric(sound_handle_t id, const std::string &group_name, + bool loop, f32 volume, f32 fade, f32 pitch, bool use_local_fallback, + f32 start_time, const std::optional> &pos_vel_opt) +{ + assert(id != 0); + + if (group_name.empty()) { + reportRemovedSound(id); + return; + } + + // choose random sound name from group name + std::string sound_name = use_local_fallback ? + getOrLoadLoadedSoundNameFromGroup(group_name) : + getLoadedSoundNameFromGroup(group_name); + if (sound_name.empty()) { + infostream << "OpenALSoundManager: \"" << group_name << "\" not found." + << std::endl; + reportRemovedSound(id); + return; + } + + volume = std::max(0.0f, volume); + f32 target_fade_volume = volume; + if (fade > 0.0f) + volume = 0.0f; + + if (!(pitch > 0.0f)) { + warningstream << "OpenALSoundManager::playSoundGeneric: Illegal pitch value: " + << start_time << std::endl; + pitch = 1.0f; + } + + if (!std::isfinite(start_time)) { + warningstream << "OpenALSoundManager::playSoundGeneric: Illegal start_time value: " + << start_time << std::endl; + start_time = 0.0f; + } + + // play it + std::shared_ptr sound = createPlayingSound(sound_name, loop, + volume, pitch, start_time, pos_vel_opt); + if (!sound) { + reportRemovedSound(id); + return; + } + + // add to streaming sounds if streaming + if (sound->isStreaming()) + m_sounds_streaming_next_bigstep.push_back(sound); + + m_sounds_playing.emplace(id, std::move(sound)); + + if (fade > 0.0f) + fadeSound(id, fade, target_fade_volume); +} + +int OpenALSoundManager::removeDeadSounds() +{ + int num_deleted_sounds = 0; + + for (auto it = m_sounds_playing.begin(); it != m_sounds_playing.end();) { + sound_handle_t id = it->first; + PlayingSound &sound = *it->second; + // If dead, remove it + if (sound.isDead()) { + it = m_sounds_playing.erase(it); + reportRemovedSound(id); + ++num_deleted_sounds; + } else { + ++it; + } + } + + return num_deleted_sounds; +} + +OpenALSoundManager::OpenALSoundManager(SoundManagerSingleton *smg, + std::unique_ptr fallback_path_provider) : + Thread("OpenALSoundManager"), + m_fallback_path_provider(std::move(fallback_path_provider)), + m_device(smg->m_device.get()), + m_context(smg->m_context.get()) +{ + SANITY_CHECK(!!m_fallback_path_provider); + + infostream << "Audio: Initialized: OpenAL " << std::endl; +} + +OpenALSoundManager::~OpenALSoundManager() +{ + infostream << "Audio: Deinitializing..." << std::endl; +} + +/* Interface */ + +void OpenALSoundManager::step(f32 dtime) +{ + m_time_until_dead_removal -= dtime; + if (m_time_until_dead_removal <= 0.0f) { + if (!m_sounds_playing.empty()) { + verbosestream << "OpenALSoundManager::step(): " + << m_sounds_playing.size() << " playing sounds, " + << m_sound_datas_unopen.size() << " unopen sounds, " + << m_sound_datas_open.size() << " open sounds and " + << m_sound_groups.size() << " sound groups loaded." + << std::endl; + } + + int num_deleted_sounds = removeDeadSounds(); + + if (num_deleted_sounds != 0) + verbosestream << "OpenALSoundManager::step(): Deleted " + << num_deleted_sounds << " dead playing sounds." << std::endl; + + m_time_until_dead_removal = REMOVE_DEAD_SOUNDS_INTERVAL; + } + + doFades(dtime); + stepStreams(dtime); +} + +void OpenALSoundManager::pauseAll() +{ + for (auto &snd_p : m_sounds_playing) { + PlayingSound &snd = *snd_p.second; + snd.pause(); + } + m_is_paused = true; +} + +void OpenALSoundManager::resumeAll() +{ + for (auto &snd_p : m_sounds_playing) { + PlayingSound &snd = *snd_p.second; + snd.resume(); + } + m_is_paused = false; +} + +void OpenALSoundManager::updateListener(const v3f &pos_, const v3f &vel_, + const v3f &at_, const v3f &up_) +{ + v3f pos = swap_handedness(pos_); + v3f vel = swap_handedness(vel_); + v3f at = swap_handedness(at_); + v3f up = swap_handedness(up_); + ALfloat orientation[6] = {at.X, at.Y, at.Z, up.X, up.Y, up.Z}; + + alListener3f(AL_POSITION, pos.X, pos.Y, pos.Z); + alListener3f(AL_VELOCITY, vel.X, vel.Y, vel.Z); + alListenerfv(AL_ORIENTATION, orientation); + warn_if_al_error("updateListener"); +} + +void OpenALSoundManager::setListenerGain(f32 gain) +{ + alListenerf(AL_GAIN, gain); +} + +bool OpenALSoundManager::loadSoundFile(const std::string &name, const std::string &filepath) +{ + // do not add twice + if (m_sound_datas_open.count(name) != 0 || m_sound_datas_unopen.count(name) != 0) + return false; + + // coarse check + if (!fs::IsFile(filepath)) + return false; + + loadSoundFileNoCheck(name, filepath); + return true; +} + +bool OpenALSoundManager::loadSoundData(const std::string &name, std::string &&filedata) +{ + // do not add twice + if (m_sound_datas_open.count(name) != 0 || m_sound_datas_unopen.count(name) != 0) + return false; + + loadSoundDataNoCheck(name, std::move(filedata)); + return true; +} + +void OpenALSoundManager::loadSoundFileNoCheck(const std::string &name, const std::string &filepath) +{ + // remember for lazy loading + m_sound_datas_unopen.emplace(name, std::make_unique(filepath)); +} + +void OpenALSoundManager::loadSoundDataNoCheck(const std::string &name, std::string &&filedata) +{ + // remember for lazy loading + m_sound_datas_unopen.emplace(name, std::make_unique(std::move(filedata))); +} + +void OpenALSoundManager::addSoundToGroup(const std::string &sound_name, const std::string &group_name) +{ + auto it_groups = m_sound_groups.find(group_name); + if (it_groups != m_sound_groups.end()) + it_groups->second.push_back(sound_name); + else + m_sound_groups.emplace(group_name, std::vector{sound_name}); +} + +void OpenALSoundManager::playSound(sound_handle_t id, const SoundSpec &spec) +{ + return playSoundGeneric(id, spec.name, spec.loop, spec.gain, spec.fade, spec.pitch, + spec.use_local_fallback, spec.start_time, std::nullopt); +} + +void OpenALSoundManager::playSoundAt(sound_handle_t id, const SoundSpec &spec, + const v3f &pos_, const v3f &vel_) +{ + std::optional> pos_vel_opt({ + swap_handedness(pos_), + swap_handedness(vel_) + }); + + return playSoundGeneric(id, spec.name, spec.loop, spec.gain, spec.fade, spec.pitch, + spec.use_local_fallback, spec.start_time, pos_vel_opt); +} + +void OpenALSoundManager::stopSound(sound_handle_t sound) +{ + m_sounds_playing.erase(sound); + reportRemovedSound(sound); +} + +void OpenALSoundManager::fadeSound(sound_handle_t soundid, f32 step, f32 target_gain) +{ + // Ignore the command if step isn't valid. + if (step == 0.0f) + return; + auto sound_it = m_sounds_playing.find(soundid); + if (sound_it == m_sounds_playing.end()) + return; // No sound to fade + PlayingSound &sound = *sound_it->second; + if (sound.fade(step, target_gain)) + m_sounds_fading.emplace_back(sound_it->second); +} + +void OpenALSoundManager::updateSoundPosVel(sound_handle_t id, const v3f &pos_, + const v3f &vel_) +{ + v3f pos = swap_handedness(pos_); + v3f vel = swap_handedness(vel_); + + auto i = m_sounds_playing.find(id); + if (i == m_sounds_playing.end()) + return; + i->second->updatePosVel(pos, vel); +} + +/* Thread stuff */ + +void *OpenALSoundManager::run() +{ + using namespace sound_manager_messages_to_mgr; + + struct MsgVisitor { + enum class Result { Ok, Empty, StopRequested }; + + OpenALSoundManager &mgr; + + Result operator()(std::monostate &&) { + return Result::Empty; } + + Result operator()(PauseAll &&) { + mgr.pauseAll(); return Result::Ok; } + Result operator()(ResumeAll &&) { + mgr.resumeAll(); return Result::Ok; } + + Result operator()(UpdateListener &&msg) { + mgr.updateListener(msg.pos_, msg.vel_, msg.at_, msg.up_); return Result::Ok; } + Result operator()(SetListenerGain &&msg) { + mgr.setListenerGain(msg.gain); return Result::Ok; } + + Result operator()(LoadSoundFile &&msg) { + mgr.loadSoundFileNoCheck(msg.name, msg.filepath); return Result::Ok; } + Result operator()(LoadSoundData &&msg) { + mgr.loadSoundDataNoCheck(msg.name, std::move(msg.filedata)); return Result::Ok; } + Result operator()(AddSoundToGroup &&msg) { + mgr.addSoundToGroup(msg.sound_name, msg.group_name); return Result::Ok; } + + Result operator()(PlaySound &&msg) { + mgr.playSound(msg.id, msg.spec); return Result::Ok; } + Result operator()(PlaySoundAt &&msg) { + mgr.playSoundAt(msg.id, msg.spec, msg.pos_, msg.vel_); return Result::Ok; } + Result operator()(StopSound &&msg) { + mgr.stopSound(msg.sound); return Result::Ok; } + Result operator()(FadeSound &&msg) { + mgr.fadeSound(msg.soundid, msg.step, msg.target_gain); return Result::Ok; } + Result operator()(UpdateSoundPosVel &&msg) { + mgr.updateSoundPosVel(msg.sound, msg.pos_, msg.vel_); return Result::Ok; } + + Result operator()(PleaseStop &&msg) { + return Result::StopRequested; } + }; + + u64 t_step_start = porting::getTimeMs(); + while (true) { + auto get_time_since_last_step = [&] { + return (f32)(porting::getTimeMs() - t_step_start); + }; + auto get_remaining_timeout = [&] { + return (s32)((1.0e3f * SOUNDTHREAD_DTIME) - get_time_since_last_step()); + }; + + bool stop_requested = false; + + while (true) { + SoundManagerMsgToMgr msg = + m_queue_to_mgr.pop_frontNoEx(std::max(get_remaining_timeout(), 0)); + + MsgVisitor::Result res = std::visit(MsgVisitor{*this}, std::move(msg)); + + if (res == MsgVisitor::Result::Empty && get_remaining_timeout() <= 0) { + break; // finished sleeping + } else if (res == MsgVisitor::Result::StopRequested) { + stop_requested = true; + break; + } + } + if (stop_requested) + break; + + f32 dtime = get_time_since_last_step(); + t_step_start = porting::getTimeMs(); + step(dtime); + } + + send(sound_manager_messages_to_proxy::Stopped{}); + + return nullptr; +} + +} // namespace sound diff --git a/src/client/sound/sound_manager.h b/src/client/sound/sound_manager.h new file mode 100644 index 0000000000000..7da18ccbfb4c9 --- /dev/null +++ b/src/client/sound/sound_manager.h @@ -0,0 +1,176 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +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; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include "playing_sound.h" +#include "sound_constants.h" +#include "sound_manager_messages.h" +#include "../sound.h" +#include "threading/thread.h" +#include "util/container.h" // MutexedQueue + +namespace sound { + +class SoundManagerSingleton; + +/* + * The SoundManager thread + * + * It's not an ISoundManager. It doesn't allocate ids, and doesn't accept id 0. + * All sound loading and interaction with OpenAL happens in this thread, and in + * SoundManagerSingleton. + * Access from other threads happens via ProxySoundManager. + * + * See sound_constants.h for more details. + */ + +class OpenALSoundManager final : public Thread +{ +private: + std::unique_ptr m_fallback_path_provider; + + ALCdevice *m_device; + ALCcontext *m_context; + + // time in seconds until which removeDeadSounds will be called again + f32 m_time_until_dead_removal = REMOVE_DEAD_SOUNDS_INTERVAL; + + // loaded sounds + std::unordered_map> m_sound_datas_unopen; + std::unordered_map> m_sound_datas_open; + // sound groups + std::unordered_map> m_sound_groups; + + // currently playing sounds + std::unordered_map> m_sounds_playing; + + // streamed sounds + std::vector> m_sounds_streaming_current_bigstep; + std::vector> m_sounds_streaming_next_bigstep; + // time left until current bigstep finishes + f32 m_stream_timer = STREAM_BIGSTEP_TIME; + + std::vector> m_sounds_fading; + + // if true, all sounds will be directly paused after creation + bool m_is_paused = false; + +public: + // used for communication with ProxySoundManager + MutexedQueue m_queue_to_mgr; + MutexedQueue m_queue_to_proxy; + +private: + void stepStreams(f32 dtime); + void doFades(f32 dtime); + + /** + * Gives the open sound for a loaded sound. + * Opens the sound if currently unopened. + * + * @param sound_name Name of the sound. + * @return The open sound. + */ + std::shared_ptr openSingleSound(const std::string &sound_name); + + /** + * Gets a random sound name from a group. + * + * @param group_name The name of the sound group. + * @return The name of a sound in the group, or "" on failure. Getting the + * sound with `openSingleSound` directly afterwards will not fail. + */ + std::string getLoadedSoundNameFromGroup(const std::string &group_name); + + /** + * Same as `getLoadedSoundNameFromGroup`, but if sound does not exist, try to + * load from local files. + */ + std::string getOrLoadLoadedSoundNameFromGroup(const std::string &group_name); + + std::shared_ptr createPlayingSound(const std::string &sound_name, + bool loop, f32 volume, f32 pitch, f32 start_time, + const std::optional> &pos_vel_opt); + + void playSoundGeneric(sound_handle_t id, const std::string &group_name, bool loop, + f32 volume, f32 fade, f32 pitch, bool use_local_fallback, f32 start_time, + const std::optional> &pos_vel_opt); + + /** + * Deletes sounds that are dead (=finished). + * + * @return Number of removed sounds. + */ + int removeDeadSounds(); + +public: + OpenALSoundManager(SoundManagerSingleton *smg, + std::unique_ptr fallback_path_provider); + + ~OpenALSoundManager() override; + + DISABLE_CLASS_COPY(OpenALSoundManager) + +private: + /* Similar to ISoundManager */ + + void step(f32 dtime); + void pauseAll(); + void resumeAll(); + + void updateListener(const v3f &pos_, const v3f &vel_, const v3f &at_, const v3f &up_); + void setListenerGain(f32 gain); + + bool loadSoundFile(const std::string &name, const std::string &filepath); + bool loadSoundData(const std::string &name, std::string &&filedata); + void loadSoundFileNoCheck(const std::string &name, const std::string &filepath); + void loadSoundDataNoCheck(const std::string &name, std::string &&filedata); + void addSoundToGroup(const std::string &sound_name, const std::string &group_name); + + void playSound(sound_handle_t id, const SoundSpec &spec); + void playSoundAt(sound_handle_t id, const SoundSpec &spec, const v3f &pos_, + const v3f &vel_); + void stopSound(sound_handle_t sound); + void fadeSound(sound_handle_t soundid, f32 step, f32 target_gain); + void updateSoundPosVel(sound_handle_t sound, const v3f &pos_, const v3f &vel_); + +protected: + /* Thread stuff */ + + void *run() override; + +private: + void send(SoundManagerMsgToProxy msg) + { + m_queue_to_proxy.push_back(std::move(msg)); + } + + void reportRemovedSound(sound_handle_t id) + { + send(sound_manager_messages_to_proxy::ReportRemovedSound{id}); + } +}; + +} // namespace sound diff --git a/src/client/sound/sound_manager_messages.h b/src/client/sound/sound_manager_messages.h new file mode 100644 index 0000000000000..41f4ffac1bfba --- /dev/null +++ b/src/client/sound/sound_manager_messages.h @@ -0,0 +1,84 @@ +/* +Minetest +Copyright (C) 2023 DS + +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; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include "../sound.h" +#include "../../sound.h" +#include + +namespace sound { + +namespace sound_manager_messages_to_mgr { + struct PauseAll {}; + struct ResumeAll {}; + + struct UpdateListener { v3f pos_; v3f vel_; v3f at_; v3f up_; }; + struct SetListenerGain { f32 gain; }; + + struct LoadSoundFile { std::string name; std::string filepath; }; + struct LoadSoundData { std::string name; std::string filedata; }; + struct AddSoundToGroup { std::string sound_name; std::string group_name; }; + + struct PlaySound { sound_handle_t id; SoundSpec spec; }; + struct PlaySoundAt { sound_handle_t id; SoundSpec spec; v3f pos_; v3f vel_; }; + struct StopSound { sound_handle_t sound; }; + struct FadeSound { sound_handle_t soundid; f32 step; f32 target_gain; }; + struct UpdateSoundPosVel { sound_handle_t sound; v3f pos_; v3f vel_; }; + + struct PleaseStop {}; +} + +using SoundManagerMsgToMgr = std::variant< + std::monostate, + + sound_manager_messages_to_mgr::PauseAll, + sound_manager_messages_to_mgr::ResumeAll, + + sound_manager_messages_to_mgr::UpdateListener, + sound_manager_messages_to_mgr::SetListenerGain, + + sound_manager_messages_to_mgr::LoadSoundFile, + sound_manager_messages_to_mgr::LoadSoundData, + sound_manager_messages_to_mgr::AddSoundToGroup, + + sound_manager_messages_to_mgr::PlaySound, + sound_manager_messages_to_mgr::PlaySoundAt, + sound_manager_messages_to_mgr::StopSound, + sound_manager_messages_to_mgr::FadeSound, + sound_manager_messages_to_mgr::UpdateSoundPosVel, + + sound_manager_messages_to_mgr::PleaseStop + >; + +namespace sound_manager_messages_to_proxy { + struct ReportRemovedSound { sound_handle_t id; }; + + struct Stopped {}; +} + +using SoundManagerMsgToProxy = std::variant< + std::monostate, + + sound_manager_messages_to_proxy::ReportRemovedSound, + + sound_manager_messages_to_proxy::Stopped + >; + +} // namespace sound diff --git a/src/client/sound_openal.cpp b/src/client/sound/sound_openal.cpp similarity index 90% rename from src/client/sound_openal.cpp rename to src/client/sound/sound_openal.cpp index 9baa0d152b825..89f4b57ad9154 100644 --- a/src/client/sound_openal.cpp +++ b/src/client/sound/sound_openal.cpp @@ -22,7 +22,9 @@ with this program; ifnot, write to the Free Software Foundation, Inc., */ #include "sound_openal.h" -#include "sound_openal_internal.h" + +#include "sound_singleton.h" +#include "proxy_sound_manager.h" std::shared_ptr g_sound_manager_singleton; @@ -38,5 +40,5 @@ std::shared_ptr createSoundManagerSingleton() std::unique_ptr createOpenALSoundManager(SoundManagerSingleton *smg, std::unique_ptr fallback_path_provider) { - return std::make_unique(smg, std::move(fallback_path_provider)); + return std::make_unique(smg, std::move(fallback_path_provider)); }; diff --git a/src/client/sound_openal.h b/src/client/sound/sound_openal.h similarity index 90% rename from src/client/sound_openal.h rename to src/client/sound/sound_openal.h index 50762331b98ea..bae4fbd79f0b6 100644 --- a/src/client/sound_openal.h +++ b/src/client/sound/sound_openal.h @@ -19,11 +19,12 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once -#include "sound.h" - +#include "client/sound.h" #include -class SoundManagerSingleton; +namespace sound { class SoundManagerSingleton; } +using sound::SoundManagerSingleton; + extern std::shared_ptr g_sound_manager_singleton; std::shared_ptr createSoundManagerSingleton(); diff --git a/src/client/sound/sound_singleton.cpp b/src/client/sound/sound_singleton.cpp new file mode 100644 index 0000000000000..4a600243ccd64 --- /dev/null +++ b/src/client/sound/sound_singleton.cpp @@ -0,0 +1,73 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +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; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "sound_singleton.h" + +namespace sound { + +bool SoundManagerSingleton::init() +{ + if (!(m_device = unique_ptr_alcdevice(alcOpenDevice(nullptr)))) { + errorstream << "Audio: Global Initialization: Failed to open device" << std::endl; + return false; + } + + if (!(m_context = unique_ptr_alccontext(alcCreateContext(m_device.get(), nullptr)))) { + errorstream << "Audio: Global Initialization: Failed to create context" << std::endl; + return false; + } + + if (!alcMakeContextCurrent(m_context.get())) { + errorstream << "Audio: Global Initialization: Failed to make current context" << std::endl; + return false; + } + + alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED); + + // Speed of sound in nodes per second + // FIXME: This value assumes 1 node sidelength = 1 meter, and "normal" air. + // Ideally this should be mod-controlled. + alSpeedOfSound(343.3f); + + // doppler effect turned off for now, for best backwards compatibility + alDopplerFactor(0.0f); + + if (alGetError() != AL_NO_ERROR) { + errorstream << "Audio: Global Initialization: OpenAL Error " << alGetError() << std::endl; + return false; + } + + infostream << "Audio: Global Initialized: OpenAL " << alGetString(AL_VERSION) + << ", using " << alcGetString(m_device.get(), ALC_DEVICE_SPECIFIER) + << std::endl; + + return true; +} + +SoundManagerSingleton::~SoundManagerSingleton() +{ + infostream << "Audio: Global Deinitialized." << std::endl; +} + +} // namespace sound diff --git a/src/client/sound/sound_singleton.h b/src/client/sound/sound_singleton.h new file mode 100644 index 0000000000000..5168c1c9becd3 --- /dev/null +++ b/src/client/sound/sound_singleton.h @@ -0,0 +1,64 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +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; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include "al_helpers.h" + +namespace sound { + +/** + * Class for the openal device and context + */ +class SoundManagerSingleton +{ +public: + struct AlcDeviceDeleter { + void operator()(ALCdevice *p) + { + alcCloseDevice(p); + } + }; + + struct AlcContextDeleter { + void operator()(ALCcontext *p) + { + alcMakeContextCurrent(nullptr); + alcDestroyContext(p); + } + }; + + using unique_ptr_alcdevice = std::unique_ptr; + using unique_ptr_alccontext = std::unique_ptr; + + unique_ptr_alcdevice m_device; + unique_ptr_alccontext m_context; + +public: + bool init(); + + ~SoundManagerSingleton(); +}; + +} // namespace sound diff --git a/src/client/sound_openal_internal.cpp b/src/client/sound_openal_internal.cpp deleted file mode 100644 index 5ab9e4c7c5b61..0000000000000 --- a/src/client/sound_openal_internal.cpp +++ /dev/null @@ -1,1128 +0,0 @@ -/* -Minetest -Copyright (C) 2022 DS -Copyright (C) 2013 celeron55, Perttu Ahola -OpenAL support based on work by: -Copyright (C) 2011 Sebastian 'Bahamada' Rühl -Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits -Copyright (C) 2011 Giuseppe Bilotta - -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; ifnot, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -#include "sound_openal_internal.h" - -#include "util/numeric.h" // myrand() -#include "../sound.h" -#include "filesys.h" -#include "settings.h" -#include -#include - -/* - * Helpers - */ - -static const char *getAlErrorString(ALenum err) noexcept -{ - switch (err) { - case AL_NO_ERROR: - return "no error"; - case AL_INVALID_NAME: - return "invalid name"; - case AL_INVALID_ENUM: - return "invalid enum"; - case AL_INVALID_VALUE: - return "invalid value"; - case AL_INVALID_OPERATION: - return "invalid operation"; - case AL_OUT_OF_MEMORY: - return "out of memory"; - default: - return ""; - } -} - -static ALenum warn_if_al_error(const char *desc) -{ - ALenum err = alGetError(); - if (err == AL_NO_ERROR) - return err; - warningstream << "[OpenAL Error] " << desc << ": " << getAlErrorString(err) - << std::endl; - return err; -} - -/** - * Transforms vectors from a left-handed coordinate system to a right-handed one - * and vice-versa. - * (Needed because Minetest uses a left-handed one and OpenAL a right-handed one.) - */ -static inline v3f swap_handedness(v3f v) noexcept -{ - return v3f(-v.X, v.Y, v.Z); -} - -/* - * RAIIALSoundBuffer struct - */ - -RAIIALSoundBuffer &RAIIALSoundBuffer::operator=(RAIIALSoundBuffer &&other) noexcept -{ - if (&other != this) - reset(other.release()); - return *this; -} - -void RAIIALSoundBuffer::reset(ALuint buf) noexcept -{ - if (m_buffer != 0) { - alDeleteBuffers(1, &m_buffer); - warn_if_al_error("Failed to free sound buffer"); - } - - m_buffer = buf; -} - -RAIIALSoundBuffer RAIIALSoundBuffer::generate() noexcept -{ - ALuint buf; - alGenBuffers(1, &buf); - return RAIIALSoundBuffer(buf); -} - -/* - * OggVorbisBufferSource struct - */ - -size_t OggVorbisBufferSource::read_func(void *ptr, size_t size, size_t nmemb, - void *datasource) noexcept -{ - OggVorbisBufferSource *s = (OggVorbisBufferSource *)datasource; - size_t copied_size = MYMIN(s->buf.size() - s->cur_offset, size); - memcpy(ptr, s->buf.data() + s->cur_offset, copied_size); - s->cur_offset += copied_size; - return copied_size; -} - -int OggVorbisBufferSource::seek_func(void *datasource, ogg_int64_t offset, int whence) noexcept -{ - OggVorbisBufferSource *s = (OggVorbisBufferSource *)datasource; - if (whence == SEEK_SET) { - if (offset < 0 || (size_t)offset > s->buf.size()) { - // offset out of bounds - return -1; - } - s->cur_offset = offset; - return 0; - } else if (whence == SEEK_CUR) { - if ((size_t)MYMIN(-offset, 0) > s->cur_offset - || s->cur_offset + offset > s->buf.size()) { - // offset out of bounds - return -1; - } - s->cur_offset += offset; - return 0; - } else if (whence == SEEK_END) { - if (offset > 0 || (size_t)-offset > s->buf.size()) { - // offset out of bounds - return -1; - } - s->cur_offset = s->buf.size() - offset; - return 0; - } - return -1; -} - -int OggVorbisBufferSource::close_func(void *datasource) noexcept -{ - auto s = reinterpret_cast(datasource); - delete s; - return 0; -} - -long OggVorbisBufferSource::tell_func(void *datasource) noexcept -{ - OggVorbisBufferSource *s = (OggVorbisBufferSource *)datasource; - return s->cur_offset; -} - -const ov_callbacks OggVorbisBufferSource::s_ov_callbacks = { - &OggVorbisBufferSource::read_func, - &OggVorbisBufferSource::seek_func, - &OggVorbisBufferSource::close_func, - &OggVorbisBufferSource::tell_func -}; - -/* - * RAIIOggFile struct - */ - -std::optional RAIIOggFile::getDecodeInfo(const std::string &filename_for_logging) -{ - OggFileDecodeInfo ret; - - vorbis_info *pInfo = ov_info(&m_file, -1); - if (!pInfo) - return std::nullopt; - - ret.name_for_logging = filename_for_logging; - - if (pInfo->channels == 1) { - ret.is_stereo = false; - ret.format = AL_FORMAT_MONO16; - ret.bytes_per_sample = 2; - } else if (pInfo->channels == 2) { - ret.is_stereo = true; - ret.format = AL_FORMAT_STEREO16; - ret.bytes_per_sample = 4; - } else { - warningstream << "Audio: Can't decode. Sound is neither mono nor stereo: " - << ret.name_for_logging << std::endl; - return std::nullopt; - } - - ret.freq = pInfo->rate; - - ret.length_samples = static_cast(ov_pcm_total(&m_file, -1)); - ret.length_seconds = static_cast(ov_time_total(&m_file, -1)); - - return ret; -} - -RAIIALSoundBuffer RAIIOggFile::loadBuffer(const OggFileDecodeInfo &decode_info, - ALuint pcm_start, ALuint pcm_end) -{ - constexpr int endian = 0; // 0 for Little-Endian, 1 for Big-Endian - constexpr int word_size = 2; // we use s16 samples - constexpr int word_signed = 1; // ^ - - // seek - if (ov_pcm_tell(&m_file) != pcm_start) { - if (ov_pcm_seek(&m_file, pcm_start) != 0) { - warningstream << "Audio: Error decoding (could not seek) " - << decode_info.name_for_logging << std::endl; - return RAIIALSoundBuffer(); - } - } - - const size_t size = static_cast(pcm_end - pcm_start) - * decode_info.bytes_per_sample; - - std::unique_ptr snd_buffer(new char[size]); - - // read size bytes - size_t read_count = 0; - int bitStream; - while (read_count < size) { - // Read up to a buffer's worth of decoded sound data - long num_bytes = ov_read(&m_file, &snd_buffer[read_count], size - read_count, - endian, word_size, word_signed, &bitStream); - - if (num_bytes <= 0) { - warningstream << "Audio: Error decoding " - << decode_info.name_for_logging << std::endl; - return RAIIALSoundBuffer(); - } - - read_count += num_bytes; - } - - // load buffer to openal - RAIIALSoundBuffer snd_buffer_id = RAIIALSoundBuffer::generate(); - alBufferData(snd_buffer_id.get(), decode_info.format, &(snd_buffer[0]), size, - decode_info.freq); - - ALenum error = alGetError(); - if (error != AL_NO_ERROR) { - warningstream << "Audio: OpenAL error: " << getAlErrorString(error) - << "preparing sound buffer for sound \"" - << decode_info.name_for_logging << "\"" << std::endl; - } - - return snd_buffer_id; -} - -/* - * SoundManagerSingleton class - */ - -bool SoundManagerSingleton::init() -{ - if (!(m_device = unique_ptr_alcdevice(alcOpenDevice(nullptr)))) { - errorstream << "Audio: Global Initialization: Failed to open device" << std::endl; - return false; - } - - if (!(m_context = unique_ptr_alccontext(alcCreateContext(m_device.get(), nullptr)))) { - errorstream << "Audio: Global Initialization: Failed to create context" << std::endl; - return false; - } - - if (!alcMakeContextCurrent(m_context.get())) { - errorstream << "Audio: Global Initialization: Failed to make current context" << std::endl; - return false; - } - - alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED); - - // Speed of sound in nodes per second - // FIXME: This value assumes 1 node sidelength = 1 meter, and "normal" air. - // Ideally this should be mod-controlled. - alSpeedOfSound(343.3f); - - // doppler effect turned off for now, for best backwards compatibility - alDopplerFactor(0.0f); - - if (alGetError() != AL_NO_ERROR) { - errorstream << "Audio: Global Initialization: OpenAL Error " << alGetError() << std::endl; - return false; - } - - infostream << "Audio: Global Initialized: OpenAL " << alGetString(AL_VERSION) - << ", using " << alcGetString(m_device.get(), ALC_DEVICE_SPECIFIER) - << std::endl; - - return true; -} - -SoundManagerSingleton::~SoundManagerSingleton() -{ - infostream << "Audio: Global Deinitialized." << std::endl; -} - -/* - * ISoundDataOpen struct - */ - -std::shared_ptr ISoundDataOpen::fromOggFile(std::unique_ptr oggfile, - const std::string &filename_for_logging) -{ - // Get some information about the OGG file - std::optional decode_info = oggfile->getDecodeInfo(filename_for_logging); - if (!decode_info.has_value()) { - warningstream << "Audio: Error decoding " - << filename_for_logging << std::endl; - return nullptr; - } - - // use duration (in seconds) to decide whether to load all at once or to stream - if (decode_info->length_seconds <= SOUND_DURATION_MAX_SINGLE) { - return std::make_shared(std::move(oggfile), *decode_info); - } else { - return std::make_shared(std::move(oggfile), *decode_info); - } -} - -/* - * SoundDataUnopenBuffer struct - */ - -std::shared_ptr SoundDataUnopenBuffer::open(const std::string &sound_name) && -{ - // load from m_buffer - - auto oggfile = std::make_unique(); - - auto buffer_source = std::make_unique(); - buffer_source->buf = std::move(m_buffer); - - oggfile->m_needs_clear = true; - if (ov_open_callbacks(buffer_source.release(), oggfile->get(), nullptr, 0, - OggVorbisBufferSource::s_ov_callbacks) != 0) { - warningstream << "Audio: Error opening " << sound_name << " for decoding" - << std::endl; - return nullptr; - } - - return ISoundDataOpen::fromOggFile(std::move(oggfile), sound_name); -} - -/* - * SoundDataUnopenFile struct - */ - -std::shared_ptr SoundDataUnopenFile::open(const std::string &sound_name) && -{ - // load from file at m_path - - auto oggfile = std::make_unique(); - - if (ov_fopen(m_path.c_str(), oggfile->get()) != 0) { - warningstream << "Audio: Error opening " << m_path << " for decoding" - << std::endl; - return nullptr; - } - oggfile->m_needs_clear = true; - - return ISoundDataOpen::fromOggFile(std::move(oggfile), sound_name); -} - -/* - * SoundDataOpenBuffer struct - */ - -SoundDataOpenBuffer::SoundDataOpenBuffer(std::unique_ptr oggfile, - const OggFileDecodeInfo &decode_info) : ISoundDataOpen(decode_info) -{ - m_buffer = oggfile->loadBuffer(m_decode_info, 0, m_decode_info.length_samples); - if (m_buffer.get() == 0) { - warningstream << "SoundDataOpenBuffer: Failed to load sound \"" - << m_decode_info.name_for_logging << "\"" << std::endl; - return; - } -} - -/* - * SoundDataOpenStream struct - */ - -SoundDataOpenStream::SoundDataOpenStream(std::unique_ptr oggfile, - const OggFileDecodeInfo &decode_info) : - ISoundDataOpen(decode_info), m_oggfile(std::move(oggfile)) -{ - // do nothing here. buffers are loaded at getOrLoadBufferAt -} - -std::tuple SoundDataOpenStream::getOrLoadBufferAt(ALuint offset) -{ - if (offset >= m_decode_info.length_samples) - return {0, m_decode_info.length_samples, 0}; - - // find the right-most ContiguousBuffers, such that `m_start <= offset` - // equivalent: the first element from the right such that `!(m_start > offset)` - // (from the right, `offset` is a lower bound to the `m_start`s) - auto lower_rit = std::lower_bound(m_bufferss.rbegin(), m_bufferss.rend(), offset, - [](const ContiguousBuffers &bufs, ALuint offset) { - return bufs.m_start > offset; - }); - - if (lower_rit != m_bufferss.rend()) { - std::vector &bufs = lower_rit->m_buffers; - // find the left-most SoundBufferUntil, such that `m_end > offset` - // equivalent: the first element from the left such that `m_end > offset` - // (returns first element where comp gives true) - auto upper_it = std::upper_bound(bufs.begin(), bufs.end(), offset, - [](ALuint offset, const SoundBufferUntil &buf) { - return offset < buf.m_end; - }); - - if (upper_it != bufs.end()) { - ALuint start = upper_it == bufs.begin() ? lower_rit->m_start - : (upper_it - 1)->m_end; - return {upper_it->m_buffer.get(), upper_it->m_end, offset - start}; - } - } - - // no loaded buffer starts before or at `offset` - // or no loaded buffer (that starts before or at `offset`) ends after `offset` - - // lower_rit, but not reverse and 1 farther - auto after_it = m_bufferss.begin() + (m_bufferss.rend() - lower_rit); - - return loadBufferAt(offset, after_it); -} - -std::tuple SoundDataOpenStream::loadBufferAt(ALuint offset, - std::vector::iterator after_it) -{ - bool has_before = after_it != m_bufferss.begin(); - bool has_after = after_it != m_bufferss.end(); - - ALuint end_before = has_before ? (after_it - 1)->m_buffers.back().m_end : 0; - ALuint start_after = has_after ? after_it->m_start : m_decode_info.length_samples; - - const ALuint min_buf_len_samples = m_decode_info.freq * MIN_STREAM_BUFFER_LENGTH; - - // - // 1) Find the actual start and end of the new buffer - // - - ALuint new_buf_start = offset; - ALuint new_buf_end = offset + min_buf_len_samples; - - // Don't load into next buffer, or past the end - if (new_buf_end > start_after) { - new_buf_end = start_after; - // Also move start (for min buf size) (but not *into* previous buffer) - if (new_buf_end - new_buf_start < min_buf_len_samples) { - new_buf_start = std::max( - end_before, - new_buf_end < min_buf_len_samples ? 0 - : new_buf_end - min_buf_len_samples - ); - } - } - - // Widen if space to right or left is smaller than min buf size - if (new_buf_start - end_before < min_buf_len_samples) - new_buf_start = end_before; - if (start_after - new_buf_end < min_buf_len_samples) - new_buf_end = start_after; - - // - // 2) Load [new_buf_start, new_buf_end) - // - - // If it fails, we get a 0-buffer. we store it and won't try loading again - RAIIALSoundBuffer new_buf = m_oggfile->loadBuffer(m_decode_info, new_buf_start, - new_buf_end); - - // - // 3) Insert before after_it - // - - // Choose ContiguousBuffers to add the new SoundBufferUntil into: - // * `after_it - 1` (=before) if existent and if there's no space between its - // last buffer and the new buffer - // * A new ContiguousBuffers otherwise - auto it = has_before && new_buf_start == end_before ? after_it - 1 - : m_bufferss.insert(after_it, ContiguousBuffers{new_buf_start, {}}); - - // Add the new SoundBufferUntil - size_t new_buf_i = it->m_buffers.size(); - it->m_buffers.push_back(SoundBufferUntil{new_buf_end, std::move(new_buf)}); - - if (has_after && new_buf_end == start_after) { - // Merge after into my ContiguousBuffers - auto &bufs = it->m_buffers; - auto &bufs_after = (it + 1)->m_buffers; - bufs.insert(bufs.end(), std::make_move_iterator(bufs_after.begin()), - std::make_move_iterator(bufs_after.end())); - it = m_bufferss.erase(it + 1) - 1; - } - - return {it->m_buffers[new_buf_i].m_buffer.get(), new_buf_end, offset - new_buf_start}; -} - -/* - * PlayingSound class - */ - -PlayingSound::PlayingSound(ALuint source_id, std::shared_ptr data, - bool loop, f32 volume, f32 pitch, f32 start_time, - const std::optional> &pos_vel_opt) - : m_source_id(source_id), m_data(std::move(data)), m_looping(loop), - m_is_positional(pos_vel_opt.has_value()) -{ - // Calculate actual start_time (see lua_api.txt for specs) - f32 len_seconds = m_data->m_decode_info.length_seconds; - f32 len_samples = m_data->m_decode_info.length_samples; - if (!m_looping) { - if (start_time < 0.0f) { - start_time = std::fmax(start_time + len_seconds, 0.0f); - } else if (start_time >= len_seconds) { - // No sound - m_next_sample_pos = len_samples; - return; - } - } else { - // Modulo offset to be within looping time - start_time = start_time - std::floor(start_time / len_seconds) * len_seconds; - } - - // Queue first buffers - - m_next_sample_pos = std::min((start_time / len_seconds) * len_samples, len_samples); - - if (m_looping && m_next_sample_pos == len_samples) - m_next_sample_pos = 0; - - if (!m_data->isStreaming()) { - // If m_next_sample_pos >= len_samples, buf will be 0, and setting it as - // AL_BUFFER is a NOP (source stays AL_UNDETERMINED). => No sound will be - // played. - - auto [buf, buf_end, offset_in_buf] = m_data->getOrLoadBufferAt(m_next_sample_pos); - m_next_sample_pos = buf_end; - - alSourcei(m_source_id, AL_BUFFER, buf); - alSourcei(m_source_id, AL_SAMPLE_OFFSET, offset_in_buf); - - alSourcei(m_source_id, AL_LOOPING, m_looping ? AL_TRUE : AL_FALSE); - - warn_if_al_error("when creating non-streaming sound"); - - } else { - // Start with 2 buffers - ALuint buf_ids[2]; - - // If m_next_sample_pos >= len_samples (happens only if not looped), one - // or both of buf_ids will be 0. Queuing 0 is a NOP. - - auto [buf0, buf0_end, offset_in_buf0] = m_data->getOrLoadBufferAt(m_next_sample_pos); - buf_ids[0] = buf0; - m_next_sample_pos = buf0_end; - - if (m_looping && m_next_sample_pos == len_samples) - m_next_sample_pos = 0; - - auto [buf1, buf1_end, offset_in_buf1] = m_data->getOrLoadBufferAt(m_next_sample_pos); - buf_ids[1] = buf1; - m_next_sample_pos = buf1_end; - assert(offset_in_buf1 == 0); - - alSourceQueueBuffers(m_source_id, 2, buf_ids); - alSourcei(m_source_id, AL_SAMPLE_OFFSET, offset_in_buf0); - - // We can't use AL_LOOPING because more buffers are queued later - // looping is therefore done manually - - m_stopped_means_dead = false; - - warn_if_al_error("when creating streaming sound"); - } - - // Set initial pos, volume, pitch - if (m_is_positional) { - updatePosVel(pos_vel_opt->first, pos_vel_opt->second); - } else { - // Make position-less - alSourcei(m_source_id, AL_SOURCE_RELATIVE, true); - alSource3f(m_source_id, AL_POSITION, 0.0f, 0.0f, 0.0f); - alSource3f(m_source_id, AL_VELOCITY, 0.0f, 0.0f, 0.0f); - warn_if_al_error("PlayingSound::PlayingSound at making position-less"); - } - setGain(volume); - setPitch(pitch); -} - -bool PlayingSound::stepStream() -{ - if (isDead()) - return false; - - // unqueue finished buffers - ALint num_unqueued_bufs = 0; - alGetSourcei(m_source_id, AL_BUFFERS_PROCESSED, &num_unqueued_bufs); - if (num_unqueued_bufs == 0) - return true; - // We always have 2 buffers enqueued at most - SANITY_CHECK(num_unqueued_bufs <= 2); - ALuint unqueued_buffer_ids[2]; - alSourceUnqueueBuffers(m_source_id, num_unqueued_bufs, unqueued_buffer_ids); - - // Fill up again - for (ALint i = 0; i < num_unqueued_bufs; ++i) { - if (m_next_sample_pos == m_data->m_decode_info.length_samples) { - // Reached end - if (m_looping) { - m_next_sample_pos = 0; - } else { - m_stopped_means_dead = true; - return false; - } - } - - auto [buf, buf_end, offset_in_buf] = m_data->getOrLoadBufferAt(m_next_sample_pos); - m_next_sample_pos = buf_end; - assert(offset_in_buf == 0); - - alSourceQueueBuffers(m_source_id, 1, &buf); - - // Start again if queue was empty and resulted in stop - if (getState() == AL_STOPPED) { - play(); - warningstream << "PlayingSound::stepStream: Sound queue ran empty for \"" - << m_data->m_decode_info.name_for_logging << "\"" << std::endl; - } - } - - return true; -} - -bool PlayingSound::fade(f32 step, f32 target_gain) noexcept -{ - bool already_fading = m_fade_state.has_value(); - - target_gain = MYMAX(target_gain, 0.0f); // 0.0f if nan - step = target_gain - getGain() > 0.0f ? std::abs(step) : -std::abs(step); - - m_fade_state = FadeState{step, target_gain}; - - return !already_fading; -} - -bool PlayingSound::doFade(f32 dtime) noexcept -{ - if (!m_fade_state || isDead()) - return false; - - FadeState &fade = *m_fade_state; - assert(fade.step != 0.0f); - - f32 current_gain = getGain(); - current_gain += fade.step * dtime; - - if (fade.step < 0.0f) - current_gain = std::max(current_gain, fade.target_gain); - else - current_gain = std::min(current_gain, fade.target_gain); - - if (current_gain <= 0.0f) { - // stop sound - m_stopped_means_dead = true; - alSourceStop(m_source_id); - - m_fade_state = std::nullopt; - return false; - } - - setGain(current_gain); - - if (current_gain == fade.target_gain) { - m_fade_state = std::nullopt; - return false; - } else { - return true; - } -} - -void PlayingSound::updatePosVel(const v3f &pos, const v3f &vel) noexcept -{ - alSourcei(m_source_id, AL_SOURCE_RELATIVE, false); - alSource3f(m_source_id, AL_POSITION, pos.X, pos.Y, pos.Z); - alSource3f(m_source_id, AL_VELOCITY, vel.X, vel.Y, vel.Z); - // Using alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED) and setting reference - // distance to clamp gain at <1 node distance avoids excessive volume when - // closer. - alSourcef(m_source_id, AL_REFERENCE_DISTANCE, 1.0f); - - warn_if_al_error("PlayingSound::updatePosVel"); -} - -void PlayingSound::setGain(f32 gain) noexcept -{ - // AL_REFERENCE_DISTANCE was once reduced from 3 nodes to 1 node. - // We compensate this by multiplying the volume by 3. - if (m_is_positional) - gain *= 3.0f; - - alSourcef(m_source_id, AL_GAIN, gain); -} - -f32 PlayingSound::getGain() noexcept -{ - ALfloat gain; - alGetSourcef(m_source_id, AL_GAIN, &gain); - // Same as above, but inverse. - if (m_is_positional) - gain *= 1.0f/3.0f; - return gain; -} - -/* - * OpenALSoundManager class - */ - -void OpenALSoundManager::stepStreams(f32 dtime) -{ - // spread work across steps - const size_t num_issued_sounds = std::min( - m_sounds_streaming_current_bigstep.size(), - (size_t)std::ceil(m_sounds_streaming_current_bigstep.size() - * dtime / m_stream_timer) - ); - - for (size_t i = 0; i < num_issued_sounds; ++i) { - auto wptr = std::move(m_sounds_streaming_current_bigstep.back()); - m_sounds_streaming_current_bigstep.pop_back(); - - std::shared_ptr snd = wptr.lock(); - if (!snd) - continue; - - if (!snd->stepStream()) - continue; - - // sound still lives and needs more stream-stepping => add to next bigstep - m_sounds_streaming_next_bigstep.push_back(std::move(wptr)); - } - - m_stream_timer -= dtime; - if (m_stream_timer <= 0.0f) { - m_stream_timer = STREAM_BIGSTEP_TIME; - using std::swap; - swap(m_sounds_streaming_current_bigstep, m_sounds_streaming_next_bigstep); - } -} - -void OpenALSoundManager::doFades(f32 dtime) -{ - for (size_t i = 0; i < m_sounds_fading.size();) { - std::shared_ptr snd = m_sounds_fading[i].lock(); - if (snd) { - if (snd->doFade(dtime)) { - // needs more fading later, keep in m_sounds_fading - ++i; - continue; - } - } - - // sound no longer needs to be faded - m_sounds_fading[i] = std::move(m_sounds_fading.back()); - m_sounds_fading.pop_back(); - // continue with same i - } -} - -std::shared_ptr OpenALSoundManager::openSingleSound(const std::string &sound_name) -{ - // if already open, nothing to do - auto it = m_sound_datas_open.find(sound_name); - if (it != m_sound_datas_open.end()) - return it->second; - - // find unopened data - auto it_unopen = m_sound_datas_unopen.find(sound_name); - if (it_unopen == m_sound_datas_unopen.end()) - return nullptr; - std::unique_ptr unopn_snd = std::move(it_unopen->second); - m_sound_datas_unopen.erase(it_unopen); - - // open - std::shared_ptr opn_snd = std::move(*unopn_snd).open(sound_name); - if (!opn_snd) - return nullptr; - m_sound_datas_open.emplace(sound_name, opn_snd); - return opn_snd; -} - -std::string OpenALSoundManager::getLoadedSoundNameFromGroup(const std::string &group_name) -{ - std::string chosen_sound_name = ""; - - auto it_groups = m_sound_groups.find(group_name); - if (it_groups == m_sound_groups.end()) - return chosen_sound_name; - - std::vector &group_sounds = it_groups->second; - while (!group_sounds.empty()) { - // choose one by random - int j = myrand() % group_sounds.size(); - chosen_sound_name = group_sounds[j]; - - // find chosen one - std::shared_ptr snd = openSingleSound(chosen_sound_name); - if (snd) - break; - - // it doesn't exist - // remove it from the group and try again - group_sounds[j] = std::move(group_sounds.back()); - group_sounds.pop_back(); - } - - return chosen_sound_name; -} - -std::string OpenALSoundManager::getOrLoadLoadedSoundNameFromGroup(const std::string &group_name) -{ - std::string sound_name = getLoadedSoundNameFromGroup(group_name); - if (!sound_name.empty()) - return sound_name; - - // load - std::vector paths = m_fallback_path_provider - ->getLocalFallbackPathsForSoundname(group_name); - for (const std::string &path : paths) { - if (loadSoundFile(path, path)) - addSoundToGroup(path, group_name); - } - return getLoadedSoundNameFromGroup(group_name); -} - -std::shared_ptr OpenALSoundManager::createPlayingSound( - const std::string &sound_name, bool loop, f32 volume, f32 pitch, - f32 start_time, const std::optional> &pos_vel_opt) -{ - infostream << "OpenALSoundManager: Creating playing sound \"" << sound_name - << "\"" << std::endl; - warn_if_al_error("before createPlayingSound"); - - std::shared_ptr lsnd = openSingleSound(sound_name); - if (!lsnd) { - // does not happen because of the call to getLoadedSoundNameFromGroup - errorstream << "OpenALSoundManager::createPlayingSound: Sound \"" - << sound_name << "\" disappeared." << std::endl; - return nullptr; - } - - if (lsnd->m_decode_info.is_stereo && pos_vel_opt.has_value()) { - warningstream << "OpenALSoundManager::createPlayingSound: " - << "Creating positional stereo sound \"" << sound_name << "\"." - << std::endl; - } - - ALuint source_id; - alGenSources(1, &source_id); - if (warn_if_al_error("createPlayingSound (alGenSources)") != AL_NO_ERROR) { - // happens ie. if there are too many sources (out of memory) - return nullptr; - } - - auto sound = std::make_shared(source_id, std::move(lsnd), loop, - volume, pitch, start_time, pos_vel_opt); - - sound->play(); - if (m_is_paused) - sound->pause(); - warn_if_al_error("createPlayingSound"); - return sound; -} - -void OpenALSoundManager::playSoundGeneric(sound_handle_t id, const std::string &group_name, - bool loop, f32 volume, f32 fade, f32 pitch, bool use_local_fallback, - f32 start_time, const std::optional> &pos_vel_opt) -{ - if (id == 0) - id = allocateId(1); - - if (group_name.empty()) { - reportRemovedSound(id); - return; - } - - // choose random sound name from group name - std::string sound_name = use_local_fallback ? - getOrLoadLoadedSoundNameFromGroup(group_name) : - getLoadedSoundNameFromGroup(group_name); - if (sound_name.empty()) { - infostream << "OpenALSoundManager: \"" << group_name << "\" not found." - << std::endl; - reportRemovedSound(id); - return; - } - - volume = std::max(0.0f, volume); - f32 target_fade_volume = volume; - if (fade > 0.0f) - volume = 0.0f; - - if (!(pitch > 0.0f)) { - warningstream << "OpenALSoundManager::playSoundGeneric: Illegal pitch value: " - << start_time << std::endl; - pitch = 1.0f; - } - - if (!std::isfinite(start_time)) { - warningstream << "OpenALSoundManager::playSoundGeneric: Illegal start_time value: " - << start_time << std::endl; - start_time = 0.0f; - } - - // play it - std::shared_ptr sound = createPlayingSound(sound_name, loop, - volume, pitch, start_time, pos_vel_opt); - if (!sound) { - reportRemovedSound(id); - return; - } - - // add to streaming sounds if streaming - if (sound->isStreaming()) - m_sounds_streaming_next_bigstep.push_back(sound); - - m_sounds_playing.emplace(id, std::move(sound)); - - if (fade > 0.0f) - fadeSound(id, fade, target_fade_volume); -} - -int OpenALSoundManager::removeDeadSounds() -{ - int num_deleted_sounds = 0; - - for (auto it = m_sounds_playing.begin(); it != m_sounds_playing.end();) { - sound_handle_t id = it->first; - PlayingSound &sound = *it->second; - // If dead, remove it - if (sound.isDead()) { - it = m_sounds_playing.erase(it); - reportRemovedSound(id); - ++num_deleted_sounds; - } else { - ++it; - } - } - - return num_deleted_sounds; -} - -OpenALSoundManager::OpenALSoundManager(SoundManagerSingleton *smg, - std::unique_ptr fallback_path_provider) : - m_fallback_path_provider(std::move(fallback_path_provider)), - m_device(smg->m_device.get()), - m_context(smg->m_context.get()) -{ - SANITY_CHECK(!!m_fallback_path_provider); - - infostream << "Audio: Initialized: OpenAL " << std::endl; -} - -OpenALSoundManager::~OpenALSoundManager() -{ - infostream << "Audio: Deinitializing..." << std::endl; -} - -/* Interface */ - -void OpenALSoundManager::step(f32 dtime) -{ - m_time_until_dead_removal -= dtime; - if (m_time_until_dead_removal <= 0.0f) { - if (!m_sounds_playing.empty()) { - verbosestream << "OpenALSoundManager::step(): " - << m_sounds_playing.size() << " playing sounds, " - << m_sound_datas_unopen.size() << " unopen sounds, " - << m_sound_datas_open.size() << " open sounds and " - << m_sound_groups.size() << " sound groups loaded." - << std::endl; - } - - int num_deleted_sounds = removeDeadSounds(); - - if (num_deleted_sounds != 0) - verbosestream << "OpenALSoundManager::step(): Deleted " - << num_deleted_sounds << " dead playing sounds." << std::endl; - - m_time_until_dead_removal = REMOVE_DEAD_SOUNDS_INTERVAL; - } - - doFades(dtime); - stepStreams(dtime); -} - -void OpenALSoundManager::pauseAll() -{ - for (auto &snd_p : m_sounds_playing) { - PlayingSound &snd = *snd_p.second; - snd.pause(); - } - m_is_paused = true; -} - -void OpenALSoundManager::resumeAll() -{ - for (auto &snd_p : m_sounds_playing) { - PlayingSound &snd = *snd_p.second; - snd.resume(); - } - m_is_paused = false; -} - -void OpenALSoundManager::updateListener(const v3f &pos_, const v3f &vel_, - const v3f &at_, const v3f &up_) -{ - v3f pos = swap_handedness(pos_); - v3f vel = swap_handedness(vel_); - v3f at = swap_handedness(at_); - v3f up = swap_handedness(up_); - ALfloat orientation[6] = {at.X, at.Y, at.Z, up.X, up.Y, up.Z}; - - alListener3f(AL_POSITION, pos.X, pos.Y, pos.Z); - alListener3f(AL_VELOCITY, vel.X, vel.Y, vel.Z); - alListenerfv(AL_ORIENTATION, orientation); - warn_if_al_error("updateListener"); -} - -void OpenALSoundManager::setListenerGain(f32 gain) -{ - alListenerf(AL_GAIN, gain); -} - -bool OpenALSoundManager::loadSoundFile(const std::string &name, const std::string &filepath) -{ - // do not add twice - if (m_sound_datas_open.count(name) != 0 || m_sound_datas_unopen.count(name) != 0) - return false; - - // coarse check - if (!fs::IsFile(filepath)) - return false; - - // remember for lazy loading - m_sound_datas_unopen.emplace(name, std::make_unique(filepath)); - return true; -} - -bool OpenALSoundManager::loadSoundData(const std::string &name, std::string &&filedata) -{ - // do not add twice - if (m_sound_datas_open.count(name) != 0 || m_sound_datas_unopen.count(name) != 0) - return false; - - // remember for lazy loading - m_sound_datas_unopen.emplace(name, std::make_unique(std::move(filedata))); - return true; -} - -void OpenALSoundManager::addSoundToGroup(const std::string &sound_name, const std::string &group_name) -{ - auto it_groups = m_sound_groups.find(group_name); - if (it_groups != m_sound_groups.end()) - it_groups->second.push_back(sound_name); - else - m_sound_groups.emplace(group_name, std::vector{sound_name}); -} - -void OpenALSoundManager::playSound(sound_handle_t id, const SoundSpec &spec) -{ - return playSoundGeneric(id, spec.name, spec.loop, spec.gain, spec.fade, spec.pitch, - spec.use_local_fallback, spec.start_time, std::nullopt); -} - -void OpenALSoundManager::playSoundAt(sound_handle_t id, const SoundSpec &spec, - const v3f &pos_, const v3f &vel_) -{ - std::optional> pos_vel_opt({ - swap_handedness(pos_), - swap_handedness(vel_) - }); - - return playSoundGeneric(id, spec.name, spec.loop, spec.gain, spec.fade, spec.pitch, - spec.use_local_fallback, spec.start_time, pos_vel_opt); -} - -void OpenALSoundManager::stopSound(sound_handle_t sound) -{ - m_sounds_playing.erase(sound); - reportRemovedSound(sound); -} - -void OpenALSoundManager::fadeSound(sound_handle_t soundid, f32 step, f32 target_gain) -{ - // Ignore the command if step isn't valid. - if (step == 0.0f) - return; - auto sound_it = m_sounds_playing.find(soundid); - if (sound_it == m_sounds_playing.end()) - return; // No sound to fade - PlayingSound &sound = *sound_it->second; - if (sound.fade(step, target_gain)) - m_sounds_fading.emplace_back(sound_it->second); -} - -void OpenALSoundManager::updateSoundPosVel(sound_handle_t id, const v3f &pos_, - const v3f &vel_) -{ - v3f pos = swap_handedness(pos_); - v3f vel = swap_handedness(vel_); - - auto i = m_sounds_playing.find(id); - if (i == m_sounds_playing.end()) - return; - i->second->updatePosVel(pos, vel); -} diff --git a/src/client/sound_openal_internal.h b/src/client/sound_openal_internal.h deleted file mode 100644 index 7fcb73a34e7a3..0000000000000 --- a/src/client/sound_openal_internal.h +++ /dev/null @@ -1,613 +0,0 @@ -/* -Minetest -Copyright (C) 2022 DS -Copyright (C) 2013 celeron55, Perttu Ahola -OpenAL support based on work by: -Copyright (C) 2011 Sebastian 'Bahamada' Rühl -Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits -Copyright (C) 2011 Giuseppe Bilotta - -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; ifnot, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -#pragma once - -#include "log.h" -#include "porting.h" -#include "sound_openal.h" -#include "util/basic_macros.h" - -#if defined(_WIN32) - #include - #include - //#include -#elif defined(__APPLE__) - #define OPENAL_DEPRECATED - #include - #include - //#include -#else - #include - #include - #include -#endif -#include - -#include -#include -#include -#include - - -/* - * - * The coordinate space for sounds (sound-space): - * ---------------------------------------------- - * - * * The functions from ISoundManager (see sound.h) take spatial vectors in node-space. - * * All other `v3f`s here are, if not told otherwise, in sound-space, which is - * defined as node-space mirrored along the x-axis. - * (This is needed because OpenAL uses a right-handed coordinate system.) - * * Use `swap_handedness()` to convert between those two coordinate spaces. - * - * - * How sounds are loaded: - * ---------------------- - * - * * Step 1: - * `loadSoundFile` or `loadSoundFile` is called. This adds an unopen sound with - * the given name to `m_sound_datas_unopen`. - * Unopen / lazy sounds (`ISoundDataUnopen`) are ogg-vorbis files that we did not yet - * start to decode. (Decoding an unopen sound does not fail under normal circumstances - * (because we check whether the file exists at least), if it does fail anyways, - * we should notify the user.) - * * Step 2: - * `addSoundToGroup` is called, to add the name from step 1 to a group. If the - * group does not yet exist, a new one is created. A group can later be played. - * (The mapping is stored in `m_sound_groups`.) - * * Step 3: - * `playSound` or `playSoundAt` is called. - * * Step 3.1: - * If the group with the name `spec.name` does not exist, and `spec.use_local_fallback` - * is true, a new group is created using the user's sound-pack. - * * Step 3.2: - * We choose one random sound name from the given group. - * * Step 3.3: - * We open the sound (see `openSingleSound`). - * If the sound is already open (in `m_sound_datas_open`), we take that one. - * Otherwise we open it by calling `ISoundDataUnopen::open`. We choose (by - * sound length), whether it's a single-buffer (`SoundDataOpenBuffer`) or - * streamed (`SoundDataOpenStream`) sound. - * Single-buffer sounds are always completely loaded. Streamed sounds can be - * partially loaded. - * The sound is erased from `m_sound_datas_unopen` and added to `m_sound_datas_open`. - * Open sounds are kept forever. - * * Step 3.4: - * We create the new `PlayingSound`. It has a `shared_ptr` to its open sound. - * If the open sound is streaming, the playing sound needs to be stepped using - * `PlayingSound::stepStream` for enqueuing buffers. For this purpose, the sound - * is added to `m_sounds_streaming` (as `weak_ptr`). - * If the sound is fading, it is added to `m_sounds_fading` for regular fade-stepping. - * The sound is also added to `m_sounds_playing`, so that one can access it - * via its sound handle. - * * Step 4: - * Streaming sounds are updated. For details see [Streaming of sounds]. - * * Step 5: - * At deinitialization, we can just let the destructors do their work. - * Sound sources are deleted (and with this also stopped) by ~PlayingSound. - * Buffers can't be deleted while sound sources using them exist, because - * PlayingSound has a shared_ptr to its ISoundData. - * - * - * Streaming of sounds: - * -------------------- - * - * In each "bigstep", all streamed sounds are stepStream()ed. This means a - * sound can be stepped at any point in time in the bigstep's interval. - * - * In the worst case, a sound is stepped at the start of one bigstep and in the - * end of the next bigstep. So between two stepStream()-calls lie at most - * 2 * STREAM_BIGSTEP_TIME seconds. - * As there are always 2 sound buffers enqueued, at least one untouched full buffer - * is still available after the first stepStream(). - * If we take a MIN_STREAM_BUFFER_LENGTH > 2 * STREAM_BIGSTEP_TIME, we can hence - * not run into an empty queue. - * - * The MIN_STREAM_BUFFER_LENGTH needs to be a little bigger because of dtime jitter, - * other sounds that may have taken long to stepStream(), and sounds being played - * faster due to Doppler effect. - * - */ - -// constants - -// in seconds -constexpr f32 REMOVE_DEAD_SOUNDS_INTERVAL = 2.0f; -// maximum length in seconds that a sound can have without being streamed -constexpr f32 SOUND_DURATION_MAX_SINGLE = 3.0f; -// minimum time in seconds of a single buffer in a streamed sound -constexpr f32 MIN_STREAM_BUFFER_LENGTH = 1.0f; -// duration in seconds of one bigstep -constexpr f32 STREAM_BIGSTEP_TIME = 0.3f; - -static_assert(MIN_STREAM_BUFFER_LENGTH > STREAM_BIGSTEP_TIME * 2.0f, - "See [Streaming of sounds]."); -static_assert(SOUND_DURATION_MAX_SINGLE >= MIN_STREAM_BUFFER_LENGTH * 2.0f, - "There's no benefit in streaming if we can't queue more than 2 buffers."); - - -/** - * RAII wrapper for openal sound buffers. - */ -struct RAIIALSoundBuffer final -{ - RAIIALSoundBuffer() noexcept = default; - explicit RAIIALSoundBuffer(ALuint buffer) noexcept : m_buffer(buffer) {}; - - ~RAIIALSoundBuffer() noexcept { reset(0); } - - DISABLE_CLASS_COPY(RAIIALSoundBuffer) - - RAIIALSoundBuffer(RAIIALSoundBuffer &&other) noexcept : m_buffer(other.release()) {} - RAIIALSoundBuffer &operator=(RAIIALSoundBuffer &&other) noexcept; - - ALuint get() noexcept { return m_buffer; } - - ALuint release() noexcept { return std::exchange(m_buffer, 0); } - - void reset(ALuint buf) noexcept; - - static RAIIALSoundBuffer generate() noexcept; - -private: - // According to openal specification: - // > Deleting buffer name 0 is a legal NOP. - // - // and: - // > [...] the NULL buffer (i.e., 0) which can always be queued. - ALuint m_buffer = 0; -}; - -/** - * For vorbisfile to read from our buffer instead of from a file. - */ -struct OggVorbisBufferSource { - std::string buf; - size_t cur_offset = 0; - - static size_t read_func(void *ptr, size_t size, size_t nmemb, void *datasource) noexcept; - static int seek_func(void *datasource, ogg_int64_t offset, int whence) noexcept; - static int close_func(void *datasource) noexcept; - static long tell_func(void *datasource) noexcept; - - static const ov_callbacks s_ov_callbacks; -}; - -/** - * Metadata of an Ogg-Vorbis file, used for decoding. - * We query this information once and store it in this struct. - */ -struct OggFileDecodeInfo { - std::string name_for_logging; - bool is_stereo; - ALenum format; // AL_FORMAT_MONO16 or AL_FORMAT_STEREO16 - size_t bytes_per_sample; - ALsizei freq; - ALuint length_samples = 0; - f32 length_seconds = 0.0f; -}; - -/** - * RAII wrapper for OggVorbis_File. - */ -struct RAIIOggFile { - bool m_needs_clear = false; - OggVorbis_File m_file; - - RAIIOggFile() = default; - - DISABLE_CLASS_COPY(RAIIOggFile) - - ~RAIIOggFile() noexcept - { - if (m_needs_clear) - ov_clear(&m_file); - } - - OggVorbis_File *get() { return &m_file; } - - std::optional getDecodeInfo(const std::string &filename_for_logging); - - /** - * Main function for loading ogg vorbis sounds. - * Loads exactly the specified interval of PCM-data, and creates an OpenAL - * buffer with it. - * - * @param decode_info Cached meta information of the file. - * @param pcm_start First sample in the interval. - * @param pcm_end One after last sample of the interval (=> exclusive). - * @return An AL sound buffer, or a 0-buffer on failure. - */ - RAIIALSoundBuffer loadBuffer(const OggFileDecodeInfo &decode_info, ALuint pcm_start, - ALuint pcm_end); -}; - - -/** - * Class for the openal device and context - */ -class SoundManagerSingleton -{ -public: - struct AlcDeviceDeleter { - void operator()(ALCdevice *p) - { - alcCloseDevice(p); - } - }; - - struct AlcContextDeleter { - void operator()(ALCcontext *p) - { - alcMakeContextCurrent(nullptr); - alcDestroyContext(p); - } - }; - - using unique_ptr_alcdevice = std::unique_ptr; - using unique_ptr_alccontext = std::unique_ptr; - - unique_ptr_alcdevice m_device; - unique_ptr_alccontext m_context; - -public: - bool init(); - - ~SoundManagerSingleton(); -}; - - -/** - * Stores sound pcm data buffers. - */ -struct ISoundDataOpen -{ - OggFileDecodeInfo m_decode_info; - - explicit ISoundDataOpen(const OggFileDecodeInfo &decode_info) : - m_decode_info(decode_info) {} - - virtual ~ISoundDataOpen() = default; - - /** - * Iff the data is streaming, there is more than one buffer. - * @return Whether it's streaming data. - */ - virtual bool isStreaming() const noexcept = 0; - - /** - * Load a buffer containing data starting at the given offset. Or just get it - * if it was already loaded. - * - * This function returns multiple values: - * * `buffer`: The OpenAL buffer. - * * `buffer_end`: The offset (in the file) where `buffer` ends (exclusive). - * * `offset_in_buffer`: Offset relative to `buffer`'s start where the requested - * `offset` is. - * `offset_in_buffer == 0` is guaranteed if some loaded buffer ends at - * `offset`. - * - * @param offset The start of the buffer. - * @return `{buffer, buffer_end, offset_in_buffer}` or `{0, sound_data_end, 0}` - * if `offset` is invalid. - */ - virtual std::tuple getOrLoadBufferAt(ALuint offset) = 0; - - static std::shared_ptr fromOggFile(std::unique_ptr oggfile, - const std::string &filename_for_logging); -}; - -/** - * Will be opened lazily when first used. - */ -struct ISoundDataUnopen -{ - virtual ~ISoundDataUnopen() = default; - - // Note: The ISoundDataUnopen is moved (see &&). It is not meant to be kept - // after opening. - virtual std::shared_ptr open(const std::string &sound_name) && = 0; -}; - -/** - * Sound file is in a memory buffer. - */ -struct SoundDataUnopenBuffer final : ISoundDataUnopen -{ - std::string m_buffer; - - explicit SoundDataUnopenBuffer(std::string &&buffer) : m_buffer(std::move(buffer)) {} - - std::shared_ptr open(const std::string &sound_name) && override; -}; - -/** - * Sound file is in file system. - */ -struct SoundDataUnopenFile final : ISoundDataUnopen -{ - std::string m_path; - - explicit SoundDataUnopenFile(const std::string &path) : m_path(path) {} - - std::shared_ptr open(const std::string &sound_name) && override; -}; - -/** - * Non-streaming opened sound data. - * All data is completely loaded in one buffer. - */ -struct SoundDataOpenBuffer final : ISoundDataOpen -{ - RAIIALSoundBuffer m_buffer; - - SoundDataOpenBuffer(std::unique_ptr oggfile, - const OggFileDecodeInfo &decode_info); - - bool isStreaming() const noexcept override { return false; } - - std::tuple getOrLoadBufferAt(ALuint offset) override - { - if (offset >= m_decode_info.length_samples) - return {0, m_decode_info.length_samples, 0}; - return {m_buffer.get(), m_decode_info.length_samples, offset}; - } -}; - -/** - * Streaming opened sound data. - * - * Uses a sorted list of contiguous sound data regions (`ContiguousBuffers`s) for - * efficient seeking. - */ -struct SoundDataOpenStream final : ISoundDataOpen -{ - /** - * An OpenAL buffer that goes until `m_end` (exclusive). - */ - struct SoundBufferUntil final - { - ALuint m_end; - RAIIALSoundBuffer m_buffer; - }; - - /** - * A sorted non-empty vector of contiguous buffers. - * The start (inclusive) of each buffer is the end of its predecessor, or - * `m_start` for the first buffer. - */ - struct ContiguousBuffers final - { - ALuint m_start; - std::vector m_buffers; - }; - - std::unique_ptr m_oggfile; - // A sorted vector of non-overlapping, non-contiguous `ContiguousBuffers`s. - std::vector m_bufferss; - - SoundDataOpenStream(std::unique_ptr oggfile, - const OggFileDecodeInfo &decode_info); - - bool isStreaming() const noexcept override { return true; } - - std::tuple getOrLoadBufferAt(ALuint offset) override; - -private: - // offset must be before after_it's m_start and after (after_it-1)'s last m_end - // new buffer will be inserted into m_bufferss before after_it - // returns same as getOrLoadBufferAt - std::tuple loadBufferAt(ALuint offset, - std::vector::iterator after_it); -}; - - -/** - * A sound that is currently played. - * Can be streaming. - * Can be fading. - */ -class PlayingSound final -{ - struct FadeState { - f32 step; - f32 target_gain; - }; - - ALuint m_source_id; - std::shared_ptr m_data; - ALuint m_next_sample_pos = 0; - bool m_looping; - bool m_is_positional; - bool m_stopped_means_dead = true; - std::optional m_fade_state = std::nullopt; - -public: - PlayingSound(ALuint source_id, std::shared_ptr data, bool loop, - f32 volume, f32 pitch, f32 start_time, - const std::optional> &pos_vel_opt); - - ~PlayingSound() noexcept - { - alDeleteSources(1, &m_source_id); - } - - DISABLE_CLASS_COPY(PlayingSound) - - // return false means streaming finished - bool stepStream(); - - // retruns true if it wasn't fading already - bool fade(f32 step, f32 target_gain) noexcept; - - // returns true if more fade is needed later - bool doFade(f32 dtime) noexcept; - - void updatePosVel(const v3f &pos, const v3f &vel) noexcept; - - void setGain(f32 gain) noexcept; - - f32 getGain() noexcept; - - void setPitch(f32 pitch) noexcept { alSourcef(m_source_id, AL_PITCH, pitch); } - - bool isStreaming() const noexcept { return m_data->isStreaming(); } - - void play() noexcept { alSourcePlay(m_source_id); } - - // returns one of AL_INITIAL, AL_PLAYING, AL_PAUSED, AL_STOPPED - ALint getState() noexcept - { - ALint state; - alGetSourcei(m_source_id, AL_SOURCE_STATE, &state); - return state; - } - - bool isDead() noexcept - { - // streaming sounds can (but should not) stop because the queue runs empty - return m_stopped_means_dead && getState() == AL_STOPPED; - } - - void pause() noexcept - { - // this is a NOP if state != AL_PLAYING - alSourcePause(m_source_id); - } - - void resume() noexcept - { - if (getState() == AL_PAUSED) - play(); - } -}; - - -/* - * The public ISoundManager interface - */ - -class OpenALSoundManager final : public ISoundManager -{ -private: - std::unique_ptr m_fallback_path_provider; - - ALCdevice *m_device; - ALCcontext *m_context; - - // time in seconds until which removeDeadSounds will be called again - f32 m_time_until_dead_removal = REMOVE_DEAD_SOUNDS_INTERVAL; - - // loaded sounds - std::unordered_map> m_sound_datas_unopen; - std::unordered_map> m_sound_datas_open; - // sound groups - std::unordered_map> m_sound_groups; - - // currently playing sounds - std::unordered_map> m_sounds_playing; - - // streamed sounds - std::vector> m_sounds_streaming_current_bigstep; - std::vector> m_sounds_streaming_next_bigstep; - // time left until current bigstep finishes - f32 m_stream_timer = STREAM_BIGSTEP_TIME; - - std::vector> m_sounds_fading; - - // if true, all sounds will be directly paused after creation - bool m_is_paused = false; - -private: - void stepStreams(f32 dtime); - void doFades(f32 dtime); - - /** - * Gives the open sound for a loaded sound. - * Opens the sound if currently unopened. - * - * @param sound_name Name of the sound. - * @return The open sound. - */ - std::shared_ptr openSingleSound(const std::string &sound_name); - - /** - * Gets a random sound name from a group. - * - * @param group_name The name of the sound group. - * @return The name of a sound in the group, or "" on failure. Getting the - * sound with `openSingleSound` directly afterwards will not fail. - */ - std::string getLoadedSoundNameFromGroup(const std::string &group_name); - - /** - * Same as `getLoadedSoundNameFromGroup`, but if sound does not exist, try to - * load from local files. - */ - std::string getOrLoadLoadedSoundNameFromGroup(const std::string &group_name); - - std::shared_ptr createPlayingSound(const std::string &sound_name, - bool loop, f32 volume, f32 pitch, f32 start_time, - const std::optional> &pos_vel_opt); - - void playSoundGeneric(sound_handle_t id, const std::string &group_name, bool loop, - f32 volume, f32 fade, f32 pitch, bool use_local_fallback, f32 start_time, - const std::optional> &pos_vel_opt); - - /** - * Deletes sounds that are dead (=finished). - * - * @return Number of removed sounds. - */ - int removeDeadSounds(); - -public: - OpenALSoundManager(SoundManagerSingleton *smg, - std::unique_ptr fallback_path_provider); - - ~OpenALSoundManager() override; - - DISABLE_CLASS_COPY(OpenALSoundManager) - - /* Interface */ - - void step(f32 dtime) override; - void pauseAll() override; - void resumeAll() override; - - void updateListener(const v3f &pos_, const v3f &vel_, const v3f &at_, const v3f &up_) override; - void setListenerGain(f32 gain) override; - - bool loadSoundFile(const std::string &name, const std::string &filepath) override; - bool loadSoundData(const std::string &name, std::string &&filedata) override; - void addSoundToGroup(const std::string &sound_name, const std::string &group_name) override; - - void playSound(sound_handle_t id, const SoundSpec &spec) override; - void playSoundAt(sound_handle_t id, const SoundSpec &spec, const v3f &pos_, - const v3f &vel_) override; - void stopSound(sound_handle_t sound) override; - void fadeSound(sound_handle_t soundid, f32 step, f32 target_gain) override; - void updateSoundPosVel(sound_handle_t sound, const v3f &pos_, const v3f &vel_) override; -}; diff --git a/src/client/tile.cpp b/src/client/tile.cpp index 4e6e68b46aa4a..867c28dd3fd92 100644 --- a/src/client/tile.cpp +++ b/src/client/tile.cpp @@ -433,10 +433,8 @@ class TextureSource : public IWritableTextureSource // Maps image file names to loaded palettes. std::unordered_map m_palettes; - // Cached settings needed for making textures from meshes - bool m_setting_mipmap; - bool m_setting_trilinear_filter; - bool m_setting_bilinear_filter; + // Cached settings needed for making textures for meshes + bool m_mesh_texture_prefilter; }; IWritableTextureSource *createTextureSource() @@ -455,9 +453,9 @@ TextureSource::TextureSource() // Cache some settings // Note: Since this is only done once, the game must be restarted // for these settings to take effect - m_setting_mipmap = g_settings->getBool("mip_map"); - m_setting_trilinear_filter = g_settings->getBool("trilinear_filter"); - m_setting_bilinear_filter = g_settings->getBool("bilinear_filter"); + m_mesh_texture_prefilter = + g_settings->getBool("mip_map") || g_settings->getBool("bilinear_filter") || + g_settings->getBool("trilinear_filter") || g_settings->getBool("anisotropic_filter"); } TextureSource::~TextureSource() @@ -701,12 +699,8 @@ video::ITexture* TextureSource::getTexture(const std::string &name, u32 *id) video::ITexture* TextureSource::getTextureForMesh(const std::string &name, u32 *id) { - static thread_local bool filter_needed = - g_settings->getBool("texture_clean_transparent") || m_setting_mipmap || - ((m_setting_trilinear_filter || m_setting_bilinear_filter) && - g_settings->getS32("texture_min_size") > 1); // Avoid duplicating texture if it won't actually change - if (filter_needed) + if (m_mesh_texture_prefilter) return getTexture(name + "^[applyfiltersformesh", id); return getTexture(name, id); } @@ -1741,46 +1735,8 @@ bool TextureSource::generateImagePart(std::string part_of_name, } // Apply the "clean transparent" filter, if needed - if (m_setting_mipmap || g_settings->getBool("texture_clean_transparent")) + if (m_mesh_texture_prefilter) imageCleanTransparent(baseimg, 127); - - /* Upscale textures to user's requested minimum size. This is a trick to make - * filters look as good on low-res textures as on high-res ones, by making - * low-res textures BECOME high-res ones. This is helpful for worlds that - * mix high- and low-res textures, or for mods with least-common-denominator - * textures that don't have the resources to offer high-res alternatives. - */ - const bool filter = m_setting_trilinear_filter || m_setting_bilinear_filter; - const s32 scaleto = filter ? g_settings->getU16("texture_min_size") : 1; - if (scaleto > 1) { - const core::dimension2d dim = baseimg->getDimension(); - - /* Calculate scaling needed to make the shortest texture dimension - * equal to the target minimum. If e.g. this is a vertical frames - * animation, the short dimension will be the real size. - */ - if ((dim.Width == 0) || (dim.Height == 0)) { - errorstream << "generateImagePart(): Illegal 0 dimension " - << "for part_of_name=\""<< part_of_name - << "\", cancelling." << std::endl; - return false; - } - u32 xscale = scaleto / dim.Width; - u32 yscale = scaleto / dim.Height; - u32 scale = (xscale > yscale) ? xscale : yscale; - - // Never downscale; only scale up by 2x or more. - if (scale > 1) { - u32 w = scale * dim.Width; - u32 h = scale * dim.Height; - const core::dimension2d newdim = core::dimension2d(w, h); - video::IImage *newimg = driver->createImage( - baseimg->getColorFormat(), newdim); - baseimg->copyToScaling(newimg); - baseimg->drop(); - baseimg = newimg; - } - } } /* [resize:WxH diff --git a/src/client/wieldmesh.cpp b/src/client/wieldmesh.cpp index 10a8a56651c6a..d6d2468f58da3 100644 --- a/src/client/wieldmesh.cpp +++ b/src/client/wieldmesh.cpp @@ -297,11 +297,8 @@ void WieldMeshSceneNode::setExtruded(const std::string &imagename, material.MaterialType = m_material_type; material.MaterialTypeParam = 0.5f; material.BackfaceCulling = true; - // Enable bi/trilinear filtering only for high resolution textures - bool bilinear_filter = dim.Width > 32 && m_bilinear_filter; - bool trilinear_filter = dim.Width > 32 && m_trilinear_filter; material.forEachTexture([=] (auto &tex) { - tex.setFiltersMinetest(bilinear_filter, trilinear_filter, + setMaterialFilters(tex, m_bilinear_filter, m_trilinear_filter, m_anisotropic_filter); }); // mipmaps cause "thin black line" artifacts @@ -446,7 +443,8 @@ void WieldMeshSceneNode::setItem(const ItemStack &item, Client *client, bool che default: { // Render non-trivial drawtypes like the actual node MapNode n(id); - n.setParam2(def.place_param2); + if (def.place_param2) + n.setParam2(*def.place_param2); mesh = createSpecialNodeMesh(client, n, &m_colors, f); changeToMesh(mesh); @@ -465,7 +463,7 @@ void WieldMeshSceneNode::setItem(const ItemStack &item, Client *client, bool che material.MaterialTypeParam = 0.5f; material.BackfaceCulling = cull_backface; material.forEachTexture([this] (auto &tex) { - tex.setFiltersMinetest(m_bilinear_filter, m_trilinear_filter, + setMaterialFilters(tex, m_bilinear_filter, m_trilinear_filter, m_anisotropic_filter); }); } @@ -641,7 +639,8 @@ void getItemMesh(Client *client, const ItemStack &item, ItemMesh *result) default: { // Render non-trivial drawtypes like the actual node MapNode n(id); - n.setParam2(def.place_param2); + if (def.place_param2) + n.setParam2(*def.place_param2); mesh = createSpecialNodeMesh(client, n, &result->buffer_colors, f); scaleMesh(mesh, v3f(0.12, 0.12, 0.12)); diff --git a/src/clientiface.cpp b/src/clientiface.cpp index 45df9684af246..e609c98f69d81 100644 --- a/src/clientiface.cpp +++ b/src/clientiface.cpp @@ -412,7 +412,9 @@ void RemoteClient::GetNextBlocks ( if (d > full_d_max) { new_nearest_unsent_d = 0; m_nothing_to_send_pause_timer = 2.0f; - infostream << "Server: Player " << m_name << ", RemoteClient " << peer_id << ": full map send completed after " << m_map_send_completion_timer << "s, restarting" << std::endl; + infostream << "Server: Player " << m_name << ", peer_id=" << peer_id + << ": full map send completed after " << m_map_send_completion_timer + << "s, restarting" << std::endl; m_map_send_completion_timer = 0.0f; } else { if (nearest_sent_d != -1) diff --git a/src/debug.cpp b/src/debug.cpp index 3c82ed9e1c23e..04d59a6d7af73 100644 --- a/src/debug.cpp +++ b/src/debug.cpp @@ -32,6 +32,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #ifdef _MSC_VER #include + #include + #include #include "version.h" #include "filesys.h" #endif @@ -74,6 +76,13 @@ void fatal_error_fn(const char *msg, const char *file, abort(); } +std::string debug_describe_exc(const std::exception &e) +{ + if (dynamic_cast(&e)) + return "C++ out of memory"; + return std::string("\"").append(e.what()).append("\""); +} + #ifdef _MSC_VER const char *Win32ExceptionCodeToString(DWORD exception_code) diff --git a/src/debug.h b/src/debug.h index fcef2091cac44..aeea81d47268c 100644 --- a/src/debug.h +++ b/src/debug.h @@ -25,11 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "gettime.h" #include "log.h" -#ifdef _WIN32 - #include - #ifdef _MSC_VER - #include - #endif +#ifdef _MSC_VER #define FUNCTION_NAME __FUNCTION__ #else #define FUNCTION_NAME __PRETTY_FUNCTION__ @@ -75,6 +71,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #define sanity_check(expr) SANITY_CHECK(expr) +std::string debug_describe_exc(const std::exception &e); void debug_set_exception_handler(); @@ -86,9 +83,10 @@ void debug_set_exception_handler(); #define BEGIN_DEBUG_EXCEPTION_HANDLER try { #define END_DEBUG_EXCEPTION_HANDLER \ } catch (std::exception &e) { \ + std::string e_descr = debug_describe_exc(e); \ errorstream << "An unhandled exception occurred: " \ - << e.what() << std::endl; \ - FATAL_ERROR(e.what()); \ + << e_descr << std::endl; \ + FATAL_ERROR(e_descr.c_str()); \ } #else // Dummy ones diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp index e237847162c09..587878376d3e4 100644 --- a/src/defaultsettings.cpp +++ b/src/defaultsettings.cpp @@ -172,10 +172,11 @@ void set_default_settings() #else settings->setDefault("show_debug", "true"); #endif - settings->setDefault("fsaa", "0"); + settings->setDefault("fsaa", "2"); settings->setDefault("undersampling", "1"); settings->setDefault("world_aligned_mode", "enable"); settings->setDefault("autoscale_mode", "disable"); + settings->setDefault("texture_min_size", "64"); settings->setDefault("enable_fog", "true"); settings->setDefault("fog_start", "0.4"); settings->setDefault("3d_mode", "none"); @@ -235,8 +236,6 @@ void set_default_settings() settings->setDefault("hud_hotbar_max_width", "1.0"); settings->setDefault("enable_local_map_saving", "false"); settings->setDefault("show_entity_selectionbox", "false"); - settings->setDefault("texture_clean_transparent", "false"); - settings->setDefault("texture_min_size", "64"); settings->setDefault("ambient_occlusion_gamma", "1.8"); settings->setDefault("enable_shaders", "true"); settings->setDefault("enable_particles", "true"); @@ -252,9 +251,9 @@ void set_default_settings() settings->setDefault("directional_colored_fog", "true"); settings->setDefault("inventory_items_animations", "false"); settings->setDefault("mip_map", "false"); - settings->setDefault("anisotropic_filter", "false"); settings->setDefault("bilinear_filter", "false"); settings->setDefault("trilinear_filter", "false"); + settings->setDefault("anisotropic_filter", "false"); settings->setDefault("tone_mapping", "false"); settings->setDefault("enable_waving_water", "false"); settings->setDefault("water_wave_height", "1.0"); @@ -274,7 +273,7 @@ void set_default_settings() // Effects Shadows settings->setDefault("enable_dynamic_shadows", "false"); settings->setDefault("shadow_strength_gamma", "1.0"); - settings->setDefault("shadow_map_max_distance", "200.0"); + settings->setDefault("shadow_map_max_distance", "140.0"); settings->setDefault("shadow_map_texture_size", "2048"); settings->setDefault("shadow_map_texture_32bit", "true"); settings->setDefault("shadow_map_color", "false"); @@ -360,14 +359,13 @@ void set_default_settings() // Network settings->setDefault("enable_ipv6", "true"); settings->setDefault("ipv6_server", "false"); - settings->setDefault("max_packets_per_iteration","1024"); + settings->setDefault("max_packets_per_iteration", "1024"); settings->setDefault("port", "30000"); settings->setDefault("strict_protocol_version_checking", "false"); settings->setDefault("player_transfer_distance", "0"); settings->setDefault("max_simultaneous_block_sends_per_client", "40"); settings->setDefault("time_send_interval", "5"); - settings->setDefault("default_game", "minetest"); settings->setDefault("motd", ""); settings->setDefault("max_users", "15"); settings->setDefault("creative_mode", "false"); @@ -473,7 +471,8 @@ void set_default_settings() #endif #ifdef HAVE_TOUCHSCREENGUI - settings->setDefault("touchscreen_threshold","20"); + settings->setDefault("touchscreen_threshold", "20"); + settings->setDefault("touchscreen_sensitivity", "0.2"); settings->setDefault("touch_use_crosshair", "false"); settings->setDefault("fixed_virtual_joystick", "false"); settings->setDefault("virtual_joystick_triggers_aux1", "false"); @@ -486,23 +485,18 @@ void set_default_settings() settings->setDefault("screen_w", "0"); settings->setDefault("screen_h", "0"); settings->setDefault("fullscreen", "true"); - settings->setDefault("smooth_lighting", "false"); settings->setDefault("performance_tradeoffs", "true"); settings->setDefault("max_simultaneous_block_sends_per_client", "10"); settings->setDefault("emergequeue_limit_diskonly", "16"); settings->setDefault("emergequeue_limit_generate", "16"); settings->setDefault("max_block_generate_distance", "5"); - settings->setDefault("enable_3d_clouds", "false"); - settings->setDefault("fps_max_unfocused", "10"); settings->setDefault("sqlite_synchronous", "1"); - settings->setDefault("map_compression_level_disk", "-1"); - settings->setDefault("map_compression_level_net", "-1"); settings->setDefault("server_map_save_interval", "15"); settings->setDefault("client_mapblock_limit", "1000"); settings->setDefault("active_block_range", "2"); settings->setDefault("viewing_range", "50"); settings->setDefault("leaves_style", "simple"); - settings->setDefault("curl_verify_cert","false"); + settings->setDefault("curl_verify_cert", "false"); // Apply settings according to screen size float x_inches = (float) porting::getDisplaySize().X / diff --git a/src/gui/guiButton.cpp b/src/gui/guiButton.cpp index 136a04a96e61b..d191886997fd2 100644 --- a/src/gui/guiButton.cpp +++ b/src/gui/guiButton.cpp @@ -27,15 +27,10 @@ using namespace gui; //! constructor GUIButton::GUIButton(IGUIEnvironment* environment, IGUIElement* parent, - s32 id, core::rect rectangle, ISimpleTextureSource *tsrc, - bool noclip) -: IGUIButton(environment, parent, id, rectangle), - SpriteBank(0), OverrideFont(0), - OverrideColorEnabled(false), OverrideColor(video::SColor(101,255,255,255)), - ClickTime(0), HoverTime(0), FocusTime(0), - ClickShiftState(false), ClickControlState(false), - IsPushButton(false), Pressed(false), - UseAlphaChannel(false), DrawBorder(true), ScaleImage(false), TSrc(tsrc) + s32 id, core::rect rectangle, ISimpleTextureSource *tsrc, + bool noclip) : + IGUIButton(environment, parent, id, rectangle), + TSrc(tsrc) { setNotClipped(noclip); diff --git a/src/gui/guiButton.h b/src/gui/guiButton.h index 5006e2df98ded..b0fc4b647a665 100644 --- a/src/gui/guiButton.h +++ b/src/gui/guiButton.h @@ -185,11 +185,9 @@ class GUIButton : public gui::IGUIButton struct ButtonImage { - ButtonImage() : Texture(0), SourceRect(core::rect(0,0,0,0)) - { - } + ButtonImage() = default; - ButtonImage(const ButtonImage& other) : Texture(0), SourceRect(core::rect(0,0,0,0)) + ButtonImage(const ButtonImage& other) { *this = other; } @@ -220,8 +218,8 @@ class GUIButton : public gui::IGUIButton } - video::ITexture* Texture; - core::rect SourceRect; + video::ITexture* Texture = nullptr; + core::rect SourceRect = core::rect(0,0,0,0); }; gui::EGUI_BUTTON_IMAGE_STATE getImageState(bool pressed, const ButtonImage* images) const; @@ -230,43 +228,41 @@ class GUIButton : public gui::IGUIButton struct ButtonSprite { - ButtonSprite() : Index(-1), Loop(false), Scale(false) - { - } - - bool operator==(const ButtonSprite& other) const + bool operator==(const ButtonSprite &other) const { return Index == other.Index && Color == other.Color && Loop == other.Loop && Scale == other.Scale; } - s32 Index; + s32 Index = -1; video::SColor Color; - bool Loop; - bool Scale; + bool Loop = false; + bool Scale = false; }; ButtonSprite ButtonSprites[gui::EGBS_COUNT]; - gui::IGUISpriteBank* SpriteBank; + gui::IGUISpriteBank* SpriteBank = nullptr; ButtonImage ButtonImages[gui::EGBIS_COUNT]; std::array Styles; - gui::IGUIFont* OverrideFont; + gui::IGUIFont* OverrideFont = nullptr; - bool OverrideColorEnabled; - video::SColor OverrideColor; + bool OverrideColorEnabled = false; + video::SColor OverrideColor = video::SColor(101,255,255,255); - u32 ClickTime, HoverTime, FocusTime; + u32 ClickTime = 0; + u32 HoverTime = 0; + u32 FocusTime = 0; - bool ClickShiftState; - bool ClickControlState; + bool ClickShiftState = false; + bool ClickControlState = false; - bool IsPushButton; - bool Pressed; - bool UseAlphaChannel; - bool DrawBorder; - bool ScaleImage; + bool IsPushButton = false; + bool Pressed = false; + bool UseAlphaChannel = false; + bool DrawBorder = true; + bool ScaleImage = false; video::SColor Colors[4]; // PATCH @@ -279,6 +275,6 @@ class GUIButton : public gui::IGUIButton core::rect BgMiddle; core::rect Padding; core::vector2d ContentOffset; - video::SColor BgColor; + video::SColor BgColor = video::SColor(0xFF,0xFF,0xFF,0xFF); // END PATCH }; diff --git a/src/gui/guiEditBoxWithScrollbar.cpp b/src/gui/guiEditBoxWithScrollbar.cpp index 91d4172537e15..ed5db785bb815 100644 --- a/src/gui/guiEditBoxWithScrollbar.cpp +++ b/src/gui/guiEditBoxWithScrollbar.cpp @@ -25,9 +25,10 @@ numerical //! constructor GUIEditBoxWithScrollBar::GUIEditBoxWithScrollBar(const wchar_t* text, bool border, IGUIEnvironment* environment, IGUIElement* parent, s32 id, - const core::rect& rectangle, bool writable, bool has_vscrollbar) + const core::rect& rectangle, ISimpleTextureSource *tsrc, + bool writable, bool has_vscrollbar) : GUIEditBox(environment, parent, id, rectangle, border, writable), - m_background(true), m_bg_color_used(false) + m_background(true), m_bg_color_used(false), m_tsrc(tsrc) { #ifdef _DEBUG setDebugName("GUIEditBoxWithScrollBar"); @@ -635,7 +636,7 @@ void GUIEditBoxWithScrollBar::createVScrollBar() irr::core::rect scrollbarrect = m_frame_rect; scrollbarrect.UpperLeftCorner.X += m_frame_rect.getWidth() - m_scrollbar_width; m_vscrollbar = new GUIScrollBar(Environment, getParent(), -1, - scrollbarrect, false, true); + scrollbarrect, false, true, m_tsrc); m_vscrollbar->setVisible(false); m_vscrollbar->setSmallStep(3 * fontHeight); diff --git a/src/gui/guiEditBoxWithScrollbar.h b/src/gui/guiEditBoxWithScrollbar.h index cea482fc26be4..22c9dce6dbc5e 100644 --- a/src/gui/guiEditBoxWithScrollbar.h +++ b/src/gui/guiEditBoxWithScrollbar.h @@ -7,6 +7,8 @@ #include "guiEditBox.h" +class ISimpleTextureSource; + class GUIEditBoxWithScrollBar : public GUIEditBox { public: @@ -14,7 +16,7 @@ class GUIEditBoxWithScrollBar : public GUIEditBox //! constructor GUIEditBoxWithScrollBar(const wchar_t* text, bool border, IGUIEnvironment* environment, IGUIElement* parent, s32 id, const core::rect& rectangle, - bool writable = true, bool has_vscrollbar = true); + ISimpleTextureSource *tsrc, bool writable = true, bool has_vscrollbar = true); //! destructor virtual ~GUIEditBoxWithScrollBar() {} @@ -56,6 +58,8 @@ class GUIEditBoxWithScrollBar : public GUIEditBox bool m_bg_color_used; video::SColor m_bg_color; + + ISimpleTextureSource *m_tsrc; }; diff --git a/src/gui/guiEngine.cpp b/src/gui/guiEngine.cpp index b33974b7fafff..8e75fa77e7a18 100644 --- a/src/gui/guiEngine.cpp +++ b/src/gui/guiEngine.cpp @@ -31,7 +31,6 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "settings.h" #include "guiMainMenu.h" #include "sound.h" -#include "client/sound_openal.h" #include "httpfetch.h" #include "log.h" #include "client/fontengine.h" @@ -39,6 +38,10 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "irrlicht_changes/static_text.h" #include "client/tile.h" +#if USE_SOUND + #include "client/sound/sound_openal.h" +#endif + /******************************************************************************/ void TextDestGuiEngine::gotText(const StringMap &fields) diff --git a/src/gui/guiFormSpecMenu.cpp b/src/gui/guiFormSpecMenu.cpp index cea84fd8018ea..8b6c78838bf46 100644 --- a/src/gui/guiFormSpecMenu.cpp +++ b/src/gui/guiFormSpecMenu.cpp @@ -666,7 +666,7 @@ void GUIFormSpecMenu::parseScrollBar(parserData* data, const std::string &elemen spec.ftype = f_ScrollBar; spec.send = true; GUIScrollBar *e = new GUIScrollBar(Environment, data->current_parent, - spec.fid, rect, is_horizontal, true); + spec.fid, rect, is_horizontal, true, m_tsrc); auto style = getDefaultStyleForElement("scrollbar", name); e->setNotClipped(style.getBool(StyleSpec::NOCLIP, false)); @@ -1493,7 +1493,7 @@ void GUIFormSpecMenu::createTextField(parserData *data, FieldSpec &spec, gui::IGUIEditBox *e = nullptr; if (is_multiline) { e = new GUIEditBoxWithScrollBar(spec.fdefault.c_str(), true, Environment, - data->current_parent, spec.fid, rect, is_editable, true); + data->current_parent, spec.fid, rect, m_tsrc, is_editable, true); } else if (is_editable) { e = Environment->addEditBox(spec.fdefault.c_str(), rect, true, data->current_parent, spec.fid); @@ -1670,8 +1670,6 @@ void GUIFormSpecMenu::parseField(parserData* data, const std::string &element, void GUIFormSpecMenu::parseHyperText(parserData *data, const std::string &element) { - MY_CHECKCLIENT("hypertext"); - std::vector parts; if (!precheckElement("hypertext", element, 4, 4, parts)) return; @@ -4314,10 +4312,7 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event) if (button == BET_MIDDLE) craft_amount = 10; else if (event.MouseInput.Shift && button == BET_LEFT) - // TODO: We should craft everything with shift-left-click, - // but the slow crafting code limits us, so we only craft one - craft_amount = 1; - //craft_amount = list_s->getItem(s.i).getStackMax(m_client->idef()); + craft_amount = list_s->getItem(s.i).getStackMax(m_client->idef()); else craft_amount = 1; @@ -4452,6 +4447,12 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event) if (m_left_drag_stacks.size() > 1) { // Finalize the left-dragging for (auto &ds : m_left_drag_stacks) { + if (ds.first == *m_selected_item) { + // This entry is needed to properly calculate the stack sizes. + // The stack already exists, hence no further action needed here. + continue; + } + // Check how many items we should move to this slot, // it may be less than the full split Inventory *inv_to = m_invmgr->getInventory(ds.first.inventoryloc); diff --git a/src/gui/guiHyperText.cpp b/src/gui/guiHyperText.cpp index c2241c944fbe3..d50a6cc7f13d5 100644 --- a/src/gui/guiHyperText.cpp +++ b/src/gui/guiHyperText.cpp @@ -600,8 +600,7 @@ u32 ParsedText::parseTag(const wchar_t *text, u32 cursor) TextDrawer::TextDrawer(const wchar_t *text, Client *client, gui::IGUIEnvironment *environment, ISimpleTextureSource *tsrc) : - m_text(text), - m_client(client), m_environment(environment) + m_text(text), m_client(client), m_tsrc(tsrc), m_guienv(environment) { // Size all elements for (auto &p : m_text.m_paragraphs) { @@ -632,7 +631,7 @@ TextDrawer::TextDrawer(const wchar_t *text, Client *client, if (e.type == ParsedText::ELEMENT_IMAGE) { video::ITexture *texture = - m_client->getTextureSource()-> + m_tsrc-> getTexture(stringw_to_utf8(e.text)); if (texture) dim = texture->getOriginalSize(); @@ -914,7 +913,7 @@ void TextDrawer::place(const core::rect &dest_rect) void TextDrawer::draw(const core::rect &clip_rect, const core::position2d &dest_offset) { - irr::video::IVideoDriver *driver = m_environment->getVideoDriver(); + irr::video::IVideoDriver *driver = m_guienv->getVideoDriver(); core::position2d offset = dest_offset; offset.Y += m_voffset; @@ -958,10 +957,10 @@ void TextDrawer::draw(const core::rect &clip_rect, case ParsedText::ELEMENT_IMAGE: { video::ITexture *texture = - m_client->getTextureSource()->getTexture( + m_tsrc->getTexture( stringw_to_utf8(el.text)); if (texture != 0) - m_environment->getVideoDriver()->draw2DImage( + m_guienv->getVideoDriver()->draw2DImage( texture, rect, irr::core::rect( core::position2d(0, 0), @@ -970,15 +969,15 @@ void TextDrawer::draw(const core::rect &clip_rect, } break; case ParsedText::ELEMENT_ITEM: { - IItemDefManager *idef = m_client->idef(); - ItemStack item; - item.deSerialize(stringw_to_utf8(el.text), idef); - - drawItemStack( - m_environment->getVideoDriver(), - g_fontengine->getFont(), item, rect, &clip_rect, - m_client, IT_ROT_OTHER, el.angle, el.rotation - ); + if (m_client) { + IItemDefManager *idef = m_client->idef(); + ItemStack item; + item.deSerialize(stringw_to_utf8(el.text), idef); + + drawItemStack(m_guienv->getVideoDriver(), + g_fontengine->getFont(), item, rect, &clip_rect, m_client, + IT_ROT_OTHER, el.angle, el.rotation); + } } break; } } @@ -993,7 +992,7 @@ GUIHyperText::GUIHyperText(const wchar_t *text, IGUIEnvironment *environment, IGUIElement *parent, s32 id, const core::rect &rectangle, Client *client, ISimpleTextureSource *tsrc) : IGUIElement(EGUIET_ELEMENT, environment, parent, id, rectangle), - m_client(client), m_vscrollbar(nullptr), + m_tsrc(tsrc), m_vscrollbar(nullptr), m_drawer(text, client, environment, tsrc), m_text_scrollpos(0, 0) { @@ -1011,7 +1010,7 @@ GUIHyperText::GUIHyperText(const wchar_t *text, IGUIEnvironment *environment, RelativeRect.getWidth() - m_scrollbar_width, 0, RelativeRect.getWidth(), RelativeRect.getHeight()); - m_vscrollbar = new GUIScrollBar(Environment, this, -1, rect, false, true); + m_vscrollbar = new GUIScrollBar(Environment, this, -1, rect, false, true, tsrc); m_vscrollbar->setVisible(false); } diff --git a/src/gui/guiHyperText.h b/src/gui/guiHyperText.h index 04c664df54512..0616a37ce3f86 100644 --- a/src/gui/guiHyperText.h +++ b/src/gui/guiHyperText.h @@ -188,8 +188,9 @@ class TextDrawer }; ParsedText m_text; - Client *m_client; - gui::IGUIEnvironment *m_environment; + Client *m_client; ///< null in the mainmenu + ISimpleTextureSource *m_tsrc; + gui::IGUIEnvironment *m_guienv; s32 m_height; s32 m_voffset; std::vector m_floating; @@ -216,7 +217,7 @@ class GUIHyperText : public gui::IGUIElement protected: // GUI members - Client *m_client; + ISimpleTextureSource *m_tsrc; GUIScrollBar *m_vscrollbar; TextDrawer m_drawer; diff --git a/src/gui/guiInventoryList.h b/src/gui/guiInventoryList.h index 07ca93c3f328f..8ef63c8bc0c86 100644 --- a/src/gui/guiInventoryList.h +++ b/src/gui/guiInventoryList.h @@ -43,7 +43,7 @@ class GUIInventoryList : public gui::IGUIElement { } - bool operator==(const ItemSpec& other) + bool operator==(const ItemSpec& other) const { return inventoryloc == other.inventoryloc && listname == other.listname && i == other.i; diff --git a/src/gui/guiKeyChangeMenu.cpp b/src/gui/guiKeyChangeMenu.cpp index b048c229f675b..3d8c13e5e3d66 100644 --- a/src/gui/guiKeyChangeMenu.cpp +++ b/src/gui/guiKeyChangeMenu.cpp @@ -123,8 +123,8 @@ void GUIKeyChangeMenu::regenerateGui(v2u32 screensize) core::rect rect(0, 0, 600 * s, 40 * s); rect += topleft + v2s32(25 * s, 3 * s); //gui::IGUIStaticText *t = - Environment->addStaticText(wstrgettext("Keybindings.").c_str(), - rect, false, true, this, -1); + gui::StaticText::add(Environment, wstrgettext("Keybindings."), rect, + false, true, this, -1); //t->setTextAlignment(gui::EGUIA_CENTER, gui::EGUIA_UPPERLEFT); } @@ -138,8 +138,8 @@ void GUIKeyChangeMenu::regenerateGui(v2u32 screensize) { core::rect rect(0, 0, 150 * s, 20 * s); rect += topleft + v2s32(offset.X, offset.Y); - Environment->addStaticText(k->button_name.c_str(), rect, false, true, - this, -1); + gui::StaticText::add(Environment, k->button_name, rect, + false, true, this, -1); } { @@ -300,8 +300,8 @@ bool GUIKeyChangeMenu::OnEvent(const SEvent& event) if (key_in_use && !this->key_used_text) { core::rect rect(0, 0, 600, 40); rect += v2s32(0, 0) + v2s32(25, 30); - this->key_used_text = Environment->addStaticText( - wstrgettext("Key already in use").c_str(), + this->key_used_text = gui::StaticText::add(Environment, + wstrgettext("Key already in use"), rect, false, true, this, -1); } else if (!key_in_use && this->key_used_text) { this->key_used_text->remove(); diff --git a/src/gui/guiPasswordChange.cpp b/src/gui/guiPasswordChange.cpp index d8e1c702fa73c..8edb377de5d08 100644 --- a/src/gui/guiPasswordChange.cpp +++ b/src/gui/guiPasswordChange.cpp @@ -89,7 +89,7 @@ void GUIPasswordChange::regenerateGui(v2u32 screensize) { core::rect rect(0, 0, 150 * s, 20 * s); rect += topleft_client + v2s32(25 * s, ypos + 6 * s); - Environment->addStaticText(wstrgettext("Old Password").c_str(), rect, + gui::StaticText::add(Environment, wstrgettext("Old Password"), rect, false, true, this, -1); } { @@ -104,8 +104,8 @@ void GUIPasswordChange::regenerateGui(v2u32 screensize) { core::rect rect(0, 0, 150 * s, 20 * s); rect += topleft_client + v2s32(25 * s, ypos + 6 * s); - Environment->addStaticText(wstrgettext("New Password").c_str(), rect, false, true, - this, -1); + gui::StaticText::add(Environment, wstrgettext("New Password"), rect, + false, true, this, -1); } { core::rect rect(0, 0, 230 * s, 30 * s); @@ -118,7 +118,7 @@ void GUIPasswordChange::regenerateGui(v2u32 screensize) { core::rect rect(0, 0, 150 * s, 20 * s); rect += topleft_client + v2s32(25 * s, ypos + 6 * s); - Environment->addStaticText(wstrgettext("Confirm Password").c_str(), rect, + gui::StaticText::add(Environment, wstrgettext("Confirm Password"), rect, false, true, this, -1); } { @@ -147,9 +147,9 @@ void GUIPasswordChange::regenerateGui(v2u32 screensize) { core::rect rect(0, 0, 300 * s, 20 * s); rect += topleft_client + v2s32(35 * s, ypos); - IGUIElement *e = Environment->addStaticText( - wstrgettext("Passwords do not match!").c_str(), rect, false, - true, this, ID_message); + IGUIElement *e = gui::StaticText::add( + Environment, wstrgettext("Passwords do not match!"), rect, + false, true, this, ID_message); e->setVisible(false); } } diff --git a/src/gui/guiScrollBar.cpp b/src/gui/guiScrollBar.cpp index c6a03f3e49535..8ec387d18d046 100644 --- a/src/gui/guiScrollBar.cpp +++ b/src/gui/guiScrollBar.cpp @@ -11,17 +11,19 @@ the arrow buttons where there is insufficient space. */ #include "guiScrollBar.h" -#include +#include "guiButton.h" #include GUIScrollBar::GUIScrollBar(IGUIEnvironment *environment, IGUIElement *parent, s32 id, - core::rect rectangle, bool horizontal, bool auto_scale) : + core::rect rectangle, bool horizontal, bool auto_scale, + ISimpleTextureSource *tsrc) : IGUIElement(EGUIET_ELEMENT, environment, parent, id, rectangle), up_button(nullptr), down_button(nullptr), is_dragging(false), is_horizontal(horizontal), is_auto_scaling(auto_scale), dragged_by_slider(false), tray_clicked(false), scroll_pos(0), draw_center(0), thumb_size(0), min_pos(0), max_pos(100), small_step(10), - large_step(50), drag_offset(0), page_size(100), border_size(0) + large_step(50), drag_offset(0), page_size(100), border_size(0), + m_tsrc(tsrc) { refreshControls(); setNotClipped(false); @@ -343,8 +345,9 @@ void GUIScrollBar::refreshControls() s32 h = RelativeRect.getHeight(); border_size = RelativeRect.getWidth() < h * 4 ? 0 : h; if (!up_button) { - up_button = Environment->addButton( - core::rect(0, 0, h, h), this); + core::rect up_button_rect(0, 0, h, h); + up_button = GUIButton::addButton(Environment, up_button_rect, m_tsrc, + this, -1, L""); up_button->setSubElement(true); up_button->setTabStop(false); } @@ -361,10 +364,12 @@ void GUIScrollBar::refreshControls() up_button->setAlignment(EGUIA_UPPERLEFT, EGUIA_UPPERLEFT, EGUIA_UPPERLEFT, EGUIA_LOWERRIGHT); if (!down_button) { - down_button = Environment->addButton( - core::rect(RelativeRect.getWidth() - h, 0, - RelativeRect.getWidth(), h), - this); + core::rect down_button_rect( + RelativeRect.getWidth() - h, 0, + RelativeRect.getWidth(), h + ); + down_button = GUIButton::addButton(Environment, down_button_rect, m_tsrc, + this, -1, L""); down_button->setSubElement(true); down_button->setTabStop(false); } @@ -386,8 +391,9 @@ void GUIScrollBar::refreshControls() s32 w = RelativeRect.getWidth(); border_size = RelativeRect.getHeight() < w * 4 ? 0 : w; if (!up_button) { - up_button = Environment->addButton( - core::rect(0, 0, w, w), this); + core::rect up_button_rect(0, 0, w, w); + up_button = GUIButton::addButton(Environment, up_button_rect, m_tsrc, + this, -1, L""); up_button->setSubElement(true); up_button->setTabStop(false); } @@ -404,10 +410,12 @@ void GUIScrollBar::refreshControls() up_button->setAlignment(EGUIA_UPPERLEFT, EGUIA_LOWERRIGHT, EGUIA_UPPERLEFT, EGUIA_UPPERLEFT); if (!down_button) { - down_button = Environment->addButton( - core::rect(0, RelativeRect.getHeight() - w, - w, RelativeRect.getHeight()), - this); + core::rect down_button_rect( + 0, RelativeRect.getHeight() - w, + w, RelativeRect.getHeight() + ); + down_button = GUIButton::addButton(Environment, down_button_rect, m_tsrc, + this, -1, L""); down_button->setSubElement(true); down_button->setTabStop(false); } diff --git a/src/gui/guiScrollBar.h b/src/gui/guiScrollBar.h index d18f8e8753ec8..3ff3bba3591b3 100644 --- a/src/gui/guiScrollBar.h +++ b/src/gui/guiScrollBar.h @@ -14,6 +14,8 @@ the arrow buttons where there is insufficient space. #include "irrlichttypes_extrabloated.h" +class ISimpleTextureSource; + using namespace irr; using namespace gui; @@ -21,7 +23,8 @@ class GUIScrollBar : public IGUIElement { public: GUIScrollBar(IGUIEnvironment *environment, IGUIElement *parent, s32 id, - core::rect rectangle, bool horizontal, bool auto_scale); + core::rect rectangle, bool horizontal, bool auto_scale, + ISimpleTextureSource *tsrc); enum ArrowVisibility { @@ -74,4 +77,6 @@ class GUIScrollBar : public IGUIElement core::rect slider_rect; video::SColor current_icon_color; + + ISimpleTextureSource *m_tsrc; }; diff --git a/src/gui/guiTable.cpp b/src/gui/guiTable.cpp index 3929d678b35da..b5042802a50d9 100644 --- a/src/gui/guiTable.cpp +++ b/src/gui/guiTable.cpp @@ -66,7 +66,7 @@ GUITable::GUITable(gui::IGUIEnvironment *env, 0, RelativeRect.getWidth(), RelativeRect.getHeight()), - false, true); + false, true, tsrc); m_scrollbar->setSubElement(true); m_scrollbar->setTabStop(false); m_scrollbar->setAlignment(gui::EGUIA_LOWERRIGHT, gui::EGUIA_LOWERRIGHT, @@ -901,7 +901,7 @@ bool GUITable::OnEvent(const SEvent &event) setToolTipText(cell ? m_strings[cell->tooltip_index].c_str() : L""); // Fix for #1567/#1806: - // IGUIScrollBar passes double click events to its parent, + // GUIScrollBar passes double click events to its parent, // which we don't want. Detect this case and discard the event if (event.MouseInput.Event != EMIE_MOUSE_MOVED && m_scrollbar->isVisible() && diff --git a/src/gui/guiVolumeChange.cpp b/src/gui/guiVolumeChange.cpp index aec24f59084ce..590149513b661 100644 --- a/src/gui/guiVolumeChange.cpp +++ b/src/gui/guiVolumeChange.cpp @@ -20,11 +20,11 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #include "guiVolumeChange.h" #include "debug.h" #include "guiButton.h" +#include "guiScrollBar.h" #include "serialization.h" #include #include #include -#include #include #include #include "settings.h" @@ -73,7 +73,7 @@ void GUIVolumeChange::regenerateGui(v2u32 screensize) core::rect rect(0, 0, 160 * s, 20 * s); rect = rect + v2s32(size.X / 2 - 80 * s, size.Y / 2 - 70 * s); - Environment->addStaticText(fwgettext("Sound Volume: %d%%", volume).c_str(), + StaticText::add(Environment, fwgettext("Sound Volume: %d%%", volume), rect, false, true, this, ID_soundText); } { @@ -85,8 +85,8 @@ void GUIVolumeChange::regenerateGui(v2u32 screensize) { core::rect rect(0, 0, 300 * s, 20 * s); rect = rect + v2s32(size.X / 2 - 150 * s, size.Y / 2); - gui::IGUIScrollBar *e = Environment->addScrollBar(true, - rect, this, ID_soundSlider); + auto e = make_irr(Environment, this, + ID_soundSlider, rect, true, false, m_tsrc); e->setMax(100); e->setPos(volume); } @@ -151,7 +151,7 @@ bool GUIVolumeChange::OnEvent(const SEvent& event) } if (event.GUIEvent.EventType == gui::EGET_SCROLL_BAR_CHANGED) { if (event.GUIEvent.Caller->getID() == ID_soundSlider) { - s32 pos = ((gui::IGUIScrollBar*)event.GUIEvent.Caller)->getPos(); + s32 pos = static_cast(event.GUIEvent.Caller)->getPos(); g_settings->setFloat("sound_volume", (float) pos / 100); gui::IGUIElement *e = getElementFromId(ID_soundText); diff --git a/src/gui/touchscreengui.cpp b/src/gui/touchscreengui.cpp index f0774bebdcaa4..4a6486573c6ae 100644 --- a/src/gui/touchscreengui.cpp +++ b/src/gui/touchscreengui.cpp @@ -581,25 +581,28 @@ touch_gui_button_id TouchScreenGUI::getButtonID(size_t eventID) return after_last_element_id; } -bool TouchScreenGUI::isHUDButton(const SEvent &event) +bool TouchScreenGUI::isHotbarButton(const SEvent &event) { - // check if hud item is pressed - for (auto &hud_rect : m_hud_rects) { - if (hud_rect.second.isPointInside(v2s32(event.TouchInput.X, event.TouchInput.Y))) { - SEvent translated{}; - translated.EventType = irr::EET_KEY_INPUT_EVENT; - translated.KeyInput.Key = (irr::EKEY_CODE) (KEY_KEY_1 + hud_rect.first); - translated.KeyInput.Control = false; - translated.KeyInput.Shift = false; - translated.KeyInput.PressedDown = true; - m_receiver->OnEvent(translated); - m_hud_ids[event.TouchInput.ID] = translated.KeyInput.Key; + const v2s32 touch_pos = v2s32(event.TouchInput.X, event.TouchInput.Y); + // check if hotbar item is pressed + for (auto &[index, rect] : m_hotbar_rects) { + if (rect.isPointInside(touch_pos)) { + // We can't just emit a keypress event because the number keys + // range from 1 to 9, but there may be more hotbar items. + m_hotbar_selection = index; return true; } } return false; } +std::optional TouchScreenGUI::getHotbarSelection() +{ + auto selection = m_hotbar_selection; + m_hotbar_selection = std::nullopt; + return selection; +} + void TouchScreenGUI::handleButtonEvent(touch_gui_button_id button, size_t eventID, bool action) { @@ -743,10 +746,10 @@ void TouchScreenGUI::translateEvent(const SEvent &event) handleButtonEvent(button, eventID, true); m_settings_bar.deactivate(); m_rare_controls_bar.deactivate(); - } else if (isHUDButton(event)) { + } else if (isHotbarButton(event)) { m_settings_bar.deactivate(); m_rare_controls_bar.deactivate(); - // already handled in isHUDButton() + // already handled in isHotbarButton() } else if (m_settings_bar.isButton(event)) { m_rare_controls_bar.deactivate(); // already handled in isSettingsBarButton() @@ -826,7 +829,7 @@ void TouchScreenGUI::translateEvent(const SEvent &event) m_pointer_pos[event.TouchInput.ID] = touch_pos; // adapt to similar behavior as pc screen - const double d = g_settings->getFloat("mouse_sensitivity", 0.001f, 10.0f) * 3.0f; + const double d = g_settings->getFloat("touchscreen_sensitivity", 0.001f, 10.0f) * 3.0f; m_camera_yaw_change -= dir_free.X * d; m_camera_pitch = MYMIN(MYMAX(m_camera_pitch + (dir_free.Y * d), -180.0f), 180.0f); @@ -1069,14 +1072,14 @@ void TouchScreenGUI::step(float dtime) m_rare_controls_bar.step(dtime); } -void TouchScreenGUI::resetHud() +void TouchScreenGUI::resetHotbarRects() { - m_hud_rects.clear(); + m_hotbar_rects.clear(); } -void TouchScreenGUI::registerHudItem(int index, const rect &rect) +void TouchScreenGUI::registerHotbarRect(u16 index, const rect &rect) { - m_hud_rects[index] = rect; + m_hotbar_rects[index] = rect; } void TouchScreenGUI::setVisible(bool visible) diff --git a/src/gui/touchscreengui.h b/src/gui/touchscreengui.h index 5b22c29c55794..ff5219f3a374f 100644 --- a/src/gui/touchscreengui.h +++ b/src/gui/touchscreengui.h @@ -24,7 +24,9 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include -#include +#include +#include +#include #include #include "client/tile.h" @@ -90,7 +92,7 @@ struct button_info FIRST_TEXTURE, SECOND_TEXTURE } toggleable = NOT_TOGGLEABLE; - std::vector textures; + std::vector textures; }; class AutoHideButtonBar @@ -185,14 +187,16 @@ class TouchScreenGUI float getMovementSpeed() { return m_joystick_speed; } void step(float dtime); - void resetHud(); - void registerHudItem(int index, const rect &rect); inline void setUseCrosshair(bool use_crosshair) { m_draw_crosshair = use_crosshair; } void setVisible(bool visible); void hide(); void show(); + void resetHotbarRects(); + void registerHotbarRect(u16 index, const rect &rect); + std::optional getHotbarSelection(); + private: bool m_initialized = false; IrrlichtDevice *m_device; @@ -202,10 +206,11 @@ class TouchScreenGUI v2u32 m_screensize; s32 button_size; double m_touchscreen_threshold; - std::map> m_hud_rects; - std::map m_hud_ids; bool m_visible; // is the whole touch screen gui visible + std::unordered_map> m_hotbar_rects; + std::optional m_hotbar_selection = std::nullopt; + // value in degree double m_camera_yaw_change = 0.0; double m_camera_pitch = 0.0; @@ -271,8 +276,8 @@ class TouchScreenGUI // handle a button event void handleButtonEvent(touch_gui_button_id bID, size_t eventID, bool action); - // handle pressed hud buttons - bool isHUDButton(const SEvent &event); + // handle pressing hotbar items + bool isHotbarButton(const SEvent &event); // do a right-click bool doRightClick(); @@ -284,7 +289,7 @@ class TouchScreenGUI void applyJoystickStatus(); // array for saving last known position of a pointer - std::map m_pointer_pos; + std::unordered_map m_pointer_pos; // settings bar AutoHideButtonBar m_settings_bar; diff --git a/src/inventorymanager.cpp b/src/inventorymanager.cpp index e1bb42c4847be..458deff26e263 100644 --- a/src/inventorymanager.cpp +++ b/src/inventorymanager.cpp @@ -354,6 +354,9 @@ void IMoveAction::apply(InventoryManager *mgr, ServerActiveObject *player, IGame return; } + if (list_from.get() == list_to.get() && from_i == to_i) + return; // Same slot + /* Do not handle rollback if both inventories are that of the same player */ diff --git a/src/irrlicht_changes/static_text.h b/src/irrlicht_changes/static_text.h index 3608b271c15bf..636760f6cd0ec 100644 --- a/src/irrlicht_changes/static_text.h +++ b/src/irrlicht_changes/static_text.h @@ -49,27 +49,7 @@ namespace gui s32 id = -1, bool fillBackground = false) { - if (parent == NULL) { - // parent is NULL, so we must find one, or we need not to drop - // result, but then there will be a memory leak. - // - // What Irrlicht does is to use guienv as a parent, but the problem - // is that guienv is here only an IGUIEnvironment, while it is a - // CGUIEnvironment in Irrlicht, which inherits from both IGUIElement - // and IGUIEnvironment. - // - // A solution would be to dynamic_cast guienv to a - // IGUIElement*, but Irrlicht is shipped without rtti support - // in some distributions, causing the dymanic_cast to segfault. - // - // Thus, to find the parent, we create a dummy StaticText and ask - // for its parent, and then remove it. - irr::gui::IGUIStaticText *dummy_text = - guienv->addStaticText(L"", rectangle, border, wordWrap, - parent, id, fillBackground); - parent = dummy_text->getParent(); - dummy_text->remove(); - } + parent = parent ? parent : guienv->getRootGUIElement(); irr::gui::IGUIStaticText *result = new irr::gui::StaticText( text, border, guienv, parent, id, rectangle, fillBackground); diff --git a/src/itemdef.cpp b/src/itemdef.cpp index ae252c4a01f6b..07f07a53e8959 100644 --- a/src/itemdef.cpp +++ b/src/itemdef.cpp @@ -123,7 +123,7 @@ void ItemDefinition::reset() sound_use_air = SoundSpec(); range = -1; node_placement_prediction.clear(); - place_param2 = 0; + place_param2.reset(); } void ItemDefinition::serialize(std::ostream &os, u16 protocol_version) const @@ -169,10 +169,20 @@ void ItemDefinition::serialize(std::ostream &os, u16 protocol_version) const os << serializeString16(short_description); - os << place_param2; + if (protocol_version <= 43) { + // Uncertainity whether 0 is the specified prediction or means disabled + if (place_param2) + os << *place_param2; + else + os << (u8)0; + } sound_use.serializeSimple(os, protocol_version); sound_use_air.serializeSimple(os, protocol_version); + + os << (u8)place_param2.has_value(); // protocol_version >= 43 + if (place_param2) + os << *place_param2; } void ItemDefinition::deSerialize(std::istream &is, u16 protocol_version) @@ -226,10 +236,21 @@ void ItemDefinition::deSerialize(std::istream &is, u16 protocol_version) try { short_description = deSerializeString16(is); - place_param2 = readU8(is); // 0 if missing + if (protocol_version <= 43) { + place_param2 = readU8(is); + // assume disabled prediction + if (place_param2 == 0) + place_param2.reset(); + } sound_use.deSerializeSimple(is, protocol_version); sound_use_air.deSerializeSimple(is, protocol_version); + + if (is.eof()) + throw SerializationError(""); + + if (readU8(is)) // protocol_version >= 43 + place_param2 = readU8(is); } catch(SerializationError &e) {}; } @@ -332,14 +353,14 @@ class CItemDefManager: public IWritableItemDefManager if (!inventory_overlay.empty()) cache_key += ":" + inventory_overlay; - infostream << "Lazily creating item texture and mesh for \"" - << cache_key << "\""<second.get(); + infostream << "Lazily creating item texture and mesh for \"" + << cache_key << "\"" << std::endl; + ITextureSource *tsrc = client->getTextureSource(); // Create new ClientCached diff --git a/src/itemdef.h b/src/itemdef.h index bad3a3e2411ed..8ce6a15cd07a8 100644 --- a/src/itemdef.h +++ b/src/itemdef.h @@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "irrlichttypes_extrabloated.h" #include #include +#include #include #include "itemgroup.h" #include "sound.h" @@ -87,7 +88,7 @@ struct ItemDefinition // Server will update the precise end result a moment later. // "" = no prediction std::string node_placement_prediction; - u8 place_param2; + std::optional place_param2; /* Some helpful methods diff --git a/src/main.cpp b/src/main.cpp index 309413f99c610..e3c72fdd03daa 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -713,8 +713,8 @@ static void uninit_common() static void startup_message() { - infostream << PROJECT_NAME << " " << _("with") - << " SER_FMT_VER_HIGHEST_READ=" + infostream << PROJECT_NAME_C << " " << g_version_hash + << "\nwith SER_FMT_VER_HIGHEST_READ=" << (int)SER_FMT_VER_HIGHEST_READ << ", " << g_build_info << std::endl; } @@ -994,15 +994,15 @@ static bool determine_subgame(GameParams *game_params) if (game_params->game_spec.isValid()) { gamespec = game_params->game_spec; infostream << "Using commanded gameid [" << gamespec.id << "]" << std::endl; - } else { // Otherwise we will be using "minetest" - gamespec = findSubgame(g_settings->get("default_game")); - infostream << "Using default gameid [" << gamespec.id << "]" << std::endl; - if (!gamespec.isValid()) { - errorstream << "Game specified in default_game [" - << g_settings->get("default_game") - << "] is invalid." << std::endl; - return false; + } else { + if (game_params->is_dedicated_server) { + // If this is a dedicated server and no gamespec has been specified, + // print a friendly error pointing to ContentDB. + errorstream << "To run a " PROJECT_NAME_C " server, you need to select a game using the '--gameid' argument." << std::endl + << "Check out https://content.minetest.net for a selection of games to pick from and download." << std::endl; } + + return false; } } else { // World exists std::string world_gameid = getWorldGameId(game_params->world_path, false); diff --git a/src/map.h b/src/map.h index 86fca332f2e67..07f6c56e82ed0 100644 --- a/src/map.h +++ b/src/map.h @@ -452,7 +452,7 @@ class ServerMap : public Map void reportMetrics(u64 save_time_us, u32 saved_blocks, u32 all_blocks) override; private: - friend class LuaVoxelManip; + friend class ModApiMapgen; // for m_transforming_liquid // Emerge manager EmergeManager *m_emerge; diff --git a/src/mapblock.cpp b/src/mapblock.cpp index e3c307563d96a..3b902153e29c3 100644 --- a/src/mapblock.cpp +++ b/src/mapblock.cpp @@ -91,14 +91,15 @@ bool MapBlock::onObjectsActivation() if (m_static_objects.getAllStored().empty()) return false; + const auto count = m_static_objects.getStoredSize(); verbosestream << "MapBlock::onObjectsActivation(): " - << "activating objects of block " << getPos() << " (" - << m_static_objects.getStoredSize() << " objects)" << std::endl; + << "activating " << count << "objects in block " << getPos() + << std::endl; - if (m_static_objects.getStoredSize() > g_settings->getU16("max_objects_per_block")) { + if (count > g_settings->getU16("max_objects_per_block")) { errorstream << "suspiciously large amount of objects detected: " - << m_static_objects.getStoredSize() << " in " - << getPos() << "; removing all of them." << std::endl; + << count << " in " << getPos() << "; removing all of them." + << std::endl; // Clear stored list m_static_objects.clearStored(); raiseModified(MOD_STATE_WRITE_NEEDED, MOD_REASON_TOO_MANY_OBJECTS); diff --git a/src/mapgen/mapgen.cpp b/src/mapgen/mapgen.cpp index 791c56609f7e5..a4557037e1865 100644 --- a/src/mapgen/mapgen.cpp +++ b/src/mapgen/mapgen.cpp @@ -370,8 +370,13 @@ inline bool Mapgen::isLiquidHorizontallyFlowable(u32 vi, v3s16 em) void Mapgen::updateLiquid(UniqueQueue *trans_liquid, v3s16 nmin, v3s16 nmax) { bool isignored, isliquid, wasignored, wasliquid, waschecked, waspushed; + content_t was_n; const v3s16 &em = vm->m_area.getExtent(); + isignored = true; + isliquid = false; + was_n = CONTENT_IGNORE; + for (s16 z = nmin.Z + 1; z <= nmax.Z - 1; z++) for (s16 x = nmin.X + 1; x <= nmax.X - 1; x++) { wasignored = true; @@ -381,8 +386,11 @@ void Mapgen::updateLiquid(UniqueQueue *trans_liquid, v3s16 nmin, v3s16 nm u32 vi = vm->m_area.index(x, nmax.Y, z); for (s16 y = nmax.Y; y >= nmin.Y; y--) { - isignored = vm->m_data[vi].getContent() == CONTENT_IGNORE; - isliquid = ndef->get(vm->m_data[vi]).isLiquid(); + const content_t is_n = vm->m_data[vi].getContent(); + if (is_n != was_n) { + isignored = is_n == CONTENT_IGNORE; + isliquid = ndef->get(is_n).isLiquid(); + } if (isignored || wasignored || isliquid == wasliquid) { // Neither topmost node of liquid column nor topmost node below column @@ -411,6 +419,7 @@ void Mapgen::updateLiquid(UniqueQueue *trans_liquid, v3s16 nmin, v3s16 nm } } + was_n = is_n; wasliquid = isliquid; wasignored = isignored; VoxelArea::add_y(em, vi, -1); diff --git a/src/mapgen/mapgen_singlenode.cpp b/src/mapgen/mapgen_singlenode.cpp index 1a6cba6b02cd9..6585ea7a948fb 100644 --- a/src/mapgen/mapgen_singlenode.cpp +++ b/src/mapgen/mapgen_singlenode.cpp @@ -74,8 +74,8 @@ void MapgenSinglenode::makeChunk(BlockMakeData *data) } } - // Add top and bottom side of water to transforming_liquid queue - updateLiquid(&data->transforming_liquid, node_min, node_max); + if (ndef->get(n_node).isLiquid()) + updateLiquid(&data->transforming_liquid, node_min, node_max); // Set lighting if ((flags & MG_LIGHT) && set_light == LIGHT_SUN) diff --git a/src/network/networkpacket.cpp b/src/network/networkpacket.cpp index 6b8b0f7037a24..2867c12a5977e 100644 --- a/src/network/networkpacket.cpp +++ b/src/network/networkpacket.cpp @@ -63,7 +63,8 @@ void NetworkPacket::putRawPacket(const u8 *data, u32 datasize, session_t peer_id // split command and datas m_command = readU16(&data[0]); - memcpy(m_data.data(), &data[2], m_datasize); + if (m_datasize > 0) + memcpy(m_data.data(), &data[2], m_datasize); } void NetworkPacket::clear() @@ -553,7 +554,8 @@ Buffer NetworkPacket::oldForgePacket() { Buffer sb(m_datasize + 2); writeU16(&sb[0], m_command); - memcpy(&sb[2], m_data.data(), m_datasize); + if (m_datasize > 0) + memcpy(&sb[2], m_data.data(), m_datasize); return sb; } diff --git a/src/network/networkprotocol.h b/src/network/networkprotocol.h index c9e29cf12bac6..e012fc06cbbf9 100644 --- a/src/network/networkprotocol.h +++ b/src/network/networkprotocol.h @@ -217,6 +217,7 @@ with this program; if not, write to the Free Software Foundation, Inc., [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] */ diff --git a/src/nodedef.cpp b/src/nodedef.cpp index fef55e7dfab64..bdaf0ebed55d7 100644 --- a/src/nodedef.cpp +++ b/src/nodedef.cpp @@ -415,6 +415,7 @@ void ContentFeatures::reset() node_dig_prediction = "air"; move_resistance = 0; liquid_move_physics = false; + post_effect_color_shaded = false; } void ContentFeatures::setAlphaFromLegacy(u8 legacy_alpha) @@ -543,6 +544,7 @@ void ContentFeatures::serialize(std::ostream &os, u16 protocol_version) const writeU8(os, alpha); writeU8(os, move_resistance); writeU8(os, liquid_move_physics); + writeU8(os, post_effect_color_shaded); } void ContentFeatures::deSerialize(std::istream &is, u16 protocol_version) @@ -656,6 +658,11 @@ void ContentFeatures::deSerialize(std::istream &is, u16 protocol_version) if (is.eof()) throw SerializationError(""); liquid_move_physics = tmp; + + tmp = readU8(is); + if (is.eof()) + throw SerializationError(""); + post_effect_color_shaded = tmp; } catch(SerializationError &e) {}; } diff --git a/src/nodedef.h b/src/nodedef.h index 05ba10266b811..b1ec1e5eedc59 100644 --- a/src/nodedef.h +++ b/src/nodedef.h @@ -364,6 +364,7 @@ struct ContentFeatures std::vector connects_to_ids; // Post effect color, drawn when the camera is inside the node. video::SColor post_effect_color; + bool post_effect_color_shaded; // Flowing liquid or leveled nodebox, value = default level u8 leveled; // Maximum value for leveled nodes diff --git a/src/player.h b/src/player.h index 9fdaf6ff5cf48..1a59a17ce258e 100644 --- a/src/player.h +++ b/src/player.h @@ -106,6 +106,14 @@ struct PlayerPhysicsOverride bool sneak_glitch = false; // "Temporary" option for old move code bool new_move = true; + + float speed_climb = 1.f; + float speed_crouch = 1.f; + float liquid_fluidity = 1.f; + float liquid_fluidity_smooth = 1.f; + float liquid_sink = 1.f; + float acceleration_default = 1.f; + float acceleration_air = 1.f; }; struct PlayerSettings diff --git a/src/script/common/c_content.cpp b/src/script/common/c_content.cpp index 89bf609b20a92..1756c49c5c977 100644 --- a/src/script/common/c_content.cpp +++ b/src/script/common/c_content.cpp @@ -133,7 +133,9 @@ void read_item_definition(lua_State* L, int index, getstringfield(L, index, "node_placement_prediction", def.node_placement_prediction); - getintfield(L, index, "place_param2", def.place_param2); + int place_param2; + if (getintfield(L, index, "place_param2", place_param2)) + def.place_param2 = rangelim(place_param2, 0, U8_MAX); } /******************************************************************************/ @@ -195,6 +197,43 @@ void push_item_definition_full(lua_State *L, const ItemDefinition &i) lua_setfield(L, -2, "node_placement_prediction"); } +/******************************************************************************/ +const std::array object_property_keys = { + "hp_max", + "breath_max", + "physical", + "collide_with_objects", + "collisionbox", + "selectionbox", + "pointable", + "visual", + "mesh", + "visual_size", + "textures", + "colors", + "spritediv", + "initial_sprite_basepos", + "is_visible", + "makes_footstep_sound", + "stepheight", + "eye_height", + "automatic_rotate", + "automatic_face_movement_dir", + "backface_culling", + "glow", + "nametag", + "nametag_color", + "automatic_face_movement_max_rotation_per_sec", + "infotext", + "static_save", + "wield_item", + "zoom_fov", + "use_texture_alpha", + "shaded", + "damage_texture_modifier", + "show_on_minimap" +}; + /******************************************************************************/ void read_object_properties(lua_State *L, int index, ServerActiveObject *sao, ObjectProperties *prop, IItemDefManager *idef) @@ -362,6 +401,9 @@ void read_object_properties(lua_State *L, int index, getboolfield(L, -1, "show_on_minimap", prop->show_on_minimap); getstringfield(L, -1, "damage_texture_modifier", prop->damage_texture_modifier); + + // Remember to update object_property_keys above + // when adding a new property } /******************************************************************************/ @@ -459,6 +501,9 @@ void push_object_properties(lua_State *L, ObjectProperties *prop) lua_setfield(L, -2, "damage_texture_modifier"); lua_pushboolean(L, prop->show_on_minimap); lua_setfield(L, -2, "show_on_minimap"); + + // Remember to update object_property_keys above + // when adding a new property } /******************************************************************************/ @@ -685,6 +730,8 @@ void read_content_features(lua_State *L, ContentFeatures &f, int index) read_color(L, -1, &f.post_effect_color); lua_pop(L, 1); + getboolfield(L, index, "post_effect_color_shaded", f.post_effect_color_shaded); + f.param_type = (ContentParamType)getenumfield(L, index, "paramtype", ScriptApiNode::es_ContentParamType, CPT_NONE); f.param_type_2 = (ContentParamType2)getenumfield(L, index, "paramtype2", @@ -916,6 +963,8 @@ void push_content_features(lua_State *L, const ContentFeatures &c) push_ARGB8(L, c.post_effect_color); lua_setfield(L, -2, "post_effect_color"); + lua_pushboolean(L, c.post_effect_color_shaded); + lua_setfield(L, -2, "post_effect_color_shaded"); lua_pushnumber(L, c.leveled); lua_setfield(L, -2, "leveled"); lua_pushnumber(L, c.leveled_max); diff --git a/src/script/common/c_content.h b/src/script/common/c_content.h index af08dfda23661..4335479c4ebcc 100644 --- a/src/script/common/c_content.h +++ b/src/script/common/c_content.h @@ -33,6 +33,7 @@ extern "C" { #include #include +#include #include "irrlichttypes_bloated.h" #include "util/string.h" @@ -71,6 +72,9 @@ struct collisionMoveResult; extern struct EnumString es_TileAnimationType[]; + +extern const std::array object_property_keys; + void read_content_features (lua_State *L, ContentFeatures &f, int index); void push_content_features (lua_State *L, @@ -118,6 +122,7 @@ void read_object_properties (lua_State *L, int index, ServerActiveObject *sao, ObjectProperties *prop, IItemDefManager *idef); + void push_object_properties (lua_State *L, ObjectProperties *prop); diff --git a/src/script/common/c_internal.cpp b/src/script/common/c_internal.cpp index 79063141d3e86..b02591f3ac6c0 100644 --- a/src/script/common/c_internal.cpp +++ b/src/script/common/c_internal.cpp @@ -42,7 +42,8 @@ int script_exception_wrapper(lua_State *L, lua_CFunction f) } catch (const char *s) { // Catch and convert exceptions. lua_pushstring(L, s); } catch (std::exception &e) { - lua_pushstring(L, e.what()); + std::string e_descr = debug_describe_exc(e); + lua_pushlstring(L, e_descr.c_str(), e_descr.size()); } return lua_error(L); // Rethrow as a Lua error. } @@ -177,7 +178,8 @@ void log_deprecated(lua_State *L, std::string message, int stack_depth) if (mode == DeprecatedHandlingMode::Ignore) return; - script_log_add_source(L, message, stack_depth); + if (stack_depth >= 0) + script_log_add_source(L, message, stack_depth); warningstream << message << std::endl; if (mode == DeprecatedHandlingMode::Error) diff --git a/src/script/common/c_internal.h b/src/script/common/c_internal.h index c8bce099cb9ac..eac492d081088 100644 --- a/src/script/common/c_internal.h +++ b/src/script/common/c_internal.h @@ -147,7 +147,8 @@ DeprecatedHandlingMode get_deprecated_handling_mode(); * * @param L Lua State * @param message The deprecation method - * @param stack_depth How far on the stack to the first user function (ie: not builtin or core) + * @param stack_depth How far on the stack to the first user function + * (ie: not builtin or core). -1 to disabled. */ void log_deprecated(lua_State *L, std::string message, int stack_depth = 1); diff --git a/src/script/cpp_api/s_entity.cpp b/src/script/cpp_api/s_entity.cpp index 1eb20e27ff8ee..7b96e65eda2d8 100644 --- a/src/script/cpp_api/s_entity.cpp +++ b/src/script/cpp_api/s_entity.cpp @@ -29,9 +29,6 @@ bool ScriptApiEntity::luaentity_Add(u16 id, const char *name) { SCRIPTAPI_PRECHECKHEADER - verbosestream<<"scriptapi_luaentity_add: id="< struct ObjectProperties; struct ToolCapabilities; @@ -37,7 +38,7 @@ class ScriptApiEntity void luaentity_Remove(u16 id); std::string luaentity_GetStaticdata(u16 id); void luaentity_GetProperties(u16 id, - ServerActiveObject *self, ObjectProperties *prop); + ServerActiveObject *self, ObjectProperties *prop, const std::string &entity_name); void luaentity_Step(u16 id, float dtime, const collisionMoveResult *moveresult); bool luaentity_Punch(u16 id, @@ -51,4 +52,11 @@ class ScriptApiEntity private: bool luaentity_run_simple_callback(u16 id, ServerActiveObject *sao, const char *field); + + void logDeprecationForExistingProperties(lua_State *L, int index, const std::string &name); + + /** Stores names of entities that already caused a deprecation warning due to + * properties being outside of initial_properties. If an entity's name is in here, + * it won't cause any more of those deprecation warnings. */ + std::unordered_set deprecation_warned_init_properties; }; diff --git a/src/script/lua_api/l_localplayer.cpp b/src/script/lua_api/l_localplayer.cpp index ff9c61f530ebf..42ca2e0c213f9 100644 --- a/src/script/lua_api/l_localplayer.cpp +++ b/src/script/lua_api/l_localplayer.cpp @@ -177,6 +177,27 @@ int LuaLocalPlayer::l_get_physics_override(lua_State *L) lua_pushboolean(L, phys.new_move); lua_setfield(L, -2, "new_move"); + lua_pushnumber(L, phys.speed_climb); + lua_setfield(L, -2, "speed_climb"); + + lua_pushnumber(L, phys.speed_crouch); + lua_setfield(L, -2, "speed_crouch"); + + lua_pushnumber(L, phys.liquid_fluidity); + lua_setfield(L, -2, "liquid_fluidity"); + + lua_pushnumber(L, phys.liquid_fluidity_smooth); + lua_setfield(L, -2, "liquid_fluidity_smooth"); + + lua_pushnumber(L, phys.liquid_sink); + lua_setfield(L, -2, "liquid_sink"); + + lua_pushnumber(L, phys.acceleration_default); + lua_setfield(L, -2, "acceleration_default"); + + lua_pushnumber(L, phys.acceleration_air); + lua_setfield(L, -2, "acceleration_air"); + return 1; } diff --git a/src/script/lua_api/l_mapgen.cpp b/src/script/lua_api/l_mapgen.cpp index 5d6fce4e05464..86e0c1c32cd3d 100644 --- a/src/script/lua_api/l_mapgen.cpp +++ b/src/script/lua_api/l_mapgen.cpp @@ -455,10 +455,8 @@ size_t get_biome_list(lua_State *L, int index, // returns number of failed resolutions size_t fail_count = 0; - size_t count = 0; for (lua_pushnil(L); lua_next(L, index); lua_pop(L, 1)) { - count++; Biome *biome = get_or_load_biome(L, -1, biomemgr); if (!biome) { fail_count++; @@ -1806,6 +1804,51 @@ int ModApiMapgen::l_read_schematic(lua_State *L) return 1; } +int ModApiMapgen::update_liquids(lua_State *L, MMVManip *vm) +{ + GET_ENV_PTR; + + ServerMap *map = &(env->getServerMap()); + const NodeDefManager *ndef = getServer(L)->getNodeDefManager(); + + Mapgen mg; + mg.vm = vm; + mg.ndef = ndef; + + mg.updateLiquid(&map->m_transforming_liquid, + vm->m_area.MinEdge, vm->m_area.MaxEdge); + return 0; +} + +int ModApiMapgen::calc_lighting(lua_State *L, MMVManip *vm, + v3s16 pmin, v3s16 pmax, bool propagate_shadow) +{ + const NodeDefManager *ndef = getGameDef(L)->ndef(); + EmergeManager *emerge = getServer(L)->getEmergeManager(); + + assert(vm->m_area.contains(VoxelArea(pmin, pmax))); + + Mapgen mg; + mg.vm = vm; + mg.ndef = ndef; + mg.water_level = emerge->mgparams->water_level; + + mg.calcLighting(pmin, pmax, vm->m_area.MinEdge, vm->m_area.MaxEdge, + propagate_shadow); + return 0; +} + +int ModApiMapgen::set_lighting(lua_State *L, MMVManip *vm, + v3s16 pmin, v3s16 pmax, u8 light) +{ + assert(vm->m_area.contains(VoxelArea(pmin, pmax))); + + Mapgen mg; + mg.vm = vm; + + mg.setLighting(light, pmin, pmax); + return 0; +} void ModApiMapgen::Initialize(lua_State *L, int top) { diff --git a/src/script/lua_api/l_mapgen.h b/src/script/lua_api/l_mapgen.h index 1428a91c81e3b..e7984b2dd3450 100644 --- a/src/script/lua_api/l_mapgen.h +++ b/src/script/lua_api/l_mapgen.h @@ -20,11 +20,15 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once #include "lua_api/l_base.h" +#include "irr_v3d.h" typedef u16 biome_t; // copy from mg_biome.h to avoid an unnecessary include +class MMVManip; + class ModApiMapgen : public ModApiBase { + friend class LuaVoxelManip; private: // get_biome_id(biomename) // returns the biome id as used in biomemap and returned by 'get_biome_data()' @@ -139,6 +143,21 @@ class ModApiMapgen : public ModApiBase // read_schematic(schematic, options={...}) static int l_read_schematic(lua_State *L); + // Foreign implementations + /* + * In this case the API functions belong to LuaVoxelManip (so l_vmanip.cpp), + * but the implementations are so deeply connected to mapgen-related code + * that they are better off being here. + */ + + static int update_liquids(lua_State *L, MMVManip *vm); + + static int calc_lighting(lua_State *L, MMVManip *vm, + v3s16 pmin, v3s16 pmax, bool propagate_shadow); + + static int set_lighting(lua_State *L, MMVManip *vm, + v3s16 pmin, v3s16 pmax, u8 light); + public: static void Initialize(lua_State *L, int top); diff --git a/src/script/lua_api/l_object.cpp b/src/script/lua_api/l_object.cpp index 08140f3bc0ed6..684a55dc4fcb2 100644 --- a/src/script/lua_api/l_object.cpp +++ b/src/script/lua_api/l_object.cpp @@ -1435,6 +1435,13 @@ int ObjectRef::l_set_physics_override(lua_State *L) modified |= getboolfield(L, 2, "sneak", phys.sneak); modified |= getboolfield(L, 2, "sneak_glitch", phys.sneak_glitch); modified |= getboolfield(L, 2, "new_move", phys.new_move); + modified |= getfloatfield(L, 2, "speed_climb", phys.speed_climb); + modified |= getfloatfield(L, 2, "speed_crouch", phys.speed_crouch); + modified |= getfloatfield(L, 2, "liquid_fluidity", phys.liquid_fluidity); + modified |= getfloatfield(L, 2, "liquid_fluidity_smooth", phys.liquid_fluidity_smooth); + modified |= getfloatfield(L, 2, "liquid_sink", phys.liquid_sink); + modified |= getfloatfield(L, 2, "acceleration_default", phys.acceleration_default); + modified |= getfloatfield(L, 2, "acceleration_air", phys.acceleration_air); if (modified) playersao->m_physics_override_sent = false; } else { @@ -1481,6 +1488,20 @@ int ObjectRef::l_get_physics_override(lua_State *L) lua_setfield(L, -2, "sneak_glitch"); lua_pushboolean(L, phys.new_move); lua_setfield(L, -2, "new_move"); + lua_pushnumber(L, phys.speed_climb); + lua_setfield(L, -2, "speed_climb"); + lua_pushnumber(L, phys.speed_crouch); + lua_setfield(L, -2, "speed_crouch"); + lua_pushnumber(L, phys.liquid_fluidity); + lua_setfield(L, -2, "liquid_fluidity"); + lua_pushnumber(L, phys.liquid_fluidity_smooth); + lua_setfield(L, -2, "liquid_fluidity_smooth"); + lua_pushnumber(L, phys.liquid_sink); + lua_setfield(L, -2, "liquid_sink"); + lua_pushnumber(L, phys.acceleration_default); + lua_setfield(L, -2, "acceleration_default"); + lua_pushnumber(L, phys.acceleration_air); + lua_setfield(L, -2, "acceleration_air"); return 1; } diff --git a/src/script/lua_api/l_vmanip.cpp b/src/script/lua_api/l_vmanip.cpp index 7b27402d6ac8f..da89f4e578003 100644 --- a/src/script/lua_api/l_vmanip.cpp +++ b/src/script/lua_api/l_vmanip.cpp @@ -19,16 +19,15 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include "lua_api/l_vmanip.h" +#include "lua_api/l_mapgen.h" #include "lua_api/l_internal.h" #include "common/c_content.h" #include "common/c_converter.h" #include "common/c_packer.h" -#include "emerge.h" #include "environment.h" #include "map.h" #include "mapblock.h" #include "server.h" -#include "mapgen/mapgen.h" #include "voxelalgorithms.h" // garbage collector @@ -162,64 +161,36 @@ int LuaVoxelManip::l_set_node_at(lua_State *L) int LuaVoxelManip::l_update_liquids(lua_State *L) { - GET_ENV_PTR; - LuaVoxelManip *o = checkObject(L, 1); - ServerMap *map = &(env->getServerMap()); - const NodeDefManager *ndef = getServer(L)->getNodeDefManager(); - MMVManip *vm = o->vm; - - Mapgen mg; - mg.vm = vm; - mg.ndef = ndef; - - mg.updateLiquid(&map->m_transforming_liquid, - vm->m_area.MinEdge, vm->m_area.MaxEdge); - - return 0; + return ModApiMapgen::update_liquids(L, o->vm); } int LuaVoxelManip::l_calc_lighting(lua_State *L) { - NO_MAP_LOCK_REQUIRED; - LuaVoxelManip *o = checkObject(L, 1); if (!o->is_mapgen_vm) { - warningstream << "VoxelManip:calc_lighting called for a non-mapgen " - "VoxelManip object" << std::endl; + log_deprecated(L, "calc_lighting called for a non-mapgen " + "VoxelManip object"); return 0; } - const NodeDefManager *ndef = getServer(L)->getNodeDefManager(); - EmergeManager *emerge = getServer(L)->getEmergeManager(); MMVManip *vm = o->vm; v3s16 yblock = v3s16(0, 1, 0) * MAP_BLOCKSIZE; - v3s16 fpmin = vm->m_area.MinEdge; - v3s16 fpmax = vm->m_area.MaxEdge; - v3s16 pmin = lua_istable(L, 2) ? check_v3s16(L, 2) : fpmin + yblock; - v3s16 pmax = lua_istable(L, 3) ? check_v3s16(L, 3) : fpmax - yblock; + v3s16 pmin = lua_istable(L, 2) ? check_v3s16(L, 2) : vm->m_area.MinEdge + yblock; + v3s16 pmax = lua_istable(L, 3) ? check_v3s16(L, 3) : vm->m_area.MaxEdge - yblock; bool propagate_shadow = !lua_isboolean(L, 4) || readParam(L, 4); sortBoxVerticies(pmin, pmax); if (!vm->m_area.contains(VoxelArea(pmin, pmax))) throw LuaError("Specified voxel area out of VoxelManipulator bounds"); - Mapgen mg; - mg.vm = vm; - mg.ndef = ndef; - mg.water_level = emerge->mgparams->water_level; - - mg.calcLighting(pmin, pmax, fpmin, fpmax, propagate_shadow); - - return 0; + return ModApiMapgen::calc_lighting(L, vm, pmin, pmax, propagate_shadow); } int LuaVoxelManip::l_set_lighting(lua_State *L) { - NO_MAP_LOCK_REQUIRED; - LuaVoxelManip *o = checkObject(L, 1); if (!o->is_mapgen_vm) { warningstream << "VoxelManip:set_lighting called for a non-mapgen " @@ -227,8 +198,7 @@ int LuaVoxelManip::l_set_lighting(lua_State *L) return 0; } - if (!lua_istable(L, 2)) - throw LuaError("VoxelManip:set_lighting called with missing parameter"); + luaL_checktype(L, 2, LUA_TTABLE); u8 light; light = (getintfield_default(L, 2, "day", 0) & 0x0F); @@ -244,12 +214,7 @@ int LuaVoxelManip::l_set_lighting(lua_State *L) if (!vm->m_area.contains(VoxelArea(pmin, pmax))) throw LuaError("Specified voxel area out of VoxelManipulator bounds"); - Mapgen mg; - mg.vm = vm; - - mg.setLighting(light, pmin, pmax); - - return 0; + return ModApiMapgen::set_lighting(L, vm, pmin, pmax, light); } int LuaVoxelManip::l_get_light_data(lua_State *L) diff --git a/src/script/scripting_server.cpp b/src/script/scripting_server.cpp index 644d5e3474a4a..b7114c7abeba7 100644 --- a/src/script/scripting_server.cpp +++ b/src/script/scripting_server.cpp @@ -174,6 +174,8 @@ void ServerScripting::InitializeModApi(lua_State *L, int top) void ServerScripting::InitializeAsync(lua_State *L, int top) { // classes + ItemStackMetaRef::Register(L); + LuaAreaStore::Register(L); LuaItemStack::Register(L); LuaPerlinNoise::Register(L); LuaPerlinNoiseMap::Register(L); diff --git a/src/server.cpp b/src/server.cpp index 2dca13cf027ce..3e11709eb2d2c 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -1621,8 +1621,9 @@ void Server::SendAddParticleSpawner(session_t peer_id, u16 protocol_version, ) / 4.0f * BS; const float radius_sq = radius * radius; /* Don't send short-lived spawners to distant players. - * This could be replaced with proper tracking at some point. */ - const bool distance_check = !attached_id && p.time <= 1.0f; + * This could be replaced with proper tracking at some point. + * A lifetime of 0 means that the spawner exists forever.*/ + const bool distance_check = !attached_id && p.time <= 1.0f && p.time != 0.0f; for (const session_t client_id : clients) { RemotePlayer *player = m_env->getPlayer(client_id); @@ -2513,7 +2514,7 @@ bool Server::addMediaFile(const std::string &filename, { // If name contains illegal characters, ignore the file if (!string_allowed(filename, TEXTURENAME_ALLOWED_CHARS)) { - infostream << "Server: ignoring illegal file name: \"" + warningstream << "Server: ignoring file as it has disallowed characters: \"" << filename << "\"" << std::endl; return false; } diff --git a/src/server/luaentity_sao.cpp b/src/server/luaentity_sao.cpp index 98f0b940085dd..df528293744a0 100644 --- a/src/server/luaentity_sao.cpp +++ b/src/server/luaentity_sao.cpp @@ -71,8 +71,12 @@ LuaEntitySAO::LuaEntitySAO(ServerEnvironment *env, v3f pos, const std::string &d break; } // create object - infostream << "LuaEntitySAO::create(name=\"" << name << "\" state=\"" - << state << "\")" << std::endl; + infostream << "LuaEntitySAO::create(name=\"" << name << "\" state is"; + if (state.empty()) + infostream << "empty"; + else + infostream << state.size() << " bytes"; + infostream << ")" << std::endl; m_init_name = name; m_init_state = state; @@ -103,7 +107,7 @@ void LuaEntitySAO::addedToEnvironment(u32 dtime_s) if(m_registered){ // Get properties m_env->getScriptIface()-> - luaentity_GetProperties(m_id, this, &m_prop); + luaentity_GetProperties(m_id, this, &m_prop, m_init_name); // Initialize HP from properties m_hp = m_prop.hp_max; // Activate entity, supplying serialized state @@ -281,7 +285,6 @@ std::string LuaEntitySAO::getClientInitializationData(u16 protocol_version) void LuaEntitySAO::getStaticData(std::string *result) const { - verbosestream<movement_speed_fast; // Fast speed - else - player_max_walk = m_player->movement_speed_walk; // Normal speed - player_max_walk *= m_player->physics_override.speed; + float speed_walk = m_player->movement_speed_walk * m_player->physics_override.speed; + float speed_fast = m_player->movement_speed_fast; + float speed_crouch = m_player->movement_speed_crouch * m_player->physics_override.speed_crouch; + + // Get permissible max. speed + if (m_privs.count("fast") != 0) { + // Fast priv: Get the highest speed of fast, walk or crouch + // (it is not forbidden the 'fast' speed is + // not actually the fastest) + player_max_walk = MYMAX(speed_crouch, speed_fast); + player_max_walk = MYMAX(player_max_walk, speed_walk); + } else { + // Get the highest speed of walk or crouch + // (it is not forbidden the 'walk' speed is + // lower than the crouch speed) + player_max_walk = MYMAX(speed_crouch, speed_walk); + } + player_max_walk = MYMAX(player_max_walk, override_max_H); player_max_jump = m_player->movement_speed_jump * m_player->physics_override.jump; // FIXME: Bouncy nodes cause practically unbound increase in Y speed, // until this can be verified correctly, tolerate higher jumping speeds player_max_jump *= 2.0; + player_max_jump = MYMAX(player_max_jump, m_player->movement_speed_climb * m_player->physics_override.speed_climb); player_max_jump = MYMAX(player_max_jump, override_max_V); // Don't divide by zero! diff --git a/src/serverenvironment.cpp b/src/serverenvironment.cpp index a2f721b4077bd..66c407c240db5 100644 --- a/src/serverenvironment.cpp +++ b/src/serverenvironment.cpp @@ -2162,10 +2162,6 @@ void ServerEnvironment::deactivateFarObjects(bool _force_delete) return false; } - verbosestream << "ServerEnvironment::deactivateFarObjects(): " - << "object id=" << id << " is not known by clients" - << "; deleting" << std::endl; - // Tell the object about removal obj->removingFromEnvironment(); // Deregister in scripting api diff --git a/src/unittest/test_keycode.cpp b/src/unittest/test_keycode.cpp index 3813af949e599..4a3b59ecd89e1 100644 --- a/src/unittest/test_keycode.cpp +++ b/src/unittest/test_keycode.cpp @@ -111,7 +111,7 @@ void TestKeycode::testCompare() { // Basic comparison UASSERT(KeyPress("5") == KeyPress("KEY_KEY_5")); - UASSERT(!(KeyPress("5") == KeyPress("KEY_NUMPAD_5"))); + UASSERT(!(KeyPress("5") == KeyPress("KEY_NUMPAD5"))); // Matching char suffices // note: This is a real-world example, Irrlicht maps XK_equal to irr::KEY_PLUS on Linux @@ -126,4 +126,11 @@ void TestKeycode::testCompare() in.Char = L'\0'; in2.Char = L';'; UASSERT(KeyPress(in) == KeyPress(in2)); + + // Irrlicht sets chars to the according digit for numpad keys. + // We need to distinguish them in order to bind numpad keys. + irr::SEvent::SKeyInput in3; + in3.Key = irr::KEY_NUMPAD5; + in3.Char = L'5'; + UASSERT(!(KeyPress("5") == KeyPress(in3))); } diff --git a/textures/base/pack/settings_btn.png b/textures/base/pack/settings_btn.png new file mode 100644 index 0000000000000..a325f4032ffd7 Binary files /dev/null and b/textures/base/pack/settings_btn.png differ