1
0
Fork 0
mirror of https://github.com/luanti-org/luanti.git synced 2025-06-27 16:36:03 +00:00

Merge remote-tracking branch 'upstream/master' into Visuals-Vol-2

This commit is contained in:
Gefüllte Taubenbrust 2024-10-11 20:44:12 +02:00
commit b6c099073f
183 changed files with 3919 additions and 1642 deletions

2
.gitattributes vendored
View file

@ -3,3 +3,5 @@
*.cpp diff=cpp
*.h diff=cpp
*.gltf binary

View file

@ -88,7 +88,7 @@ jobs:
- name: Install deps
run: |
source ./util/ci/common.sh
install_linux_deps clang-7 llvm
install_linux_deps clang-7 llvm-7
- name: Build
run: |
@ -102,6 +102,11 @@ jobs:
run: |
./bin/minetest --run-unittests
# Do this here because we have ASan and error paths are sensitive to dangling pointers
- name: Test error cases
run: |
./util/test_error_cases.sh
# Current clang version
clang_18:
runs-on: ubuntu-24.04

View file

@ -29,8 +29,8 @@ on:
jobs:
build:
# use lowest possible macOS running on x86_64 to support more users
runs-on: macos-12
# use lowest possible macOS running on x86_64 supported by brew to support more users
runs-on: macos-13
steps:
- uses: actions/checkout@v4
- name: Install deps

3
.gitignore vendored
View file

@ -26,6 +26,7 @@ tags
!tags/
gtags.files
.idea
.qtcreator/
# Codelite
*.project
# Visual Studio Code & plugins
@ -109,6 +110,8 @@ src/cmake_config_githash.h
*.layout
*.o
*.a
*.dump
*.dmp
*.ninja
.ninja*
*.gch

View file

@ -57,12 +57,10 @@ srifqi:
textures/base/pack/minimap_btn.png
Zughy:
textures/base/pack/cdb_add.png
textures/base/pack/cdb_downloading.png
textures/base/pack/cdb_queued.png
textures/base/pack/cdb_update.png
textures/base/pack/cdb_update_cropped.png
textures/base/pack/cdb_viewonline.png
textures/base/pack/settings_btn.png
textures/base/pack/settings_info.png
textures/base/pack/settings_reset.png
@ -79,7 +77,6 @@ kilbith:
textures/base/pack/progress_bar_bg.png
SmallJoker:
textures/base/pack/cdb_clear.png
textures/base/pack/server_favorite_delete.png (based on server_favorite.png)
DS:

View file

@ -166,20 +166,19 @@ function core.is_colored_paramtype(ptype)
end
function core.strip_param2_color(param2, paramtype2)
if not core.is_colored_paramtype(paramtype2) then
if paramtype2 == "color" then
return param2
elseif paramtype2 == "colorfacedir" then
return math.floor(param2 / 32) * 32
elseif paramtype2 == "color4dir" then
return math.floor(param2 / 4) * 4
elseif paramtype2 == "colorwallmounted" then
return math.floor(param2 / 8) * 8
elseif paramtype2 == "colordegrotate" then
return math.floor(param2 / 32) * 32
else
return nil
end
if paramtype2 == "colorfacedir" then
param2 = math.floor(param2 / 32) * 32
elseif paramtype2 == "color4dir" then
param2 = math.floor(param2 / 4) * 4
elseif paramtype2 == "colorwallmounted" then
param2 = math.floor(param2 / 8) * 8
elseif paramtype2 == "colordegrotate" then
param2 = math.floor(param2 / 32) * 32
end
-- paramtype2 == "color" requires no modification.
return param2
end
-- Content ID caching

View file

@ -235,6 +235,16 @@ function core.formspec_escape(text)
end
local hypertext_escapes = {
["\\"] = "\\\\",
["<"] = "\\<",
[">"] = "\\>",
}
function core.hypertext_escape(text)
return text and text:gsub("[\\<>]", hypertext_escapes)
end
function core.wrap_text(text, max_length, as_table)
local result = {}
local line = {}

View file

@ -66,13 +66,13 @@ local function get_formspec(self)
local content, prepend = tab.get_formspec(self, tab.name, tab.tabdata, tab.tabsize)
local ENABLE_TOUCH = core.settings:get_bool("enable_touch")
local TOUCH_GUI = core.settings:get_bool("touch_gui")
local orig_tsize = tab.tabsize or { width = self.width, height = self.height }
local tsize = { width = orig_tsize.width, height = orig_tsize.height }
tsize.height = tsize.height
+ TABHEADER_H -- tabheader included in formspec size
+ (ENABLE_TOUCH and GAMEBAR_OFFSET_TOUCH or GAMEBAR_OFFSET_DESKTOP)
+ (TOUCH_GUI and GAMEBAR_OFFSET_TOUCH or GAMEBAR_OFFSET_DESKTOP)
+ GAMEBAR_H -- gamebar included in formspec size
if self.parent == nil and not prepend then

View file

@ -44,6 +44,7 @@ core.features = {
override_item_remove_fields = true,
hotbar_hud_element = true,
bulk_lbms = true,
abm_without_neighbors = true,
}
function core.has_feature(arg)

View file

@ -182,6 +182,23 @@ function contentdb.get_package_by_id(id)
end
function contentdb.calculate_package_id(type, author, name)
local id = author:lower() .. "/"
if (type == nil or type == "game") and #name > 5 and name:sub(#name - 4) == "_game" then
id = id .. name:sub(1, #name - 5)
else
id = id .. name
end
return id
end
function contentdb.get_package_by_info(author, name)
local id = contentdb.calculate_package_id(nil, author, name)
return contentdb.package_by_id[id]
end
-- Create a coroutine from `fn` and provide results to `callback` when complete (dead).
-- Returns a resumer function.
local function make_callback_coroutine(fn, callback)
@ -415,15 +432,7 @@ local function fetch_pkgs(params)
local aliases = {}
for _, package in pairs(packages) do
local name_len = #package.name
-- This must match what contentdb.update_paths() does!
package.id = package.author:lower() .. "/"
if package.type == "game" and name_len > 5 and package.name:sub(name_len - 4) == "_game" then
package.id = package.id .. package.name:sub(1, name_len - 5)
else
package.id = package.id .. package.name
end
package.id = params.calculate_package_id(package.type, package.author, package.name)
package.url_part = core.urlencode(package.author) .. "/" .. core.urlencode(package.name)
if package.aliases then
@ -443,7 +452,7 @@ end
function contentdb.fetch_pkgs(callback)
contentdb.loading = true
core.handle_async(fetch_pkgs, nil, function(result)
core.handle_async(fetch_pkgs, { calculate_package_id = contentdb.calculate_package_id }, function(result)
if result then
contentdb.load_ok = true
contentdb.load_error = false
@ -581,3 +590,78 @@ function contentdb.filter_packages(query, by_type)
end
end
end
function contentdb.get_full_package_info(package, callback)
assert(package)
if package.full_info then
callback(package.full_info)
return
end
local function fetch(params)
local version = core.get_version()
local base_url = core.settings:get("contentdb_url")
local languages
local current_language = core.get_language()
if current_language ~= "" then
languages = { current_language, "en;q=0.8" }
else
languages = { "en" }
end
local url = base_url ..
"/api/packages/" .. params.package.url_part .. "/for-client/?" ..
"protocol_version=" .. core.urlencode(core.get_max_supp_proto()) ..
"&engine_version=" .. core.urlencode(version.string) ..
"&formspec_version=" .. core.urlencode(core.get_formspec_version()) ..
"&include_images=false"
local http = core.get_http_api()
local response = http.fetch_sync({
url = url,
extra_headers = {
"Accept-Language: " .. table.concat(languages, ", ")
},
})
if not response.succeeded then
return nil
end
return core.parse_json(response.data)
end
local function my_callback(value)
package.full_info = value
callback(value)
end
if not core.handle_async(fetch, { package = package }, my_callback) then
core.log("error", "ERROR: async event failed")
callback(nil)
end
end
function contentdb.get_formspec_padding()
-- Padding is increased on Android to account for notches
-- TODO: use Android API to determine size of cut outs
return { x = PLATFORM == "Android" and 1 or 0.5, y = PLATFORM == "Android" and 0.25 or 0.5 }
end
function contentdb.get_formspec_size()
local window = core.get_window_info()
local size = { x = window.max_formspec_size.x, y = window.max_formspec_size.y }
-- Minimum formspec size
local min_x = 15.5
local min_y = 10
if size.x < min_x or size.y < min_y then
local scale = math.max(min_x / size.x, min_y / size.y)
size.x = size.x * scale
size.y = size.y * scale
end
return size
end

View file

@ -26,68 +26,20 @@ end
-- Filter
local search_string = ""
local cur_page = 1
local num_per_page = 5
local filter_type = 1
local filter_types_titles = {
fgettext("All packages"),
fgettext("Games"),
fgettext("Mods"),
fgettext("Texture packs"),
}
local filter_type
-- Automatic package installation
local auto_install_spec = nil
local filter_types_type = {
nil,
"game",
"mod",
"txp",
local filter_type_names = {
{ "type_all", nil },
{ "type_game", "game" },
{ "type_mod", "mod" },
{ "type_txp", "txp" },
}
local function install_or_update_package(this, package)
local install_parent
if package.type == "mod" then
install_parent = core.get_modpath()
elseif package.type == "game" then
install_parent = core.get_gamepath()
elseif package.type == "txp" then
install_parent = core.get_texturepath()
else
error("Unknown package type: " .. package.type)
end
if package.queued or package.downloading then
return
end
local function on_confirm()
local dlg = create_install_dialog(package)
dlg:set_parent(this)
this:hide()
dlg:show()
dlg:load_deps()
end
if package.type == "mod" and #pkgmgr.games == 0 then
local dlg = messagebox("install_game",
fgettext("You need to install a game before you can install a mod"))
dlg:set_parent(this)
this:hide()
dlg:show()
elseif not package.path and core.is_dir(install_parent .. DIR_DELIM .. package.name) then
local dlg = create_confirm_overwrite(package, on_confirm)
dlg:set_parent(this)
this:hide()
dlg:show()
else
on_confirm()
end
end
-- Resolves the package specification stored in auto_install_spec into an actual package.
-- May only be called after the package list has been loaded successfully.
local function resolve_auto_install_spec()
@ -145,7 +97,7 @@ end
local function sort_and_filter_pkgs()
contentdb.update_paths()
contentdb.sort_packages()
contentdb.filter_packages(search_string, filter_types_type[filter_type])
contentdb.filter_packages(search_string, filter_type)
local auto_install_pkg = resolve_auto_install_spec()
if auto_install_pkg then
@ -176,72 +128,151 @@ local function load()
end
local function get_info_formspec(text)
local H = 9.5
local function get_info_formspec(size, padding, text)
return table.concat({
"formspec_version[6]",
"size[15.75,9.5]",
core.settings:get_bool("touch_gui") and "padding[0.01,0.01]" or "position[0.5,0.55]",
"size[", size.x, ",", size.y, "]",
"padding[0,0]",
"bgcolor[;true]",
"label[4,4.35;", text, "]",
"container[0,", H - 0.8 - 0.375, "]",
"button[0.375,0;5,0.8;back;", fgettext("Back to Main Menu"), "]",
"label[", padding.x + 3.625, ",4.35;", text, "]",
"container[", padding.x, ",", size.y - 0.8 - padding.y, "]",
"button[0,0;2,0.8;back;", fgettext("Back"), "]",
"container_end[]",
})
end
-- Determines how to fit `num_per_page` into `size` space
local function fit_cells(num_per_page, size)
local cell_spacing = 0.5
local columns = 1
local cell_w, cell_h
-- Fit cells into the available height
while true do
cell_w = (size.x - (columns-1)*cell_spacing) / columns
cell_h = cell_w / 4
local required_height = math.ceil(num_per_page / columns) * (cell_h + cell_spacing) - cell_spacing
-- Add 0.1 to be more lenient
if required_height <= size.y + 0.1 then
break
end
columns = columns + 1
end
return cell_spacing, columns, cell_w, cell_h
end
local function calculate_num_per_page()
local size = contentdb.get_formspec_size()
local padding = contentdb.get_formspec_padding()
local window = core.get_window_info()
size.x = size.x - padding.x * 2
size.y = size.y - padding.y * 2 - 1.425 - 0.25 - 0.8
local coordToPx = window.size.x / window.max_formspec_size.x / window.real_gui_scaling
local num_per_page = 12
while num_per_page > 2 do
local _, _, cell_w, _ = fit_cells(num_per_page, size)
if cell_w * coordToPx > 350 then
break
end
num_per_page = num_per_page - 1
end
return num_per_page
end
local function get_formspec(dlgdata)
local window_padding = contentdb.get_formspec_padding()
local size = contentdb.get_formspec_size()
if contentdb.loading then
return get_info_formspec(fgettext("Loading..."))
return get_info_formspec(size, window_padding, fgettext("Loading..."))
end
if contentdb.load_error then
return get_info_formspec(fgettext("No packages could be retrieved"))
return get_info_formspec(size, window_padding, fgettext("No packages could be retrieved"))
end
assert(contentdb.load_ok)
contentdb.update_paths()
local num_per_page = dlgdata.num_per_page
dlgdata.pagemax = math.max(math.ceil(#contentdb.packages / num_per_page), 1)
if cur_page > dlgdata.pagemax then
cur_page = 1
end
local W = 15.75
local H = 9.5
local W = size.x - window_padding.x * 2
local H = size.y - window_padding.y * 2
local category_x = 0
local number_category_buttons = 4
local max_button_w = (W - 0.375 - 0.25 - 7) / number_category_buttons
local category_button_w = math.min(max_button_w, 3)
local function make_category_button(name, label, selected)
category_x = category_x + 1
local color = selected and mt_color_green or ""
return ("style[%s;bgcolor=%s]button[%f,0;%f,0.8;%s;%s]"):format(name, color,
(category_x - 1) * category_button_w, category_button_w, name, label)
end
local selected_type = filter_type
local search_box_width = W - 0.375 - 0.25 - 2*0.8
- number_category_buttons * category_button_w
local formspec = {
"formspec_version[6]",
"size[15.75,9.5]",
core.settings:get_bool("touch_gui") and "padding[0.01,0.01]" or "position[0.5,0.55]",
"formspec_version[7]",
"size[", size.x, ",", size.y, "]",
"padding[0,0]",
"bgcolor[;true]",
"style[status,downloading,queued;border=false]",
"container[", window_padding.x, ",", window_padding.y, "]",
"container[0.375,0.375]",
"field[0,0;7.225,0.8;search_string;;", core.formspec_escape(search_string), "]",
-- Top-left: categories
make_category_button("type_all", fgettext("All"), selected_type == nil),
make_category_button("type_game", fgettext("Games"), selected_type == "game"),
make_category_button("type_mod", fgettext("Mods"), selected_type == "mod"),
make_category_button("type_txp", fgettext("Texture Packs"), selected_type == "txp"),
-- Top-right: Search
"container[", W - search_box_width - 0.8*2, ",0]",
"field[0,0;", search_box_width, ",0.8;search_string;;", core.formspec_escape(search_string), "]",
"field_enter_after_edit[search_string;true]",
"image_button[7.3,0;0.8,0.8;", core.formspec_escape(defaulttexturedir .. "search.png"), ";search;]",
"image_button[8.125,0;0.8,0.8;", core.formspec_escape(defaulttexturedir .. "clear.png"), ";clear;]",
"dropdown[9.175,0;2.7875,0.8;type;", table.concat(filter_types_titles, ","), ";", filter_type, "]",
"image_button[", search_box_width, ",0;0.8,0.8;",
core.formspec_escape(defaulttexturedir .. "search.png"), ";search;]",
"image_button[", search_box_width + 0.8, ",0;0.8,0.8;",
core.formspec_escape(defaulttexturedir .. "clear.png"), ";clear;]",
"container_end[]",
-- Page nav buttons
"container[0,", H - 0.8 - 0.375, "]",
"button[0.375,0;5,0.8;back;", fgettext("Back to Main Menu"), "]",
-- Bottom strip start
"container[0,", H - 0.8, "]",
"button[0,0;2,0.8;back;", fgettext("Back"), "]",
"container[", W - 0.375 - 0.8*4 - 2, ",0]",
"image_button[0,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "start_icon.png;pstart;]",
"image_button[0.8,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "prev_icon.png;pback;]",
-- Bottom-center: Page nav buttons
"container[", (W - 1*4 - 2) / 2, ",0]",
"image_button[0,0;1,0.8;", core.formspec_escape(defaulttexturedir), "start_icon.png;pstart;]",
"image_button[1,0;1,0.8;", core.formspec_escape(defaulttexturedir), "prev_icon.png;pback;]",
"style[pagenum;border=false]",
"button[1.6,0;2,0.8;pagenum;", tonumber(cur_page), " / ", tonumber(dlgdata.pagemax), "]",
"image_button[3.6,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "next_icon.png;pnext;]",
"image_button[4.4,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "end_icon.png;pend;]",
"container_end[]",
"button[2,0;2,0.8;pagenum;", tonumber(cur_page), " / ", tonumber(dlgdata.pagemax), "]",
"image_button[4,0;1,0.8;", core.formspec_escape(defaulttexturedir), "next_icon.png;pnext;]",
"image_button[5,0;1,0.8;", core.formspec_escape(defaulttexturedir), "end_icon.png;pend;]",
"container_end[]", -- page nav end
"container_end[]",
-- Bottom-right: updating
"container[", W - 3, ",0]",
"style[status,downloading,queued;border=false]",
}
if contentdb.number_downloading > 0 then
formspec[#formspec + 1] = "button[12.5875,0.375;2.7875,0.8;downloading;"
formspec[#formspec + 1] = "button[0,0;3,0.8;downloading;"
if #contentdb.download_queue > 0 then
formspec[#formspec + 1] = fgettext("$1 downloading,\n$2 queued",
contentdb.number_downloading, #contentdb.download_queue)
@ -260,16 +291,19 @@ local function get_formspec(dlgdata)
end
if num_avail_updates == 0 then
formspec[#formspec + 1] = "button[12.5875,0.375;2.7875,0.8;status;"
formspec[#formspec + 1] = "button[0,0;3,0.8;status;"
formspec[#formspec + 1] = fgettext("No updates")
formspec[#formspec + 1] = "]"
else
formspec[#formspec + 1] = "button[12.5875,0.375;2.7875,0.8;update_all;"
formspec[#formspec + 1] = "button[0,0;3,0.8;update_all;"
formspec[#formspec + 1] = fgettext("Update All [$1]", num_avail_updates)
formspec[#formspec + 1] = "]"
end
end
formspec[#formspec + 1] = "container_end[]" -- updating end
formspec[#formspec + 1] = "container_end[]" -- bottom strip end
if #contentdb.packages == 0 then
formspec[#formspec + 1] = "label[4,4.75;"
formspec[#formspec + 1] = fgettext("No results")
@ -281,80 +315,84 @@ local function get_formspec(dlgdata)
formspec[#formspec + 1] = "tooltip[downloading;" .. fgettext("Downloading...") .. tooltip_colors
formspec[#formspec + 1] = "tooltip[queued;" .. fgettext("Queued") .. tooltip_colors
formspec[#formspec + 1] = "container[0,1.425]"
local cell_spacing, columns, cell_w, cell_h = fit_cells(num_per_page, {
x = W,
y = H - 1.425 - 0.25 - 0.8
})
local img_w = cell_h * 3 / 2
local start_idx = (cur_page - 1) * num_per_page + 1
for i=start_idx, math.min(#contentdb.packages, start_idx+num_per_page-1) do
local package = contentdb.packages[i]
local container_y = (i - start_idx) * 1.375 + (2*0.375 + 0.8)
formspec[#formspec + 1] = "container[0.375,"
formspec[#formspec + 1] = container_y
formspec[#formspec + 1] = "]"
-- image
formspec[#formspec + 1] = "image[0,0;1.5,1;"
formspec[#formspec + 1] = core.formspec_escape(get_screenshot(package))
formspec[#formspec + 1] = "]"
table.insert_all(formspec, {
"container[",
(cell_w + cell_spacing) * ((i - start_idx) % columns),
",",
(cell_h + cell_spacing) * math.floor((i - start_idx) / columns),
"]",
-- title
formspec[#formspec + 1] = "label[1.875,0.1;"
formspec[#formspec + 1] = core.formspec_escape(
"box[0,0;", cell_w, ",", cell_h, ";#ffffff11]",
-- image,
"image[0,0;", img_w, ",", cell_h, ";",
core.formspec_escape(get_screenshot(package, package.thumbnail, 2)), "]",
"label[", img_w + 0.25 + 0.05, ",0.5;",
core.formspec_escape(
core.colorize(mt_color_green, package.title) ..
core.colorize("#BFBFBF", " by " .. package.author))
formspec[#formspec + 1] = "]"
core.colorize("#BFBFBF", " by " .. package.author)), "]",
-- buttons
local description_width = W - 2.625 - 2 * 0.7 - 2 * 0.15
"textarea[", img_w + 0.25, ",0.75;", cell_w - img_w - 0.25, ",", cell_h - 0.75, ";;;",
core.formspec_escape(package.short_description), "]",
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]"
"style[view_", i, ";border=false]",
"style[view_", i, ":hovered;bgimg=", core.formspec_escape(defaulttexturedir .. "button_hover_semitrans.png"), "]",
"style[view_", i, ":pressed;bgimg=", core.formspec_escape(defaulttexturedir .. "button_press_semitrans.png"), "]",
"button[0,0;", cell_w, ",", cell_h, ";view_", i, ";]",
})
if package.featured then
table.insert_all(formspec, {
"tooltip[0,0;0.8,0.8;", fgettext("Featured"), "]",
"image[0.2,0.2;0.4,0.4;", defaulttexturedir, "server_favorite.png]",
})
end
table.insert_all(formspec, {
"container[", cell_w - 0.625,",", 0.25, "]",
})
if package.downloading then
formspec[#formspec + 1] = "animated_image[-1.7,-0.15;1,1;downloading;"
formspec[#formspec + 1] = core.formspec_escape(defaulttexturedir)
formspec[#formspec + 1] = "cdb_downloading.png;3;400;]"
table.insert_all(formspec, {
"animated_image[0,0;0.5,0.5;downloading;", defaulttexturedir, "cdb_downloading.png;3;400;;]",
})
elseif package.queued then
formspec[#formspec + 1] = second_base
formspec[#formspec + 1] = "cdb_queued.png;queued;]"
elseif not package.path then
local elem_name = "install_" .. i .. ";"
formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#71aa34]"
formspec[#formspec + 1] = second_base .. "cdb_add.png;" .. elem_name .. "]"
formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Install") .. tooltip_colors
else
table.insert_all(formspec, {
"image[0,0;0.5,0.5;", defaulttexturedir, "cdb_queued.png]",
})
elseif package.path then
if package.installed_release < package.release then
-- The install_ action also handles updating
local elem_name = "install_" .. i .. ";"
formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#28ccdf]"
formspec[#formspec + 1] = third_base .. "cdb_update.png;" .. elem_name .. "]"
formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Update") .. tooltip_colors
description_width = description_width - 0.7 - 0.15
table.insert_all(formspec, {
"image[0,0;0.5,0.5;", defaulttexturedir, "cdb_update.png]",
})
else
table.insert_all(formspec, {
"image[0.1,0.1;0.3,0.3;", defaulttexturedir, "checkbox_64.png]",
})
end
end
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
table.insert_all(formspec, {
"container_end[]",
"container_end[]",
})
end
local web_elem_name = "view_" .. i .. ";"
formspec[#formspec + 1] = "image_button[-0.7,0;0.7,0.7;" ..
core.formspec_escape(defaulttexturedir) .. "cdb_viewonline.png;" .. web_elem_name .. "]"
formspec[#formspec + 1] = "tooltip[" .. web_elem_name ..
fgettext("View more information in a web browser") .. tooltip_colors
formspec[#formspec + 1] = "container_end[]"
-- description
formspec[#formspec + 1] = "textarea[1.855,0.3;"
formspec[#formspec + 1] = tostring(description_width)
formspec[#formspec + 1] = ",0.8;;;"
formspec[#formspec + 1] = core.formspec_escape(package.short_description)
formspec[#formspec + 1] = "]"
formspec[#formspec + 1] = "container_end[]"
end
formspec[#formspec + 1] = "container_end[]"
return table.concat(formspec)
end
@ -364,14 +402,14 @@ local function handle_submit(this, fields)
if fields.search or fields.key_enter_field == "search_string" then
search_string = fields.search_string:trim()
cur_page = 1
contentdb.filter_packages(search_string, filter_types_type[filter_type])
contentdb.filter_packages(search_string, filter_type)
return true
end
if fields.clear then
search_string = ""
cur_page = 1
contentdb.filter_packages("", filter_types_type[filter_type])
contentdb.filter_packages("", filter_type)
return true
end
@ -407,12 +445,11 @@ local function handle_submit(this, fields)
return true
end
if fields.type then
local new_type = table.indexof(filter_types_titles, fields.type)
if new_type ~= filter_type then
filter_type = new_type
for _, pair in ipairs(filter_type_names) do
if fields[pair[1]] then
filter_type = pair[2]
cur_page = 1
contentdb.filter_packages(search_string, filter_types_type[filter_type])
contentdb.filter_packages(search_string, filter_type)
return true
end
end
@ -428,32 +465,20 @@ local function handle_submit(this, fields)
return true
end
local num_per_page = this.data.num_per_page
local start_idx = (cur_page - 1) * num_per_page + 1
assert(start_idx ~= nil)
for i=start_idx, math.min(#contentdb.packages, start_idx+num_per_page-1) do
local package = contentdb.packages[i]
assert(package)
if fields["install_" .. i] then
install_or_update_package(this, package)
return true
end
if fields["uninstall_" .. i] then
local dlg = create_delete_content_dlg(package)
if fields["view_" .. i] or fields["title_" .. i] or fields["author_" .. i] then
local dlg = create_package_dialog(package)
dlg:set_parent(this)
this:hide()
dlg:show()
return true
end
if fields["view_" .. i] then
local url = ("%s/packages/%s?protocol_version=%d"):format(
core.settings:get("contentdb_url"), package.url_part,
core.get_max_supp_proto())
core.open_url(url)
return true
end
end
return false
@ -462,8 +487,8 @@ end
local function handle_events(event)
if event == "DialogShow" then
-- On touchscreen, don't show the "MINETEST" header behind the dialog.
mm_game_theme.set_engine(core.settings:get_bool("touch_gui"))
-- Don't show the "MINETEST" header behind the dialog.
mm_game_theme.set_engine(true)
-- If ContentDB is already loaded, auto-install packages here.
do_auto_install()
@ -471,6 +496,11 @@ local function handle_events(event)
return true
end
if event == "WindowInfoChange" then
ui.update()
return true
end
return false
end
@ -485,17 +515,7 @@ end
function create_contentdb_dlg(type, install_spec)
search_string = ""
cur_page = 1
if type then
-- table.indexof does not work on tables that contain `nil`
for i, v in pairs(filter_types_type) do
if v == type then
filter_type = i
break
end
end
else
filter_type = 1
end
filter_type = type
-- Keep the old auto_install_spec if the caller doesn't specify one.
if install_spec then
@ -504,8 +524,10 @@ function create_contentdb_dlg(type, install_spec)
load()
return dialog_create("contentdb",
local dlg = dialog_create("contentdb",
get_formspec,
handle_submit,
handle_events)
dlg.data.num_per_page = calculate_num_per_page()
return dlg
end

View file

@ -244,3 +244,45 @@ function create_install_dialog(package)
return dlg
end
function install_or_update_package(parent, package)
local install_parent
if package.type == "mod" then
install_parent = core.get_modpath()
elseif package.type == "game" then
install_parent = core.get_gamepath()
elseif package.type == "txp" then
install_parent = core.get_texturepath()
else
error("Unknown package type: " .. package.type)
end
if package.queued or package.downloading then
return
end
local function on_confirm()
local dlg = create_install_dialog(package)
dlg:set_parent(parent)
parent:hide()
dlg:show()
dlg:load_deps()
end
if package.type == "mod" and #pkgmgr.games == 0 then
local dlg = messagebox("install_game",
fgettext("You need to install a game before you can install a mod"))
dlg:set_parent(parent)
parent:hide()
dlg:show()
elseif not package.path and core.is_dir(install_parent .. DIR_DELIM .. package.name) then
local dlg = create_confirm_overwrite(package, on_confirm)
dlg:set_parent(parent)
parent:hide()
dlg:show()
else
on_confirm()
end
end

View file

@ -0,0 +1,333 @@
--Minetest
--Copyright (C) 2018-24 rubenwardy
--
--This program is free software; you can redistribute it and/or modify
--it under the terms of the GNU Lesser General Public License as published by
--the Free Software Foundation; either version 2.1 of the License, or
--(at your option) any later version.
--
--This program is distributed in the hope that it will be useful,
--but WITHOUT ANY WARRANTY; without even the implied warranty of
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--GNU Lesser General Public License for more details.
--
--You should have received a copy of the GNU Lesser General Public License along
--with this program; if not, write to the Free Software Foundation, Inc.,
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
local function get_info_formspec(size, padding, text)
return table.concat({
"formspec_version[6]",
"size[", size.x, ",", size.y, "]",
"padding[0,0]",
"bgcolor[;true]",
"label[4,4.35;", text, "]",
"container[", padding.x, ",", size.y - 0.8 - padding.y, "]",
"button[0,0;2,0.8;back;", fgettext("Back"), "]",
"container_end[]",
})
end
local function get_formspec(data)
local window_padding = contentdb.get_formspec_padding()
local size = contentdb.get_formspec_size()
size.x = math.min(size.x, 20)
local W = size.x - window_padding.x * 2
local H = size.y - window_padding.y * 2
if not data.info then
if not data.loading and not data.loading_error then
data.loading = true
contentdb.get_full_package_info(data.package, function(info)
data.loading = false
if info == nil then
data.loading_error = true
ui.update()
return
end
if info.forums then
info.forums = "https://forum.minetest.net/viewtopic.php?t=" .. info.forums
end
assert(data.package.name == info.name)
data.info = info
ui.update()
end)
end
-- get_full_package_info can return cached info immediately, so
-- check to see if that happened
if not data.info then
if data.loading_error then
return get_info_formspec(size, window_padding, fgettext("No packages could be retrieved"))
end
return get_info_formspec(size, window_padding, fgettext("Loading..."))
end
end
-- Check installation status
contentdb.update_paths()
local info = data.info
local info_line =
fgettext("by $1 — $2 downloads — +$3 / $4 / -$5",
info.author, info.downloads,
info.reviews.positive, info.reviews.neutral, info.reviews.negative)
local bottom_buttons_y = H - 0.8
local formspec = {
"formspec_version[7]",
"size[", size.x, ",", size.y, "]",
"padding[0,0]",
"bgcolor[;true]",
"container[", window_padding.x, ",", window_padding.y, "]",
"button[0,", bottom_buttons_y, ";2,0.8;back;", fgettext("Back"), "]",
"button[", W - 3, ",", bottom_buttons_y, ";3,0.8;open_contentdb;", fgettext("ContentDB page"), "]",
"style_type[label;font_size=+24;font=bold]",
"label[0,0.4;", core.formspec_escape(info.title), "]",
"style_type[label;font_size=;font=]",
"label[0,1.2;", core.formspec_escape(info_line), "]",
}
table.insert_all(formspec, {
"container[", W - 6, ",0]"
})
local left_button_rect = "0,0;2.875,1"
local right_button_rect = "3.125,0;2.875,1"
if data.package.downloading then
formspec[#formspec + 1] = "animated_image[5,0;1,1;downloading;"
formspec[#formspec + 1] = core.formspec_escape(defaulttexturedir)
formspec[#formspec + 1] = "cdb_downloading.png;3;400;]"
elseif data.package.queued then
formspec[#formspec + 1] = "style[queued;border=false]"
formspec[#formspec + 1] = "image_button[5,0;1,1;" .. core.formspec_escape(defaulttexturedir)
formspec[#formspec + 1] = "cdb_queued.png;queued;]"
elseif not data.package.path then
formspec[#formspec + 1] = "style[install;bgcolor=green]"
formspec[#formspec + 1] = "button["
formspec[#formspec + 1] = right_button_rect
formspec[#formspec + 1] =";install;"
formspec[#formspec + 1] = fgettext("Install [$1]", info.download_size)
formspec[#formspec + 1] = "]"
else
if data.package.installed_release < data.package.release then
-- The install_ action also handles updating
formspec[#formspec + 1] = "style[install;bgcolor=#28ccdf]"
formspec[#formspec + 1] = "button["
formspec[#formspec + 1] = left_button_rect
formspec[#formspec + 1] = ";install;"
formspec[#formspec + 1] = fgettext("Update")
formspec[#formspec + 1] = "]"
end
formspec[#formspec + 1] = "style[uninstall;bgcolor=#a93b3b]"
formspec[#formspec + 1] = "button["
formspec[#formspec + 1] = right_button_rect
formspec[#formspec + 1] = ";uninstall;"
formspec[#formspec + 1] = fgettext("Uninstall")
formspec[#formspec + 1] = "]"
end
local current_tab = data.current_tab or 1
local tab_titles = {
fgettext("Description"),
fgettext("Information"),
}
local tab_body_height = bottom_buttons_y - 2.8
table.insert_all(formspec, {
"container_end[]",
"box[0,2.55;", W, ",", tab_body_height, ";#ffffff11]",
"tabheader[0,2.55;", W, ",0.8;tabs;",
table.concat(tab_titles, ","), ";", current_tab, ";true;true]",
"container[0,2.8]",
})
if current_tab == 1 then
-- Screenshots and description
local hypertext = "<big><b>" .. core.hypertext_escape(info.short_description) .. "</b></big>\n"
local winfo = core.get_window_info()
local fs_to_px = winfo.size.x / winfo.max_formspec_size.x
for i, ss in ipairs(info.screenshots) do
local path = get_screenshot(data.package, ss.url, 2)
hypertext = hypertext .. "<action name=\"ss_" .. i .. "\"><img name=\"" ..
core.hypertext_escape(path) .. "\" width=" .. (3 * fs_to_px) ..
" height=" .. (2 * fs_to_px) .. "></action>"
if i ~= #info.screenshots then
hypertext = hypertext .. "<img name=\"blank.png\" width=" .. (0.25 * fs_to_px) ..
" height=" .. (2.25 * fs_to_px).. ">"
end
end
hypertext = hypertext .. "\n" .. info.long_description.head
local first = true
local function add_link_button(label, name)
if info[name] then
if not first then
hypertext = hypertext .. " | "
end
hypertext = hypertext .. "<action name=link_" .. name .. ">" .. core.hypertext_escape(label) .. "</action>"
info.long_description.links["link_" .. name] = info[name]
first = false
end
end
add_link_button(fgettext("Donate"), "donate_url")
add_link_button(fgettext("Website"), "website")
add_link_button(fgettext("Source"), "repo")
add_link_button(fgettext("Issue Tracker"), "issue_tracker")
add_link_button(fgettext("Translate"), "translation_url")
add_link_button(fgettext("Forum Topic"), "forums")
hypertext = hypertext .. "\n\n" .. info.long_description.body
hypertext = hypertext:gsub("<img name=\"?blank.png\"? ",
"<img name=\"" .. core.hypertext_escape(defaulttexturedir) .. "blank.png\" ")
table.insert_all(formspec, {
"hypertext[0,0;", W, ",", tab_body_height - 0.375,
";desc;", core.formspec_escape(hypertext), "]",
})
elseif current_tab == 2 then
local hypertext = info.info_hypertext.head .. info.info_hypertext.body
table.insert_all(formspec, {
"hypertext[0,0;", W, ",", tab_body_height - 0.375,
";info;", core.formspec_escape(hypertext), "]",
})
else
error("Unknown tab " .. current_tab)
end
formspec[#formspec + 1] = "container_end[]"
formspec[#formspec + 1] = "container_end[]"
return table.concat(formspec)
end
local function handle_hypertext_event(this, event, hypertext_object)
if not (event and event:sub(1, 7) == "action:") then
return
end
for i, ss in ipairs(this.data.info.screenshots) do
if event == "action:ss_" .. i then
core.open_url(ss.url)
return true
end
end
local base_url = core.settings:get("contentdb_url"):gsub("(%W)", "%%%1")
for key, url in pairs(hypertext_object.links) do
if event == "action:" .. key then
local author, name = url:match("^" .. base_url .. "/?packages/([A-Za-z0-9 _-]+)/([a-z0-9_]+)/?$")
if author and name then
local package2 = contentdb.get_package_by_info(author, name)
if package2 then
local dlg = create_package_dialog(package2)
dlg:set_parent(this)
this:hide()
dlg:show()
return true
end
end
core.open_url_dialog(url)
return true
end
end
end
local function handle_submit(this, fields)
local info = this.data.info
local package = this.data.package
if fields.back then
this:delete()
return true
end
if not info then
return false
end
if fields.open_contentdb then
local url = ("%s/packages/%s/?protocol_version=%d"):format(
core.settings:get("contentdb_url"), package.url_part,
core.get_max_supp_proto())
core.open_url(url)
return true
end
if fields.install then
install_or_update_package(this, package)
return true
end
if fields.uninstall then
local dlg = create_delete_content_dlg(package)
dlg:set_parent(this)
this:hide()
dlg:show()
return true
end
if fields.tabs then
this.data.current_tab = tonumber(fields.tabs)
return true
end
if handle_hypertext_event(this, fields.desc, info.long_description) or
handle_hypertext_event(this, fields.info, info.info_hypertext) then
return true
end
end
local function handle_events(event)
if event == "WindowInfoChange" then
ui.update()
return true
end
return false
end
function create_package_dialog(package)
assert(package)
local dlg = dialog_create("package_dialog_" .. package.id,
get_formspec,
handle_submit,
handle_events)
local data = dlg.data
data.package = package
data.info = nil
data.loading = false
data.loading_error = nil
data.current_tab = 1
return dlg
end

View file

@ -23,4 +23,5 @@ dofile(path .. DIR_DELIM .. "update_detector.lua")
dofile(path .. DIR_DELIM .. "screenshots.lua")
dofile(path .. DIR_DELIM .. "dlg_install.lua")
dofile(path .. DIR_DELIM .. "dlg_overwrite.lua")
dofile(path .. DIR_DELIM .. "dlg_package.lua")
dofile(path .. DIR_DELIM .. "dlg_contentdb.lua")

View file

@ -23,23 +23,40 @@ local screenshot_downloading = {}
local screenshot_downloaded = {}
local function get_filename(path)
local parts = path:split("/")
return parts[#parts]
end
local function get_file_extension(path)
local parts = path:split(".")
return parts[#parts]
end
function get_screenshot(package)
if not package.thumbnail then
function get_screenshot(package, screenshot_url, level)
if not screenshot_url then
return defaulttexturedir .. "no_screenshot.png"
elseif screenshot_downloading[package.thumbnail] then
end
-- Minetest only supports png and jpg
local ext = get_file_extension(screenshot_url)
if ext ~= "png" and ext ~= "jpg" then
screenshot_url = screenshot_url:sub(0, -#ext - 1) .. "png"
end
-- Set thumbnail level
screenshot_url = screenshot_url:gsub("/thumbnails/[0-9]+/", "/thumbnails/" .. level .. "/")
screenshot_url = screenshot_url:gsub("/uploads/", "/thumbnails/" .. level .. "/")
if screenshot_downloading[screenshot_url] then
return defaulttexturedir .. "loading_screenshot.png"
end
-- Get tmp screenshot path
local ext = get_file_extension(package.thumbnail)
local filepath = screenshot_dir .. DIR_DELIM ..
("%s-%s-%s.%s"):format(package.type, package.author, package.name, ext)
("%s-%s-%s-l%d-%s"):format(package.type, package.author, package.name,
level, get_filename(screenshot_url))
-- Return if already downloaded
local file = io.open(filepath, "r")
@ -49,7 +66,7 @@ function get_screenshot(package)
end
-- Show error if we've failed to download before
if screenshot_downloaded[package.thumbnail] then
if screenshot_downloaded[screenshot_url] then
return defaulttexturedir .. "error_screenshot.png"
end
@ -59,16 +76,16 @@ function get_screenshot(package)
return core.download_file(params.url, params.dest)
end
local function callback(success)
screenshot_downloading[package.thumbnail] = nil
screenshot_downloaded[package.thumbnail] = true
screenshot_downloading[screenshot_url] = nil
screenshot_downloaded[screenshot_url] = true
if not success then
core.log("warning", "Screenshot download failed for some reason")
end
ui.update()
end
if core.handle_async(download_screenshot,
{ dest = filepath, url = package.thumbnail }, callback) then
screenshot_downloading[package.thumbnail] = true
{ dest = filepath, url = screenshot_url }, callback) then
screenshot_downloading[screenshot_url] = true
else
core.log("error", "ERROR: async event failed")
return defaulttexturedir .. "error_screenshot.png"

View file

@ -67,6 +67,19 @@ function make.heading(text)
end
function make.note(text)
return {
full_width = true,
get_formspec = function(self, avail_w)
-- Assuming label height 0.4:
-- Position at y=0 to eat 0.2 of the padding above, leave 0.05.
-- The returned used_height doesn't include padding.
return ("label[0,0;%s]"):format(core.colorize("#bbb", core.formspec_escape(text))), 0.2
end,
}
end
--- Used for string and numeric style fields
---
--- @param converter Function to coerce values from strings.

View file

@ -152,9 +152,24 @@ local function load()
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 content = page_by_id.graphics_and_audio_effects.content
local idx = table.indexof(content, "enable_dynamic_shadows")
table.insert(content, idx, shadows_component)
idx = table.indexof(content, "enable_auto_exposure") + 1
local note = component_funcs.note(fgettext_ne("(The game will need to enable automatic exposure as well)"))
note.requires = get_setting_info("enable_auto_exposure").requires
table.insert(content, idx, note)
idx = table.indexof(content, "enable_bloom") + 1
note = component_funcs.note(fgettext_ne("(The game will need to enable bloom as well)"))
note.requires = get_setting_info("enable_bloom").requires
table.insert(content, idx, note)
idx = table.indexof(content, "enable_volumetric_lighting") + 1
note = component_funcs.note(fgettext_ne("(The game will need to enable volumetric lighting as well)"))
note.requires = get_setting_info("enable_volumetric_lighting").requires
table.insert(content, idx, note)
end
-- These must not be translated, as they need to show in the local
@ -222,6 +237,12 @@ local function load()
zh_CN = "中文 (简体) [zh_CN]",
zh_TW = "正體中文 (繁體) [zh_TW]",
}
get_setting_info("touch_controls").option_labels = {
["auto"] = fgettext_ne("Auto"),
["true"] = fgettext_ne("Enabled"),
["false"] = fgettext_ne("Disabled"),
}
end
@ -321,9 +342,14 @@ local function check_requirements(name, requires)
local video_driver = core.get_active_driver()
local shaders_support = video_driver == "opengl" or video_driver == "opengl3" or video_driver == "ogles2"
local touch_controls = core.settings:get("touch_controls")
local special = {
android = PLATFORM == "Android",
desktop = PLATFORM ~= "Android",
-- When touch_controls is "auto", we don't which input method will be used,
-- so we show settings for both.
touchscreen = touch_controls == "auto" or core.is_yes(touch_controls),
keyboard_mouse = touch_controls == "auto" or not core.is_yes(touch_controls),
shaders_support = shaders_support,
shaders = core.settings:get_bool("enable_shaders") and shaders_support,
opengl = video_driver == "opengl",
@ -433,19 +459,6 @@ local function build_page_components(page)
end
--- Creates a scrollbaroptions for a scroll_container
--
-- @param visible_l the length of the scroll_container and scrollbar
-- @param total_l length of the scrollable area
-- @param scroll_factor as passed to scroll_container
local function make_scrollbaroptions_for_scroll_container(visible_l, total_l, scroll_factor)
assert(total_l >= visible_l)
local max = total_l - visible_l
local thumb_size = (visible_l / total_l) * max
return ("scrollbaroptions[min=0;max=%f;thumbsize=%f]"):format(max / scroll_factor, thumb_size / scroll_factor)
end
local formspec_show_hack = false
@ -507,8 +520,8 @@ local function get_formspec(dialogdata)
"tooltip[search;", fgettext("Search"), "]",
"tooltip[search_clear;", fgettext("Clear"), "]",
"container_end[]",
"scroll_container[0.25,1.25;", tostring(left_pane_width), ",",
tostring(tabsize.height - 1.5), ";leftscroll;vertical;0.1]",
("scroll_container[0.25,1.25;%f,%f;leftscroll;vertical;0.1;0]"):format(
left_pane_width, tabsize.height - 1.5),
"style_type[button;border=false;bgcolor=#3333]",
"style_type[button:hover;border=false;bgcolor=#6663]",
}
@ -538,7 +551,6 @@ local function get_formspec(dialogdata)
fs[#fs + 1] = "scroll_container_end[]"
if y >= tabsize.height - 1.25 then
fs[#fs + 1] = make_scrollbaroptions_for_scroll_container(tabsize.height - 1.5, y, 0.1)
fs[#fs + 1] = ("scrollbar[%f,1.25;%f,%f;vertical;leftscroll;%f]"):format(
left_pane_width + 0.25, scrollbar_w, tabsize.height - 1.5, dialogdata.leftscroll or 0)
end
@ -550,7 +562,7 @@ local function get_formspec(dialogdata)
end
local right_pane_width = tabsize.width - left_pane_width - 0.375 - 2*scrollbar_w - 0.25
fs[#fs + 1] = ("scroll_container[%f,0;%f,%f;rightscroll;vertical;0.1]"):format(
fs[#fs + 1] = ("scroll_container[%f,0;%f,%f;rightscroll;vertical;0.1;0.25]"):format(
tabsize.width - right_pane_width - scrollbar_w, right_pane_width, tabsize.height)
y = 0.25
@ -606,7 +618,6 @@ local function get_formspec(dialogdata)
fs[#fs + 1] = "scroll_container_end[]"
if y >= tabsize.height then
fs[#fs + 1] = make_scrollbaroptions_for_scroll_container(tabsize.height, y + 0.375, 0.1)
fs[#fs + 1] = ("scrollbar[%f,0;%f,%f;vertical;rightscroll;%f]"):format(
tabsize.width - scrollbar_w, scrollbar_w, tabsize.height, dialogdata.rightscroll or 0)
end
@ -624,6 +635,18 @@ function write_settings_early()
end
end
local function regenerate_page_list(dialogdata)
local suggested_page_id = update_filtered_pages(dialogdata.query)
dialogdata.components = nil
if not filtered_page_by_id[dialogdata.page_id] then
dialogdata.leftscroll = 0
dialogdata.rightscroll = 0
dialogdata.page_id = suggested_page_id
end
end
local function buttonhandler(this, fields)
local dialogdata = this.data
@ -648,27 +671,7 @@ local function buttonhandler(this, fields)
local value = core.is_yes(fields.show_advanced)
core.settings:set_bool("show_advanced", value)
write_settings_early()
end
-- touch_controls is a checkbox in a setting component. We handle this
-- setting differently so we can hide/show pages using the next if-statement
if fields.touch_controls ~= nil then
local value = core.is_yes(fields.touch_controls)
core.settings:set_bool("touch_controls", value)
write_settings_early()
end
if fields.show_advanced ~= nil or fields.touch_controls ~= nil then
local suggested_page_id = update_filtered_pages(dialogdata.query)
dialogdata.components = nil
if not filtered_page_by_id[dialogdata.page_id] then
dialogdata.leftscroll = 0
dialogdata.rightscroll = 0
dialogdata.page_id = suggested_page_id
end
regenerate_page_list(dialogdata)
return true
end
@ -701,20 +704,26 @@ local function buttonhandler(this, fields)
end
end
for i, comp in ipairs(dialogdata.components) do
if comp.on_submit and comp:on_submit(fields, this) then
local function after_setting_change(comp)
write_settings_early()
if comp.setting.name == "touch_controls" then
-- Changing the "touch_controls" setting may result in a different
-- page list.
regenerate_page_list(dialogdata)
else
-- Clear components so they regenerate
dialogdata.components = nil
end
end
for i, comp in ipairs(dialogdata.components) do
if comp.on_submit and comp:on_submit(fields, this) then
after_setting_change(comp)
return true
end
if comp.setting and fields["reset_" .. i] then
core.settings:remove(comp.setting.name)
write_settings_early()
-- Clear components so they regenerate
dialogdata.components = nil
after_setting_change(comp)
return true
end
end

View file

@ -19,12 +19,7 @@
local function prepare_credits(dest, source)
local string = table.concat(source, "\n") .. "\n"
local hypertext_escapes = {
["\\"] = "\\\\",
["<"] = "\\<",
[">"] = "\\>",
}
string = string:gsub("[\\<>]", hypertext_escapes)
string = core.hypertext_escape(string)
string = string:gsub("%[.-%]", "<gray>%1</gray>")
table.insert(dest, string)

View file

@ -92,11 +92,11 @@ function singleplayer_refresh_gamebar()
end
end
local ENABLE_TOUCH = core.settings:get_bool("enable_touch")
local TOUCH_GUI = core.settings:get_bool("touch_gui")
local gamebar_pos_y = MAIN_TAB_H
+ TABHEADER_H -- tabheader included in formspec size
+ (ENABLE_TOUCH and GAMEBAR_OFFSET_TOUCH or GAMEBAR_OFFSET_DESKTOP)
+ (TOUCH_GUI and GAMEBAR_OFFSET_TOUCH or GAMEBAR_OFFSET_DESKTOP)
local btnbar = buttonbar_create(
"game_button_bar",

View file

@ -61,7 +61,7 @@
#
# # This is a comment
# #
# # Requires: shaders, enable_dynamic_shadows, !touch_controls
# # Requires: shaders, enable_dynamic_shadows, !enable_waving_leaves
# name (Readable name) type type_args
#
# A requirement can be the name of a boolean setting or an engine-defined value.
@ -72,6 +72,7 @@
# * shaders_support (a video driver that supports shaders, may not be enabled)
# * shaders (both enable_shaders and shaders_support)
# * desktop / android
# * touchscreen / keyboard_mouse
# * opengl / gles
# * You can negate any requirement by prepending with !
#
@ -91,7 +92,7 @@ camera_smoothing (Camera smoothing) float 0.0 0.0 0.99
# Smooths rotation of camera when in cinematic mode, 0 to disable. Enter cinematic mode by using the key set in Controls.
#
# Requires: !touch_controls
# Requires: keyboard_mouse
cinematic_camera_smoothing (Camera smoothing in cinematic mode) float 0.7 0.0 0.99
# If enabled, you can place nodes at the position (feet + eye level) where you stand.
@ -112,8 +113,8 @@ always_fly_fast (Always fly fast) bool true
# The time in seconds it takes between repeated node placements when holding
# the place button.
#
# Requires: !touch_controls
repeat_place_time (Place repetition interval) float 0.25 0.16 2.0
# Requires: keyboard_mouse
repeat_place_time (Place repetition interval) float 0.25 0.15 2.0
# The minimum time in seconds it takes between digging nodes when holding
# the dig button.
@ -131,60 +132,62 @@ safe_dig_and_place (Safe digging and placing) bool false
# Invert vertical mouse movement.
#
# Requires: !touch_controls
# Requires: keyboard_mouse
invert_mouse (Invert mouse) bool false
# Mouse sensitivity multiplier.
#
# Requires: !touch_controls
# Requires: keyboard_mouse
mouse_sensitivity (Mouse sensitivity) float 0.2 0.001 10.0
# Enable mouse wheel (scroll) for item selection in hotbar.
#
# Requires: !touch_controls
# Requires: keyboard_mouse
enable_hotbar_mouse_wheel (Hotbar: Enable mouse wheel for selection) bool true
# Invert mouse wheel (scroll) direction for item selection in hotbar.
#
# Requires: !touch_controls
# Requires: keyboard_mouse
invert_hotbar_mouse_wheel (Hotbar: Invert mouse wheel direction) bool false
[*Touchscreen]
# Enables the touchscreen controls, allowing you to play the game with a touchscreen.
touch_controls (Enable touchscreen controls) bool true
# "auto" means that the touchscreen controls will be enabled and disabled
# automatically depending on the last used input method.
touch_controls (Touchscreen controls) enum auto auto,true,false
# Touchscreen sensitivity multiplier.
#
# Requires: touch_controls
# Requires: touchscreen
touchscreen_sensitivity (Touchscreen sensitivity) float 0.2 0.001 10.0
# The length in pixels after which a touch interaction is considered movement.
#
# Requires: touch_controls
# Requires: touchscreen
touchscreen_threshold (Movement threshold) int 20 0 100
# The delay in milliseconds after which a touch interaction is considered a long tap.
#
# Requires: touch_controls
# Requires: touchscreen
touch_long_tap_delay (Threshold for long taps) int 400 100 1000
# Use crosshair to select object instead of whole screen.
# If enabled, a crosshair will be shown and will be used for selecting object.
#
# Requires: touch_controls
# Requires: touchscreen
touch_use_crosshair (Use crosshair for touch screen) bool false
# Fixes the position of virtual joystick.
# If disabled, virtual joystick will center to first-touch's position.
#
# Requires: touch_controls
# Requires: touchscreen
fixed_virtual_joystick (Fixed virtual joystick) bool false
# Use virtual joystick to trigger "Aux1" button.
# If enabled, virtual joystick will also tap "Aux1" button when out of main circle.
#
# Requires: touch_controls
# Requires: touchscreen
virtual_joystick_triggers_aux1 (Virtual joystick triggers Aux1 button) bool false
# The gesture for punching players/entities.
@ -197,7 +200,7 @@ virtual_joystick_triggers_aux1 (Virtual joystick triggers Aux1 button) bool fals
# Known from the classic Minetest mobile controls.
# Combat is more or less impossible.
#
# Requires: touch_controls
# Requires: touchscreen
touch_punch_gesture (Punch gesture) enum short_tap short_tap,long_tap
@ -262,31 +265,6 @@ viewing_range (Viewing range) int 190 20 4000
# Higher values result in a less detailed image.
undersampling (Undersampling) int 1 1 8
[**Graphics Effects]
# Allows liquids to be translucent.
translucent_liquids (Translucent liquids) bool true
# Leaves style:
# - Fancy: all faces visible
# - Simple: only outer faces, if defined special_tiles are used
# - Opaque: disable transparency
leaves_style (Leaves style) enum fancy fancy,simple,opaque
# Connects glass if supported by node.
connected_glass (Connect glass) bool false
# Enable smooth lighting with simple ambient occlusion.
# Disable for speed or for different looks.
smooth_lighting (Smooth lighting) bool true
# Enables tradeoffs that reduce CPU load or increase rendering performance
# at the expense of minor visual glitches that do not impact game playability.
performance_tradeoffs (Tradeoffs for performance) bool false
# Adds particles when digging a node.
enable_particles (Digging particles) bool true
[**3D]
# 3D support.
@ -466,13 +444,29 @@ enable_raytraced_culling (Enable Raytraced Culling) bool true
[*Shaders]
[*Effects]
# Shaders allow advanced visual effects and may increase performance on some video
# cards.
#
# Requires: shaders_support
enable_shaders (Shaders) bool true
# Allows liquids to be translucent.
translucent_liquids (Translucent liquids) bool true
# Leaves style:
# - Fancy: all faces visible
# - Simple: only outer faces
# - Opaque: disable transparency
leaves_style (Leaves style) enum fancy fancy,simple,opaque
# Connects glass if supported by node.
connected_glass (Connect glass) bool false
# Enable smooth lighting with simple ambient occlusion.
smooth_lighting (Smooth lighting) bool true
# Enables tradeoffs that reduce CPU load or increase rendering performance
# at the expense of minor visual glitches that do not impact game playability.
performance_tradeoffs (Tradeoffs for performance) bool false
# Adds particles when digging a node.
enable_particles (Digging particles) bool true
[**Waving Nodes]
@ -649,42 +643,12 @@ enable_vignette (Vignette) bool false
# Requires: shaders, enable_post_processing
debanding (Enable Debanding) bool true
[**Bloom]
# Set to true to enable bloom effect.
# Bright colors will bleed over the neighboring objects.
#
# Requires: shaders, enable_post_processing
enable_bloom (Enable Bloom) bool false
# Set to true to render debugging breakdown of the bloom effect.
# In debug mode, the screen is split into 4 quadrants:
# top-left - processed base image, top-right - final image
# bottom-left - raw base image, bottom-right - bloom texture.
#
# Requires: shaders, enable_post_processing, enable_bloom
enable_bloom_debug (Enable Bloom Debug) bool false
# Defines how much bloom is applied to the rendered image
# Smaller values make bloom more subtle
# Range: from 0.01 to 1.0, default: 0.05
#
# Requires: shaders, enable_post_processing, enable_bloom
bloom_intensity (Bloom Intensity) float 0.05 0.01 1.0
# Defines the magnitude of bloom overexposure.
# Range: from 0.1 to 10.0, default: 1.0
#
# Requires: shaders, enable_post_processing, enable_bloom
bloom_strength_factor (Bloom Strength Factor) float 1.0 0.1 10.0
# Logical value that controls how far the bloom effect spreads
# from the bright objects.
# Range: from 0.1 to 8, default: 1
#
# Requires: shaders, enable_post_processing, enable_bloom
bloom_radius (Bloom Radius) float 1 0.1 8
# Set to true to enable volumetric lighting effect (a.k.a. "Godrays").
#
# Requires: shaders, enable_post_processing, enable_bloom
@ -713,6 +677,11 @@ enable_translucent_foliage (Translucent foliage) bool false
# Requires: shaders, enable_dynamic_shadows
enable_node_specular (Node specular) bool false
# When enabled, liquid reflections are simulated.
#
# Requires: shaders, enable_waving_water, enable_dynamic_shadows
enable_water_reflections (Liquid reflections) bool false
[*Audio]
# Volume of all sounds.
@ -950,8 +919,13 @@ default_privs (Default privileges) string interact, shout
# Privileges that players with basic_privs can grant
basic_privs (Basic privileges) string interact, shout
# If enabled, disable cheat prevention in multiplayer.
disable_anticheat (Disable anticheat) bool false
# Server anticheat configuration.
# Flags are positive. Uncheck the flag to disable corresponding anticheat module.
anticheat_flags (Anticheat flags) flags digging,interaction,movement digging,interaction,movement
# Tolerance of movement cheat detector.
# Increase the value if players experience stuttery movement.
anticheat_movement_tolerance (Anticheat movement tolerance) float 1.0 1.0
# If enabled, actions are recorded for rollback.
# This option is only read when server starts.
@ -1896,6 +1870,11 @@ ignore_world_load_errors (Ignore world errors) bool false
[**Graphics]
# Shaders are a fundamental part of rendering and enable advanced visual effects.
#
# Requires: shaders_support
enable_shaders (Shaders) bool true
# Path to shader directory. If no path is defined, default location will be used.
#
# Requires: shaders
@ -1919,6 +1898,7 @@ cloud_radius (Cloud radius) int 12 1 62
desynchronize_mapblock_texture_animation (Desynchronize block animation) bool false
# Enables caching of facedir rotated meshes.
# This is only effective with shaders disabled.
enable_mesh_cache (Mesh cache) bool false
# Delay between mesh updates on the client in ms. Increasing this will slow
@ -1970,6 +1950,14 @@ client_mesh_chunk (Client Mesh Chunksize) int 1 1 16
# Enables debug and error-checking in the OpenGL driver.
opengl_debug (OpenGL debug) bool false
# Set to true to render debugging breakdown of the bloom effect.
# In debug mode, the screen is split into 4 quadrants:
# top-left - processed base image, top-right - final image
# bottom-left - raw base image, bottom-right - bloom texture.
#
# Requires: shaders, enable_post_processing, enable_bloom
enable_bloom_debug (Enable Bloom Debug) bool false
[**Sound]
# Comma-separated list of AL and ALC extensions that should not be used.
# Useful for testing. See al_extensions.[h,cpp] for details.

View file

@ -8,11 +8,7 @@ void main(void)
{
gl_Position = mWorldViewProj * inVertexPosition;
#ifdef GL_ES
vec4 color = inVertexColor.bgra;
#else
vec4 color = inVertexColor;
#endif
color *= materialColor;
varColor = color;

View file

@ -3,9 +3,5 @@ varying lowp vec4 varColor;
void main(void)
{
gl_Position = mWorldViewProj * inVertexPosition;
#ifdef GL_ES
varColor = inVertexColor.bgra;
#else
varColor = inVertexColor;
#endif
}

View file

@ -7,9 +7,5 @@ void main(void)
{
varTexCoord = inTexCoord0.st;
gl_Position = mWorldViewProj * inVertexPosition;
#ifdef GL_ES
varColor = inVertexColor.bgra;
#else
varColor = inVertexColor;
#endif
}

View file

@ -211,15 +211,11 @@ void main(void)
vNormal = inVertexNormal;
// Calculate color.
vec4 color = inVertexColor;
// Red, green and blue components are pre-multiplied with
// the brightness, so now we have to multiply these
// colors with the color of the incoming light.
// The pre-baked colors are halved to prevent overflow.
#ifdef GL_ES
vec4 color = inVertexColor.bgra;
#else
vec4 color = inVertexColor;
#endif
// The alpha gives the ratio of sunlight in the incoming light.
nightRatio = 1.0 - color.a;
color.rgb = color.rgb * (color.a * dayLight.rgb +

View file

@ -124,11 +124,7 @@ void main(void)
: directional_ambient(normalize(inVertexNormal));
#endif
#ifdef GL_ES
vec4 color = inVertexColor.bgra;
#else
vec4 color = inVertexColor;
#endif
color *= materialColor;

View file

@ -6,9 +6,5 @@ void main(void)
varTexCoord = inTexCoord0.st;
gl_Position = mWorldViewProj * inVertexPosition;
#ifdef GL_ES
varColor = inVertexColor.bgra;
#else
varColor = inVertexColor;
#endif
}

View file

@ -274,7 +274,7 @@ Accepted formats are:
images: .png, .jpg, .tga, (deprecated:) .bmp
sounds: .ogg vorbis
models: .x, .b3d, .obj, .gltf (Minetest 5.10 or newer)
models: .x, .b3d, .obj, (since version 5.10:) .gltf, .glb
Other formats won't be sent to the client (e.g. you can store .blend files
in a folder for convenience, without the risk that such files are transferred)
@ -294,7 +294,7 @@ depends on by supplying a file with an equal name.
Only a subset of model file format features is supported:
Simple textured meshes (with multiple textures), optionally with normals.
The .x and .b3d formats additionally support skeletal animation.
The .x, .b3d and .gltf formats additionally support (a single) animation.
#### glTF
@ -302,9 +302,15 @@ The glTF model file format for now only serves as a
more modern alternative to the other static model file formats;
it unlocks no special rendering features.
Binary glTF (`.glb`) files are supported and recommended over `.gltf` files
due to their space savings.
This means that many glTF features are not supported *yet*, including:
* Animation
* Animations
* Only a single animation is supported,
use frame ranges within this animation.
* Only integer frames are supported.
* Cameras
* Materials
* Only base color textures are supported
@ -490,6 +496,11 @@ to let the client generate textures on-the-fly.
The modifiers are applied directly in sRGB colorspace,
i.e. without gamma-correction.
### Notes
* `TEXMOD_UPSCALE`: The texture with the lower resolution will be automatically
upscaled to the higher resolution texture.
### Texture overlaying
Textures can be overlaid by putting a `^` between them.
@ -503,8 +514,9 @@ Example:
default_dirt.png^default_grass_side.png
`default_grass_side.png` is overlaid over `default_dirt.png`.
The texture with the lower resolution will be automatically upscaled to
the higher resolution texture.
*See notes: `TEXMOD_UPSCALE`*
### Texture grouping
@ -701,6 +713,8 @@ Apply a mask to the base image.
The mask is applied using binary AND.
*See notes: `TEXMOD_UPSCALE`*
#### `[sheet:<w>x<h>:<x>,<y>`
Retrieves a tile at position x, y (in tiles, 0-indexed)
@ -798,6 +812,8 @@ in GIMP. Overlay is the same as Hard light but with the role of the two
textures swapped, see the `[hardlight` modifier description for more detail
about these blend modes.
*See notes: `TEXMOD_UPSCALE`*
#### `[hardlight:<file>`
Applies a Hard light blend with the two textures, like the Hard light layer
@ -813,6 +829,8 @@ increase contrast without clipping.
Hard light is the same as Overlay but with the roles of the two textures
swapped, i.e. `A.png^[hardlight:B.png` is the same as `B.png^[overlay:A.png`
*See notes: `TEXMOD_UPSCALE`*
#### `[png:<base64>`
Embed a base64 encoded PNG image in the texture string.
@ -831,6 +849,8 @@ In particular consider `minetest.dynamic_add_media` and test whether
using other texture modifiers could result in a shorter string than
embedding a whole image, this may vary by use case.
*See notes: `TEXMOD_UPSCALE`*
Hardware coloring
-----------------
@ -1394,16 +1414,19 @@ The function of `param2` is determined by `paramtype2` in node definition.
The palette should have 256 pixels.
* `paramtype2 = "colorfacedir"`
* Same as `facedir`, but with colors.
* The first three bits of `param2` tells which color is picked from the
* The three most significant bits of `param2` tells which color is picked from the
palette. The palette should have 8 pixels.
* The five least significant bits contain the `facedir` value.
* `paramtype2 = "color4dir"`
* Same as `facedir`, but with colors.
* The first six bits of `param2` tells which color is picked from the
* Same as `4dir`, but with colors.
* The six most significant bits of `param2` tells which color is picked from the
palette. The palette should have 64 pixels.
* The two least significant bits contain the `4dir` rotation.
* `paramtype2 = "colorwallmounted"`
* Same as `wallmounted`, but with colors.
* The first five bits of `param2` tells which color is picked from the
* The five most significant bits of `param2` tells which color is picked from the
palette. The palette should have 32 pixels.
* The three least significant bits contain the `wallmounted` value.
* `paramtype2 = "glasslikeliquidlevel"`
* Only valid for "glasslike_framed" or "glasslike_framed_optional"
drawtypes. "glasslike_framed_optional" nodes are only affected if the
@ -1417,9 +1440,9 @@ The function of `param2` is determined by `paramtype2` in node definition.
* Liquid texture is defined using `special_tiles = {"modname_tilename.png"}`
* `paramtype2 = "colordegrotate"`
* Same as `degrotate`, but with colors.
* The first (most-significant) three bits of `param2` tells which color
is picked from the palette. The palette should have 8 pixels.
* Remaining 5 bits store rotation in range 023 (i.e. in 15° steps)
* The three most significant bits of `param2` tells which color is picked
from the palette. The palette should have 8 pixels.
* The five least significant bits store rotation in range 023 (i.e. in 15° steps)
* `paramtype2 = "none"`
* `param2` will not be used by the engine and can be used to store
an arbitrary value
@ -1470,7 +1493,8 @@ Look for examples in `games/devtest` or `games/minetest_game`.
'Connected Glass'.
* `allfaces`
* Often used for partially-transparent nodes.
* External and internal sides of textures are visible.
* External sides of textures, and unlike other drawtypes, the external sides
of other blocks, are visible from the inside.
* `allfaces_optional`
* Often used for leaves nodes.
* This switches between `normal`, `glasslike` and `allfaces` according to
@ -2729,6 +2753,8 @@ Version History
* Formspec version 7 (5.8.0):
* style[]: Add focused state for buttons
* Add field_enter_after_edit[] (experimental)
* Formspec version 8 (5.10.0)
* scroll_container[]: content padding parameter
Elements
--------
@ -2812,7 +2838,7 @@ Elements
* End of a container, following elements are no longer relative to this
container.
### `scroll_container[<X>,<Y>;<W>,<H>;<scrollbar name>;<orientation>;<scroll factor>]`
### `scroll_container[<X>,<Y>;<W>,<H>;<scrollbar name>;<orientation>;<scroll factor>;<content padding>]`
* Start of a scroll_container block. All contained elements will ...
* take the scroll_container coordinate as position origin,
@ -2821,6 +2847,12 @@ Elements
* be clipped to the rectangle defined by `X`, `Y`, `W` and `H`.
* `orientation`: possible values are `vertical` and `horizontal`.
* `scroll factor`: optional, defaults to `0.1`.
* `content padding`: (optional), in formspec coordinate units
* If specified, the scrollbar properties `max` and `thumbsize` are calculated automatically
based on the content size plus `content padding` at the end of the container. `min` is set to 0.
* Negative `scroll factor` is not supported.
* When active, `scrollbaroptions[]` has no effect on the affected properties.
* Defaults to empty value (= disabled).
* Nesting is possible.
* Some elements might work a little different if they are in a scroll_container.
* Note: If you want the scroll_container to actually work, you also need to add a
@ -5524,6 +5556,8 @@ Utilities
hotbar_hud_element = true,
-- Bulk LBM support (5.10.0)
bulk_lbms = true,
-- ABM supports field without_neighbors (5.10.0)
abm_without_neighbors = true,
}
```
@ -5847,8 +5881,13 @@ Call these functions only at load time!
* `clicker`: ObjectRef - Object that acted upon `player`, may or may not be a player
* `minetest.register_on_player_hpchange(function(player, hp_change, reason), modifier)`
* Called when the player gets damaged or healed
* When `hp == 0`, damage doesn't trigger this callback.
* When `hp == hp_max`, healing does still trigger this callback.
* `player`: ObjectRef of the player
* `hp_change`: the amount of change. Negative when it is damage.
* Historically, the new HP value was clamped to [0, 65535] before
calculating the HP change. This clamping has been removed as of
Minetest 5.10.0
* `reason`: a PlayerHPChangeReason table.
* The `type` field will have one of the following values:
* `set_hp`: A mod or the engine called `set_hp` without
@ -6556,6 +6595,9 @@ Formspec
* `minetest.formspec_escape(string)`: returns a string
* escapes the characters "[", "]", "\", "," and ";", which cannot be used
in formspecs.
* `minetest.hypertext_escape(string)`: returns a string
* escapes the characters "\", "<", and ">" to show text in a hypertext element.
* not safe for use with tag attributes.
* `minetest.explode_table_event(string)`: returns a table
* returns e.g. `{type="CHG", row=1, column=2}`
* `type` is one of:
@ -6813,17 +6855,6 @@ This allows you easy interoperability for delegating work to jobs.
* Register a path to a Lua file to be imported when an async environment
is initialized. You can use this to preload code which you can then call
later using `minetest.handle_async()`.
* `minetest.register_portable_metatable(name, mt)`:
* Register a metatable that should be preserved when data is transferred
between the main thread and the async environment.
* `name` is a string that identifies the metatable. It is recommended to
follow the `modname:name` convention for this identifier.
* `mt` is the metatable to register.
* Note that it is allowed to register the same metatable under multiple
names, but it is not allowed to register multiple metatables under the
same name.
* You must register the metatable in both the main environment
and the async environment for this mechanism to work.
### List of APIs available in an async environment
@ -6853,7 +6884,8 @@ Functions:
* Standalone helpers such as logging, filesystem, encoding,
hashing or compression APIs
* `minetest.register_portable_metatable` (see above)
* `minetest.register_portable_metatable`
* IPC
Variables:
@ -6931,6 +6963,7 @@ Functions:
* `minetest.get_node`, `set_node`, `find_node_near`, `find_nodes_in_area`,
`spawn_tree` and similar
* these only operate on the current chunk (if inside a callback)
* IPC
Variables:
@ -7008,6 +7041,52 @@ Server
this can make transfer of bigger files painless (if set up). Nevertheless
it is advised not to use dynamic media for big media files.
IPC
---
The engine provides a generalized mechanism to enable sharing data between the
different Lua environments (main, mapgen and async).
It is essentially a shared in-memory key-value store.
* `minetest.ipc_get(key)`:
* Read a value from the shared data area.
* `key`: string, should use the `"modname:thing"` convention to avoid conflicts.
* returns an arbitrary Lua value, or `nil` if this key does not exist
* `minetest.ipc_set(key, value)`:
* Write a value to the shared data area.
* `key`: as above
* `value`: an arbitrary Lua value, cannot be or contain userdata.
Interacting with the shared data will perform an operation comparable to
(de)serialization on each access.
For that reason modifying references will not have any effect, as in this example:
```lua
minetest.ipc_set("test:foo", {})
minetest.ipc_get("test:foo").subkey = "value" -- WRONG!
minetest.ipc_get("test:foo") -- returns an empty table
```
**Advanced**:
* `minetest.ipc_cas(key, old_value, new_value)`:
* Write a value to the shared data area, but only if the previous value
equals what was given.
This operation is called Compare-and-Swap and can be used to implement
synchronization between threads.
* `key`: as above
* `old_value`: value compared to using `==` (`nil` compares equal for non-existing keys)
* `new_value`: value that will be set
* returns: true on success, false otherwise
* `minetest.ipc_poll(key, timeout)`:
* Do a blocking wait until a value (other than `nil`) is present at the key.
* **IMPORTANT**: You usually don't need this function. Use this as a last resort
if nothing else can satisfy your use case! None of the Lua environments the
engine has are safe to block for extended periods, especially on the main
thread any delays directly translate to lag felt by players.
* `key`: as above
* `timeout`: maximum wait time, in milliseconds (positive values only)
* returns: true on success, false on timeout
Bans
----
@ -7407,6 +7486,17 @@ Misc.
* `minetest.global_exists(name)`
* Checks if a global variable has been set, without triggering a warning.
* `minetest.register_portable_metatable(name, mt)`:
* Register a metatable that should be preserved when Lua data is transferred
between environments (via IPC or `handle_async`).
* `name` is a string that identifies the metatable. It is recommended to
follow the `modname:name` convention for this identifier.
* `mt` is the metatable to register.
* Note that the same metatable can be registered under multiple names,
but multiple metatables must not be registered under the same name.
* You must register the metatable in both the main environment
and the async environment for this mechanism to work.
Global objects
--------------
@ -8017,8 +8107,7 @@ child will follow movement and rotation of that bone.
* Animation interpolates towards the end frame but stops when it is reached
* If looped, there is no interpolation back to the start frame
* If looped, the model should look identical at start and end
* Only integer numbers are supported
* default: `{x=1, y=1}`
* default: `{x=1.0, y=1.0}`
* `frame_speed`: How fast the animation plays, in frames per second (number)
* default: `15.0`
* `frame_blend`: number, default: `0.0`
@ -8241,12 +8330,18 @@ child will follow movement and rotation of that bone.
bgcolor[], any non-style elements (eg: label) may result in weird behavior.
* Only affects formspecs shown after this is called.
* `get_formspec_prepend()`: returns a formspec string.
* `get_player_control()`: returns table with player pressed keys
* The table consists of fields with the following boolean values
representing the pressed keys: `up`, `down`, `left`, `right`, `jump`,
`aux1`, `sneak`, `dig`, `place`, `LMB`, `RMB`, and `zoom`.
* `get_player_control()`: returns table with player input
* The table contains the following boolean fields representing the pressed
keys: `up`, `down`, `left`, `right`, `jump`, `aux1`, `sneak`, `dig`,
`place`, `LMB`, `RMB` and `zoom`.
* The fields `LMB` and `RMB` are equal to `dig` and `place` respectively,
and exist only to preserve backwards compatibility.
* The table also contains the fields `movement_x` and `movement_y`.
* They represent the movement of the player. Values are numbers in the
range [-1.0,+1.0].
* They take both keyboard and joystick input into account.
* You should prefer them over `up`, `down`, `left` and `right` to
support different input methods correctly.
* Returns an empty table `{}` if the object is not a player.
* `get_player_control_bits()`: returns integer with bit packed player pressed
keys.
@ -8574,23 +8669,43 @@ child will follow movement and rotation of that bone.
* values < 0 cause an effect similar to inversion,
but keeping original luma and being symmetrical in terms of saturation
(eg. -1 and 1 is the same saturation and luma, but different hues)
* This value has no effect on clients who have shaders or post-processing disabled.
* `shadows` is a table that controls ambient shadows
* This has no effect on clients who have the "Dynamic Shadows" effect disabled.
* `intensity` sets the intensity of the shadows from 0 (no shadows, default) to 1 (blackness)
* This value has no effect on clients who have the "Dynamic Shadows" shader disabled.
* `tint` tints the shadows with the provided color, with RGB values ranging from 0 to 255.
(default `{r=0, g=0, b=0}`)
* This value has no effect on clients who have the "Dynamic Shadows" shader disabled.
* `exposure` is a table that controls automatic exposure.
The basic exposure factor equation is `e = 2^exposure_correction / clamp(luminance, 2^luminance_min, 2^luminance_max)`
* This has no effect on clients who have the "Automatic Exposure" effect disabled.
* `luminance_min` set the lower luminance boundary to use in the calculation (default: `-3.0`)
* `luminance_max` set the upper luminance boundary to use in the calculation (default: `-3.0`)
* `exposure_correction` correct observed exposure by the given EV value (default: `0.0`)
* `speed_dark_bright` set the speed of adapting to bright light (default: `1000.0`)
* `speed_bright_dark` set the speed of adapting to dark scene (default: `1000.0`)
* `center_weight_power` set the power factor for center-weighted luminance measurement (default: `1.0`)
* `bloom` is a table that controls bloom.
* This has no effect on clients with protocol version < 46 or clients who
have the "Bloom" effect disabled.
* `intensity` defines much bloom is applied to the rendered image.
* Recommended range: from 0.0 to 1.0, default: 0.05
* If set to 0, bloom is disabled.
* The default value is to be changed from 0.05 to 0 in the future.
If you wish to keep the current default value, you should set it
explicitly.
* `strength_factor` defines the magnitude of bloom overexposure.
* Recommended range: from 0.1 to 10.0, default: 1.0
* `radius` is a logical value that controls how far the bloom effect
spreads from the bright objects.
* Recommended range: from 0.1 to 8.0, default: 1.0
* The behavior of values outside the recommended range is unspecified.
* `volumetric_light`: is a table that controls volumetric light (a.k.a. "godrays")
* `strength`: sets the strength of the volumetric light effect from 0 (off, default) to 1 (strongest)
* This value has no effect on clients who have the "Volumetric Lighting" or "Bloom" shaders disabled.
* This has no effect on clients who have the "Volumetric Lighting" or "Bloom" effects disabled.
* `strength`: sets the strength of the volumetric light effect from 0 (off, default) to 1 (strongest).
* `0.2` is a reasonable standard value.
* Currently, bloom `intensity` and `strength_factor` affect volumetric
lighting `strength` and vice versa. This behavior is to be changed
in the future, do not rely on it.
* `get_lighting()`: returns the current state of lighting for the player.
* Result is a table with the same fields as `light_definition` in `set_lighting`.
@ -9106,6 +9221,11 @@ Used by `minetest.register_abm`.
-- If left out or empty, any neighbor will do.
-- `group:groupname` can also be used here.
without_neighbors = {"default:lava_source", "default:lava_flowing"},
-- Only apply `action` to nodes that have no one of these neighbors.
-- If left out or empty, it has no effect.
-- `group:groupname` can also be used here.
interval = 10.0,
-- Operation interval in seconds
@ -9515,12 +9635,18 @@ Used by `minetest.register_node`.
use_texture_alpha = ...,
-- Specifies how the texture's alpha channel will be used for rendering.
-- possible values:
-- * "opaque": Node is rendered opaque regardless of alpha channel
-- * "clip": A given pixel is either fully see-through or opaque
-- depending on the alpha channel being below/above 50% in value
-- * "blend": The alpha channel specifies how transparent a given pixel
-- of the rendered node is
-- Possible values:
-- * "opaque":
-- Node is rendered opaque regardless of alpha channel.
-- * "clip":
-- A given pixel is either fully see-through or opaque
-- depending on the alpha channel being below/above 50% in value.
-- Use this for nodes with fully transparent and fully opaque areas.
-- * "blend":
-- The alpha channel specifies how transparent a given pixel
-- of the rendered node is. This comes at a performance cost.
-- Only use this when correct rendering
-- among semitransparent nodes is necessary.
-- The default is "opaque" for drawtypes normal, liquid and flowingliquid,
-- mesh and nodebox or "clip" otherwise.
-- If set to a boolean value (deprecated): true either sets it to blend

View file

@ -57,7 +57,10 @@ Functions
* returns the maximum supported network protocol version
* `core.open_url(url)`
* opens the URL in a web browser, returns false on failure.
* Must begin with http:// or https://
* `url` must begin with http:// or https://
* `core.open_url_dialog(url)`
* shows a dialog to allow the user to choose whether to open a URL.
* `url` must begin with http:// or https://
* `core.open_dir(path)`
* opens the path in the system file browser/explorer, returns false on failure.
* Must be an existing directory.
@ -65,6 +68,8 @@ Functions
* Android only. Shares file using the share popup
* `core.get_version()` (possible in async calls)
* returns current core version
* `core.get_formspec_version()`
* returns maximum supported formspec version

View file

@ -1,4 +1,4 @@
glTF test model (and corresponding texture) licenses:
The glTF test models (and corresponding textures) in this mod are all licensed freely:
* Spider (`gltf_spider.gltf`, `gltf_spider.png`):
* By [archfan7411](https://github.com/archfan7411)

View file

@ -18,13 +18,57 @@ do
register_entity("blender_cube", cube_textures)
register_entity("blender_cube_scaled", cube_textures)
register_entity("blender_cube_matrix_transform", cube_textures)
minetest.register_entity("gltf:blender_cube_glb", {
initial_properties = {
visual = "mesh",
mesh = "gltf_blender_cube.glb",
textures = cube_textures,
backface_culling = true,
},
})
end
register_entity("snow_man", {"gltf_snow_man.png"})
register_entity("spider", {"gltf_spider.png"})
-- Note: Model has an animation, but we can use it as a static test nevertheless
minetest.register_entity("gltf:spider_animated", {
initial_properties = {
visual = "mesh",
mesh = "gltf_spider_animated.gltf",
textures = {"gltf_spider.png"},
},
on_activate = function(self)
self.object:set_animation({x = 0, y = 140}, 1)
end
})
minetest.register_entity("gltf:simple_skin", {
initial_properties = {
visual = "mesh",
visual_size = vector.new(5, 5, 5),
mesh = "gltf_simple_skin.gltf",
textures = {},
backface_culling = false
},
on_activate = function(self)
self.object:set_animation({x = 0, y = 5.5}, 1)
end
})
-- The claws rendering incorrectly from one side is expected behavior:
-- They use an unsupported double-sided material.
register_entity("frog", {"gltf_frog.png"}, false)
minetest.register_entity("gltf:frog", {
initial_properties = {
visual = "mesh",
mesh = "gltf_frog.gltf",
textures = {"gltf_frog.png"},
backface_culling = false
},
on_activate = function(self)
self.object:set_animation({x = 0, y = 0.75}, 1)
end
})
minetest.register_node("gltf:frog", {
description = "glTF frog, but it's a node",

Binary file not shown.

View file

@ -0,0 +1 @@
{"scene":0,"scenes":[{"nodes":[0,1]}],"nodes":[{"skin":0,"mesh":0},{"children":[2]},{"translation":[0.0,1.0,0.0],"rotation":[0.0,0.0,0.0,1.0]}],"meshes":[{"primitives":[{"attributes":{"POSITION":1,"JOINTS_0":2,"WEIGHTS_0":3},"indices":0}]}],"skins":[{"inverseBindMatrices":4,"joints":[1,2]}],"animations":[{"channels":[{"sampler":0,"target":{"node":2,"path":"rotation"}}],"samplers":[{"input":5,"interpolation":"LINEAR","output":6}]}],"buffers":[{"uri":"data:application/gltf-buffer;base64,AAABAAMAAAADAAIAAgADAAUAAgAFAAQABAAFAAcABAAHAAYABgAHAAkABgAJAAgAAAAAvwAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAvwAAAD8AAAAAAAAAPwAAAD8AAAAAAAAAvwAAgD8AAAAAAAAAPwAAgD8AAAAAAAAAvwAAwD8AAAAAAAAAPwAAwD8AAAAAAAAAvwAAAEAAAAAAAAAAPwAAAEAAAAAA","byteLength":168},{"uri":"data:application/gltf-buffer;base64,AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAABAPwAAgD4AAAAAAAAAAAAAQD8AAIA+AAAAAAAAAAAAAAA/AAAAPwAAAAAAAAAAAAAAPwAAAD8AAAAAAAAAAAAAgD4AAEA/AAAAAAAAAAAAAIA+AABAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAA=","byteLength":320},{"uri":"data:application/gltf-buffer;base64,AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAgD8=","byteLength":128},{"uri":"data:application/gltf-buffer;base64,AAAAAAAAAD8AAIA/AADAPwAAAEAAACBAAABAQAAAYEAAAIBAAACQQAAAoEAAALBAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAkxjEPkSLbD8AAAAAAAAAAPT9ND/0/TQ/AAAAAAAAAAD0/TQ/9P00PwAAAAAAAAAAkxjEPkSLbD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAkxjEvkSLbD8AAAAAAAAAAPT9NL/0/TQ/AAAAAAAAAAD0/TS/9P00PwAAAAAAAAAAkxjEvkSLbD8AAAAAAAAAAAAAAAAAAIA/","byteLength":240}],"bufferViews":[{"buffer":0,"byteLength":48,"target":34963},{"buffer":0,"byteOffset":48,"byteLength":120,"target":34962},{"buffer":1,"byteLength":320,"byteStride":16},{"buffer":2,"byteLength":128},{"buffer":3,"byteLength":240}],"accessors":[{"bufferView":0,"componentType":5123,"count":24,"type":"SCALAR"},{"bufferView":1,"componentType":5126,"count":10,"type":"VEC3","max":[0.5,2.0,0.0],"min":[-0.5,0.0,0.0]},{"bufferView":2,"componentType":5123,"count":10,"type":"VEC4"},{"bufferView":2,"byteOffset":160,"componentType":5126,"count":10,"type":"VEC4"},{"bufferView":3,"componentType":5126,"count":2,"type":"MAT4"},{"bufferView":4,"componentType":5126,"count":12,"type":"SCALAR","max":[5.5],"min":[0.0]},{"bufferView":4,"byteOffset":48,"componentType":5126,"count":12,"type":"VEC4","max":[0.0,0.0,0.707,1.0],"min":[0.0,0.0,-0.707,0.707]}],"asset":{"version":"2.0"}}

File diff suppressed because one or more lines are too long

View file

@ -14,7 +14,21 @@ local lighting_sections = {
{n = "speed_bright_dark", d = "Dark scene adaptation speed", min = -10, max = 10, type="log2"},
{n = "center_weight_power", d = "Power factor for center-weighting", min = 0.1, max = 10},
}
}
},
{
n = "bloom", d = "Bloom",
entries = {
{n = "intensity", d = "Intensity", min = 0, max = 1},
{n = "strength_factor", d = "Strength Factor", min = 0.1, max = 10},
{n = "radius", d = "Radius", min = 0.1, max = 8},
},
},
{
n = "volumetric_light", d = "Volumetric Lighting",
entries = {
{n = "strength", d = "Strength", min = 0, max = 1},
},
},
}
local function dump_lighting(lighting)
@ -59,38 +73,40 @@ minetest.register_chatcommand("set_lighting", {
local lighting = player:get_lighting()
local exposure = lighting.exposure or {}
local form = {
"formspec_version[2]",
"size[15,30]",
"position[0.99,0.15]",
"anchor[1,0]",
"padding[0.05,0.1]",
"no_prepend[]"
};
local content = {}
local line = 1
for _,section in ipairs(lighting_sections) do
local parameters = section.entries or {}
local state = lighting[section.n] or {}
table.insert(form, "label[1,"..line..";"..section.d.."]")
table.insert(content, "label[1,"..line..";"..section.d.."]")
line = line + 1
for _,v in ipairs(parameters) do
table.insert(form, "label[2,"..line..";"..v.d.."]")
table.insert(form, "scrollbaroptions[min=0;max=1000;smallstep=10;largestep=100;thumbsize=10]")
table.insert(content, "label[2,"..line..";"..v.d.."]")
table.insert(content, "scrollbaroptions[min=0;max=1000;smallstep=10;largestep=100;thumbsize=10]")
local value = state[v.n]
if v.type == "log2" then
value = math.log(value or 1) / math.log(2)
end
local sb_scale = math.floor(1000 * (math.max(v.min, value or 0) - v.min) / (v.max - v.min))
table.insert(form, "scrollbar[2,"..(line+0.7)..";12,1;horizontal;"..section.n.."."..v.n..";"..sb_scale.."]")
table.insert(content, "scrollbar[2,"..(line+0.7)..";12,1;horizontal;"..section.n.."."..v.n..";"..sb_scale.."]")
line = line + 2.7
end
line = line + 1
end
local form = {
"formspec_version[2]",
"size[15,", line, "]",
"position[0.99,0.15]",
"anchor[1,0]",
"padding[0.05,0.1]",
"no_prepend[]",
}
table.insert_all(form, content)
minetest.show_formspec(player_name, "lighting", table.concat(form))
local debug_value = dump_lighting(lighting)
local debug_ui = player:hud_add({type="text", position={x=0.1, y=0.3}, scale={x=1,y=1}, alignment = {x=1, y=1}, text=debug_value, number=0xFFFFFF})

View file

@ -0,0 +1,6 @@
# Test ABMs
This mod contains a nodes and related ABM actions.
By placing these nodes, you can test basic ABM behaviours.
There are separate tests for ABM `chance`, `interval`, `min_y`, `max_y`, `neighbor` and `without_neighbor` fields.

View file

@ -0,0 +1,12 @@
local S = minetest.get_translator("testnodes")
-- After ABM node
minetest.register_node("testabms:after_abm", {
description = S("After ABM processed node."),
drawtype = "normal",
tiles = { "testabms_after_node.png" },
groups = { dig_immediate = 3 },
})

View file

@ -0,0 +1,56 @@
-- test ABMs with different chances
local S = minetest.get_translator("testnodes")
-- ABM chance 5 node
minetest.register_node("testabms:chance_5", {
description = S("Node for test ABM chance_5"),
drawtype = "normal",
tiles = { "testabms_wait_node.png" },
groups = { dig_immediate = 3 },
on_construct = function (pos)
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "Waiting for ABM testabms:chance_5")
end,
})
minetest.register_abm({
label = "testabms:chance_5",
nodenames = "testabms:chance_5",
interval = 10,
chance = 5,
action = function (pos)
minetest.swap_node(pos, {name="testabms:after_abm"})
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "ABM testabsm:chance_5 changed this node.")
end
})
-- ABM chance 20 node
minetest.register_node("testabms:chance_20", {
description = S("Node for test ABM chance_20"),
drawtype = "normal",
tiles = { "testabms_wait_node.png" },
groups = { dig_immediate = 3 },
on_construct = function (pos)
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "Waiting for ABM testabms:chance_20")
end,
})
minetest.register_abm({
label = "testabms:chance_20",
nodenames = "testabms:chance_20",
interval = 10,
chance = 20,
action = function (pos)
minetest.swap_node(pos, {name="testabms:after_abm"})
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "ABM testabsm:chance_20 changed this node.")
end
})

View file

@ -0,0 +1,7 @@
local path = minetest.get_modpath(minetest.get_current_modname())
dofile(path.."/after_node.lua")
dofile(path.."/chances.lua")
dofile(path.."/intervals.lua")
dofile(path.."/min_max.lua")
dofile(path.."/neighbors.lua")

View file

@ -0,0 +1,56 @@
-- test ABMs with different interval
local S = minetest.get_translator("testnodes")
-- ABM inteval 1 node
minetest.register_node("testabms:interval_1", {
description = S("Node for test ABM interval_1"),
drawtype = "normal",
tiles = { "testabms_wait_node.png" },
groups = { dig_immediate = 3 },
on_construct = function (pos)
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "Waiting for ABM testabms:interval_1")
end,
})
minetest.register_abm({
label = "testabms:interval_1",
nodenames = "testabms:interval_1",
interval = 1,
chance = 1,
action = function (pos)
minetest.swap_node(pos, {name="testabms:after_abm"})
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "ABM testabsm:interval_1 changed this node.")
end
})
-- ABM interval 60 node
minetest.register_node("testabms:interval_60", {
description = S("Node for test ABM interval_60"),
drawtype = "normal",
tiles = { "testabms_wait_node.png" },
groups = { dig_immediate = 3 },
on_construct = function (pos)
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "Waiting for ABM testabms:interval_60")
end,
})
minetest.register_abm({
label = "testabms:interval_60",
nodenames = "testabms:interval_60",
interval = 60,
chance = 1,
action = function (pos)
minetest.swap_node(pos, {name="testabms:after_abm"})
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "ABM testabsm:interval_60 changed this node.")
end
})

View file

@ -0,0 +1,58 @@
-- test ABMs with min_y and max_y
local S = minetest.get_translator("testnodes")
-- ABM min_y node
minetest.register_node("testabms:min_y", {
description = S("Node for test ABM min_y."),
drawtype = "normal",
tiles = { "testabms_wait_node.png" },
groups = { dig_immediate = 3 },
on_construct = function (pos)
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "Waiting for ABM testabms:min_y at y "..pos.y.." with min_y = 0")
end,
})
minetest.register_abm({
label = "testabms:min_y",
nodenames = "testabms:min_y",
interval = 10,
chance = 1,
min_y = 0,
action = function (pos)
minetest.swap_node(pos, {name="testabms:after_abm"})
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "ABM testabsm:min_y changed this node.")
end
})
-- ABM max_y node
minetest.register_node("testabms:max_y", {
description = S("Node for test ABM max_y."),
drawtype = "normal",
tiles = { "testabms_wait_node.png" },
groups = { dig_immediate = 3 },
on_construct = function (pos)
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "Waiting for ABM testabms:max_y at y "..pos.y.." with max_y = 0")
end,
})
minetest.register_abm({
label = "testabms:max_y",
nodenames = "testabms:max_y",
interval = 10,
chance = 1,
max_y = 0,
action = function (pos)
minetest.swap_node(pos, {name="testabms:after_abm"})
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "ABM testabsm:max_y changed this node.")
end
})

View file

@ -0,0 +1,2 @@
name = testabms
description = Contains some nodes for test ABMs.

View file

@ -0,0 +1,99 @@
-- test ABMs with neighbor and without_neighbor
local S = minetest.get_translator("testnodes")
-- ABM required neighbor
minetest.register_node("testabms:required_neighbor", {
description = S("Node for test ABM required_neighbor.") .. "\n"
.. S("Sensitive neighbor node is testnodes:normal."),
drawtype = "normal",
tiles = { "testabms_wait_node.png" },
groups = { dig_immediate = 3 },
on_construct = function (pos)
local meta = minetest.get_meta(pos)
meta:set_string("infotext",
"Waiting for ABM testabms:required_neighbor "
.. "(normal drawtype testnode sensitive)")
end,
})
minetest.register_abm({
label = "testabms:required_neighbor",
nodenames = "testabms:required_neighbor",
neighbors = {"testnodes:normal"},
interval = 1,
chance = 1,
action = function (pos)
minetest.swap_node(pos, {name="testabms:after_abm"})
local meta = minetest.get_meta(pos)
meta:set_string("infotext",
"ABM testabsm:required_neighbor changed this node.")
end
})
-- ABM missing neighbor node
minetest.register_node("testabms:missing_neighbor", {
description = S("Node for test ABM missing_neighbor.") .. "\n"
.. S("Sensitive neighbor node is testnodes:normal."),
drawtype = "normal",
tiles = { "testabms_wait_node.png" },
groups = { dig_immediate = 3 },
on_construct = function (pos)
local meta = minetest.get_meta(pos)
meta:set_string("infotext",
"Waiting for ABM testabms:missing_neighbor"
.. " (normal drawtype testnode sensitive)")
end,
})
minetest.register_abm({
label = "testabms:missing_neighbor",
nodenames = "testabms:missing_neighbor",
without_neighbors = {"testnodes:normal"},
interval = 1,
chance = 1,
action = function (pos)
minetest.swap_node(pos, {name="testabms:after_abm"})
local meta = minetest.get_meta(pos)
meta:set_string("infotext",
"ABM testabsm:missing_neighbor changed this node.")
end
})
-- ABM required and missing neighbor node
minetest.register_node("testabms:required_missing_neighbor", {
description = S("Node for test ABM required_missing_neighbor.") .. "\n"
.. S("Sensitive neighbor nodes are testnodes:normal and testnodes:glasslike."),
drawtype = "normal",
tiles = { "testabms_wait_node.png" },
groups = { dig_immediate = 3 },
on_construct = function (pos)
local meta = minetest.get_meta(pos)
meta:set_string("infotext",
"Waiting for ABM testabms:required_missing_neighbor"
.. " (wint normal drawtype testnode and no glasslike"
.. " drawtype testnode sensitive)")
end,
})
minetest.register_abm({
label = "testabms:required_missing_neighbor",
nodenames = "testabms:required_missing_neighbor",
neighbors = {"testnodes:normal"},
without_neighbors = {"testnodes:glasslike"},
interval = 1,
chance = 1,
action = function (pos)
minetest.swap_node(pos, {name="testabms:after_abm"})
local meta = minetest.get_meta(pos)
meta:set_string("infotext",
"ABM testabsm:required_missing_neighbor changed this node.")
end
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 B

View file

@ -299,7 +299,18 @@ local scroll_fs =
"scrollbaroptions[max=170]".. -- lowest seen pos is: 0.1*170+6=23 (factor*max+height)
"scrollbar[7.5,0;0.3,4;vertical;scrbar;0]"..
"scrollbar[8,0;0.3,4;vertical;scrbarhmmm;0]"..
"dropdown[0,6;2;hmdrpdwnnn;Outside,of,container;1]"
"dropdown[0,6;2;hmdrpdwnnn;Outside,of,container;1]"..
"scroll_container[0,8;10,4;scrbar420;vertical;0.1;2]"..
"button[0.5,0.5;10,1;;Container with padding=2]"..
"list[current_player;main;0,5;8,4;]"..
"scroll_container_end[]"..
"scrollbar[10.1,8;0.5,4;vertical;scrbar420;0]"..
-- Buttons for scale comparison
"button[11,8;1,1;;0]"..
"button[11,9;1,1;;1]"..
"button[11,10;1,1;;2]"..
"button[11,11;1,1;;3]"..
"button[11,12;1,1;;4]"
--style_type[label;textcolor=green]
--label[0,0;Green]
@ -462,7 +473,7 @@ mouse control = true]
]],
-- Scroll containers
"formspec_version[3]size[12,13]" ..
"formspec_version[7]size[12,13]" ..
scroll_fs,
-- Sound

View file

@ -98,6 +98,23 @@ minetest.register_node("testnodes:allfaces", {
groups = { dig_immediate = 3 },
})
minetest.register_node("testnodes:allfaces_6", {
description = S("\"allfaces 6 Textures\" Drawtype Test Node").."\n"..
S("Transparent node with visible internal backfaces"),
drawtype = "allfaces",
paramtype = "light",
tiles = {
"testnodes_allfaces.png^[colorize:red",
"testnodes_allfaces.png^[colorize:orange",
"testnodes_allfaces.png^[colorize:yellow",
"testnodes_allfaces.png^[colorize:green",
"testnodes_allfaces.png^[colorize:blue",
"testnodes_allfaces.png^[colorize:purple"
},
groups = { dig_immediate = 3 },
})
local allfaces_optional_tooltip = ""..
S("Rendering depends on 'leaves_style' setting:").."\n"..
S("* 'fancy': transparent with visible internal backfaces").."\n"..

View file

@ -52,6 +52,12 @@ minetest.register_node("testnodes:fill_positioning_reference", {
groups = {dig_immediate = 3},
})
minetest.register_node("testnodes:modifier_mask", {
description = S("[mask Modifier Test Node"),
tiles = {"testnodes_128x128_rgb.png^[mask:testnodes_mask_WRGBKW.png"},
groups = {dig_immediate = 3},
})
-- Node texture transparency test
local alphas = { 64, 128, 191 }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

View file

@ -22,9 +22,11 @@ local function do_tests()
assert(core.registered_items["unittests:description_test"].on_place == true)
end
-- there's no (usable) communcation path between mapgen and the regular env
-- so we just run the test unconditionally
do_tests()
-- first thread to get here runs the tests
if core.ipc_cas("unittests:mg_once", nil, true) then
-- this is checked from the main env
core.ipc_set("unittests:mg", { pcall(do_tests) })
end
core.register_on_generated(function(vm, pos1, pos2, blockseed)
local n = tonumber(core.get_mapgen_setting("chunksize")) * 16 - 1

View file

@ -254,3 +254,43 @@ local function test_gennotify_api()
assert(#custom == 0, "custom ids not empty")
end
unittests.register("test_gennotify_api", test_gennotify_api)
-- <=> inside_mapgen_env.lua
local function test_mapgen_env(cb)
-- emerge threads start delayed so this can take a second
local res = core.ipc_get("unittests:mg")
if res == nil then
return core.after(0, test_mapgen_env, cb)
end
-- handle error status
if res[1] then
cb()
else
cb(res[2])
end
end
unittests.register("test_mapgen_env", test_mapgen_env, {async=true})
local function test_ipc_vector_preserve(cb)
-- the IPC also uses register_portable_metatable
core.ipc_set("unittests:v", vector.new(4, 0, 4))
local v = core.ipc_get("unittests:v")
assert(type(v) == "table")
assert(vector.check(v))
end
unittests.register("test_ipc_vector_preserve", test_ipc_vector_preserve)
local function test_ipc_poll(cb)
core.ipc_set("unittests:flag", nil)
assert(core.ipc_poll("unittests:flag", 1) == false)
-- Note that unlike the async result callback - which has to wait for the
-- next server step - the IPC is instant
local t0 = core.get_us_time()
core.handle_async(function()
core.ipc_set("unittests:flag", true)
end, function() end)
assert(core.ipc_poll("unittests:flag", 1000) == true, "Wait failed (or slow machine?)")
print("delta: " .. (core.get_us_time() - t0) .. "us")
end
unittests.register("test_ipc_poll", test_ipc_poll)

View file

@ -42,41 +42,97 @@ unittests.register("test_hpchangereason", run_hpchangereason_tests, {player=true
--
local expected_diff = nil
local hpchange_counter = 0
local die_counter = 0
core.register_on_player_hpchange(function(player, hp_change, reason)
if expected_diff then
assert(hp_change == expected_diff)
hpchange_counter = hpchange_counter + 1
end
end)
core.register_on_dieplayer(function()
die_counter = die_counter + 1
end)
local function hp_diference_test(player, hp_max)
assert(hp_max >= 22)
local function run_hp_difference_tests(player)
local old_hp = player:get_hp()
local old_hp_max = player:get_properties().hp_max
expected_diff = nil
player:set_properties({hp_max = 30})
player:set_hp(22)
hpchange_counter = 0
die_counter = 0
-- final HP value is clamped to >= 0 before difference calculation
expected_diff = -22
expected_diff = nil
player:set_properties({hp_max = hp_max})
player:set_hp(22)
assert(player:get_hp() == 22)
assert(hpchange_counter == 0)
assert(die_counter == 0)
-- HP difference is not clamped
expected_diff = -25
player:set_hp(-3)
-- and actual final HP value is clamped to >= 0 too
-- actual final HP value is clamped to >= 0
assert(player:get_hp() == 0)
assert(hpchange_counter == 1)
assert(die_counter == 1)
expected_diff = 22
player:set_hp(22)
assert(player:get_hp() == 22)
assert(hpchange_counter == 2)
assert(die_counter == 1)
-- final HP value is clamped to <= U16_MAX before difference calculation
expected_diff = 65535 - 22
-- Integer overflow is prevented
-- so result is S32_MIN, not S32_MIN - 22
expected_diff = -2147483648
player:set_hp(-2147483648)
-- actual final HP value is clamped to >= 0
assert(player:get_hp() == 0)
assert(hpchange_counter == 3)
assert(die_counter == 2)
-- Damage is ignored if player is already dead (hp == 0)
expected_diff = "never equal"
player:set_hp(-11)
assert(player:get_hp() == 0)
-- no on_player_hpchange or on_dieplayer call expected
assert(hpchange_counter == 3)
assert(die_counter == 2)
expected_diff = 11
player:set_hp(11)
assert(player:get_hp() == 11)
assert(hpchange_counter == 4)
assert(die_counter == 2)
-- HP difference is not clamped
expected_diff = 1000000 - 11
player:set_hp(1000000)
-- and actual final HP value is clamped to <= hp_max
assert(player:get_hp() == 30)
-- actual final HP value is clamped to <= hp_max
assert(player:get_hp() == hp_max)
assert(hpchange_counter == 5)
assert(die_counter == 2)
-- "Healing" is not ignored when hp == hp_max
expected_diff = 80000 - hp_max
player:set_hp(80000)
assert(player:get_hp() == hp_max)
-- on_player_hpchange_call expected
assert(hpchange_counter == 6)
assert(die_counter == 2)
expected_diff = nil
player:set_properties({hp_max = old_hp_max})
player:set_hp(old_hp)
core.close_formspec(player:get_player_name(), "") -- hide death screen
end
local function run_hp_difference_tests(player)
hp_diference_test(player, 22)
hp_diference_test(player, 30)
hp_diference_test(player, 65535) -- U16_MAX
end
unittests.register("test_hp_difference", run_hp_difference_tests, {player=true})
--

View file

@ -19,11 +19,8 @@ irr::scene::SMeshBuffer etc. */
class IAnimatedMesh : public IMesh
{
public:
//! Gets the frame count of the animated mesh.
/** Note that the play-time is usually getFrameCount()-1 as it stops as soon as the last frame-key is reached.
\return The amount of frames. If the amount is 1,
it is a static, non animated mesh. */
virtual u32 getFrameCount() const = 0;
//! Gets the maximum frame number, 0 if the mesh is static.
virtual f32 getMaxFrameNumber() const = 0;
//! Gets the animation speed of the animated mesh.
/** \return The number of frames per second to play the
@ -39,19 +36,10 @@ public:
virtual void setAnimationSpeed(f32 fps) = 0;
//! Returns the IMesh interface for a frame.
/** \param frame: Frame number as zero based index. The maximum
frame number is getFrameCount() - 1;
\param detailLevel: Level of detail. 0 is the lowest, 255 the
highest level of detail. Most meshes will ignore the detail level.
\param startFrameLoop: Because some animated meshes (.MD2) are
blended between 2 static frames, and maybe animated in a loop,
the startFrameLoop and the endFrameLoop have to be defined, to
prevent the animation to be blended between frames which are
outside of this loop.
If startFrameLoop and endFrameLoop are both -1, they are ignored.
\param endFrameLoop: see startFrameLoop.
\return Returns the animated mesh based on a detail level. */
virtual IMesh *getMesh(s32 frame, s32 detailLevel = 255, s32 startFrameLoop = -1, s32 endFrameLoop = -1) = 0;
/** \param frame: Frame number, >= 0, <= getMaxFrameNumber()
Linear interpolation is used if this is between two frames.
\return Returns the animated mesh for the given frame */
virtual IMesh *getMesh(f32 frame) = 0;
//! Returns the type of the animated mesh.
/** In most cases it is not necessary to use this method.

View file

@ -63,7 +63,7 @@ public:
virtual void setCurrentFrame(f32 frame) = 0;
//! Sets the frame numbers between the animation is looped.
/** The default is 0 to getFrameCount()-1 of the mesh.
/** The default is 0 to getMaxFrameNumber() of the mesh.
Number of played frames is end-start.
It interpolates toward the last frame but stops when it is reached.
It does not interpolate back to start even when looping.
@ -71,7 +71,7 @@ public:
\param begin: Start frame number of the loop.
\param end: End frame number of the loop.
\return True if successful, false if not. */
virtual bool setFrameLoop(s32 begin, s32 end) = 0;
virtual bool setFrameLoop(f32 begin, f32 end) = 0;
//! Sets the speed with which the animation is played.
/** \param framesPerSecond: Frames per second played. */
@ -108,9 +108,9 @@ public:
//! Returns the currently displayed frame number.
virtual f32 getFrameNr() const = 0;
//! Returns the current start frame number.
virtual s32 getStartFrame() const = 0;
virtual f32 getStartFrame() const = 0;
//! Returns the current end frame number.
virtual s32 getEndFrame() const = 0;
virtual f32 getEndFrame() const = 0;
//! Sets looping mode which is on by default.
/** If set to false, animations will not be played looped. */

View file

@ -347,6 +347,9 @@ struct SEvent
//! Type of mouse event
EMOUSE_INPUT_EVENT Event;
//! Is this a simulated mouse event generated by Minetest itself?
bool Simulated;
};
//! Any kind of keyboard event.
@ -538,6 +541,11 @@ struct SEvent
struct SUserEvent UserEvent;
struct SApplicationEvent ApplicationEvent;
};
SEvent() {
// would be left uninitialized in many places otherwise
MouseInput.Simulated = false;
}
};
//! Interface of an object which can receive events.

View file

@ -159,15 +159,17 @@ public:
core::array<SWeight> Weights;
//! Unnecessary for loaders, will be overwritten on finalize
core::matrix4 GlobalMatrix;
core::matrix4 GlobalMatrix; // loaders may still choose to set this (temporarily) to calculate absolute vertex data.
core::matrix4 GlobalAnimatedMatrix;
core::matrix4 LocalAnimatedMatrix;
//! These should be set by loaders.
core::vector3df Animatedposition;
core::vector3df Animatedscale;
core::quaternion Animatedrotation;
core::matrix4 GlobalInversedMatrix; // the x format pre-calculates this
// The .x and .gltf formats pre-calculate this
std::optional<core::matrix4> GlobalInversedMatrix;
private:
//! Internal members used by CSkinnedMesh
friend class CSkinnedMesh;

View file

@ -36,11 +36,9 @@ struct SAnimatedMesh final : public IAnimatedMesh
mesh->drop();
}
//! Gets the frame count of the animated mesh.
/** \return Amount of frames. If the amount is 1, it is a static, non animated mesh. */
u32 getFrameCount() const override
f32 getMaxFrameNumber() const override
{
return static_cast<u32>(Meshes.size());
return static_cast<f32>(Meshes.size() - 1);
}
//! Gets the default animation speed of the animated mesh.
@ -59,19 +57,14 @@ struct SAnimatedMesh final : public IAnimatedMesh
}
//! Returns the IMesh interface for a frame.
/** \param frame: Frame number as zero based index. The maximum frame number is
getFrameCount() - 1;
\param detailLevel: Level of detail. 0 is the lowest,
255 the highest level of detail. Most meshes will ignore the detail level.
\param startFrameLoop: start frame
\param endFrameLoop: end frame
\return The animated mesh based on a detail level. */
IMesh *getMesh(s32 frame, s32 detailLevel = 255, s32 startFrameLoop = -1, s32 endFrameLoop = -1) override
/** \param frame: Frame number as zero based index.
\return The animated mesh based for the given frame */
IMesh *getMesh(f32 frame) override
{
if (Meshes.empty())
return 0;
return nullptr;
return Meshes[frame];
return Meshes[static_cast<s32>(frame)];
}
//! adds a Mesh

View file

@ -410,6 +410,7 @@ public:
{
bool different =
MaterialType != b.MaterialType ||
ColorParam != b.ColorParam ||
MaterialTypeParam != b.MaterialTypeParam ||
Thickness != b.Thickness ||
Wireframe != b.Wireframe ||

View file

@ -63,7 +63,7 @@ inline io::path &getFileNameExtension(io::path &dest, const io::path &source)
}
//! delete path from filename
inline io::path &deletePathFromFilename(io::path &filename)
inline io::path deletePathFromFilename(const io::path &filename)
{
// delete path from filename
const fschar_t *s = filename.c_str();
@ -73,11 +73,10 @@ inline io::path &deletePathFromFilename(io::path &filename)
while (*p != '/' && *p != '\\' && p != s)
p--;
if (p != s) {
if (p != s)
++p;
filename = p;
}
return filename;
return p;
}
//! trim paths

View file

@ -173,13 +173,24 @@ public:
return *this;
}
// no longer allowed!
_IRR_DEBUG_BREAK_IF((void *)c == (void *)c_str());
if constexpr (sizeof(T) != sizeof(B)) {
_IRR_DEBUG_BREAK_IF(
(uintptr_t)c >= (uintptr_t)(str.data()) &&
(uintptr_t)c < (uintptr_t)(str.data() + str.size()));
}
if ((void *)c == (void *)c_str())
return *this;
u32 len = calclen(c);
// In case `c` is a pointer to our own buffer, we may not resize first
// or it can become invalid.
if (len > str.size())
str.resize(len);
for (u32 l = 0; l < len; ++l)
str[l] = (T)c[l];
str[l] = static_cast<T>(c[l]);
if (len < str.size())
str.resize(len);
return *this;
}

View file

@ -24,7 +24,12 @@ namespace core
{
//! 4x4 matrix. Mostly used as transformation matrix for 3d calculations.
/** The matrix is a D3D style matrix, row major with translations in the 4th row. */
/** Conventions: Matrices are considered to be in row-major order.
* Multiplication of a matrix A with a row vector v is the premultiplication vA.
* Translations are thus in the 4th row.
* The matrix product AB yields a matrix C such that vC = (vB)A:
* B is applied first, then A.
*/
template <class T>
class CMatrix4
{
@ -242,17 +247,11 @@ public:
//! Translate a vector by the inverse of the translation part of this matrix.
void inverseTranslateVect(vector3df &vect) const;
//! Rotate a vector by the inverse of the rotation part of this matrix.
void inverseRotateVect(vector3df &vect) const;
//! Scale a vector, then rotate by the inverse of the rotation part of this matrix.
[[nodiscard]] vector3d<T> scaleThenInvRotVect(const vector3d<T> &vect) const;
//! Rotate a vector by the rotation part of this matrix.
void rotateVect(vector3df &vect) const;
//! An alternate transform vector method, writing into a second vector
void rotateVect(core::vector3df &out, const core::vector3df &in) const;
//! An alternate transform vector method, writing into an array of 3 floats
void rotateVect(T *out, const core::vector3df &in) const;
//! Rotate and scale a vector. Applies both rotation & scale part of the matrix.
[[nodiscard]] vector3d<T> rotateAndScaleVect(const vector3d<T> &vect) const;
//! Transforms the vector by this matrix
/** This operation is performed as if the vector was 4d with the 4th component =1 */
@ -1154,39 +1153,23 @@ inline bool CMatrix4<T>::isIdentity_integer_base() const
}
template <class T>
inline void CMatrix4<T>::rotateVect(vector3df &vect) const
inline vector3d<T> CMatrix4<T>::rotateAndScaleVect(const vector3d<T> &v) const
{
vector3d<T> tmp(static_cast<T>(vect.X), static_cast<T>(vect.Y), static_cast<T>(vect.Z));
vect.X = static_cast<f32>(tmp.X * M[0] + tmp.Y * M[4] + tmp.Z * M[8]);
vect.Y = static_cast<f32>(tmp.X * M[1] + tmp.Y * M[5] + tmp.Z * M[9]);
vect.Z = static_cast<f32>(tmp.X * M[2] + tmp.Y * M[6] + tmp.Z * M[10]);
}
//! An alternate transform vector method, writing into a second vector
template <class T>
inline void CMatrix4<T>::rotateVect(core::vector3df &out, const core::vector3df &in) const
{
out.X = in.X * M[0] + in.Y * M[4] + in.Z * M[8];
out.Y = in.X * M[1] + in.Y * M[5] + in.Z * M[9];
out.Z = in.X * M[2] + in.Y * M[6] + in.Z * M[10];
}
//! An alternate transform vector method, writing into an array of 3 floats
template <class T>
inline void CMatrix4<T>::rotateVect(T *out, const core::vector3df &in) const
{
out[0] = in.X * M[0] + in.Y * M[4] + in.Z * M[8];
out[1] = in.X * M[1] + in.Y * M[5] + in.Z * M[9];
out[2] = in.X * M[2] + in.Y * M[6] + in.Z * M[10];
return {
v.X * M[0] + v.Y * M[4] + v.Z * M[8],
v.X * M[1] + v.Y * M[5] + v.Z * M[9],
v.X * M[2] + v.Y * M[6] + v.Z * M[10]
};
}
template <class T>
inline void CMatrix4<T>::inverseRotateVect(vector3df &vect) const
inline vector3d<T> CMatrix4<T>::scaleThenInvRotVect(const vector3d<T> &v) const
{
vector3d<T> tmp(static_cast<T>(vect.X), static_cast<T>(vect.Y), static_cast<T>(vect.Z));
vect.X = static_cast<f32>(tmp.X * M[0] + tmp.Y * M[1] + tmp.Z * M[2]);
vect.Y = static_cast<f32>(tmp.X * M[4] + tmp.Y * M[5] + tmp.Z * M[6]);
vect.Z = static_cast<f32>(tmp.X * M[8] + tmp.Y * M[9] + tmp.Z * M[10]);
return {
v.X * M[0] + v.Y * M[1] + v.Z * M[2],
v.X * M[4] + v.Y * M[5] + v.Z * M[6],
v.X * M[8] + v.Y * M[9] + v.Z * M[10]
};
}
template <class T>
@ -1247,8 +1230,7 @@ inline void CMatrix4<T>::transformPlane(core::plane3d<f32> &plane) const
// Transform the normal by the transposed inverse of the matrix
CMatrix4<T> transposedInverse(*this, EM4CONST_INVERSE_TRANSPOSED);
vector3df normal = plane.Normal;
transposedInverse.rotateVect(normal);
vector3df normal = transposedInverse.rotateAndScaleVect(plane.Normal);
plane.setPlane(member, normal.normalize());
}

View file

@ -8,6 +8,7 @@
#include "dimension2d.h"
#include <functional>
#include <array>
namespace irr
{
@ -34,6 +35,15 @@ public:
constexpr vector2d(const dimension2d<T> &other) :
X(other.Width), Y(other.Height) {}
explicit constexpr vector2d(const std::array<T, 2> &arr) :
X(arr[0]), Y(arr[1]) {}
template <class U>
constexpr static vector2d<T> from(const vector2d<U> &other)
{
return {static_cast<T>(other.X), static_cast<T>(other.Y)};
}
// operators
vector2d<T> operator-() const { return vector2d<T>(-X, -Y); }

View file

@ -16,6 +16,7 @@
#include "IAnimatedMesh.h"
#include "IFileSystem.h"
#include "quaternion.h"
#include <algorithm>
namespace irr
{
@ -80,7 +81,7 @@ void CAnimatedMeshSceneNode::buildFrameNr(u32 timeMs)
}
if (StartFrame == EndFrame) {
CurrentFrameNr = (f32)StartFrame; // Support for non animated meshes
CurrentFrameNr = StartFrame; // Support for non animated meshes
} else if (Looping) {
// play animation looped
CurrentFrameNr += timeMs * FramesPerSecond;
@ -89,26 +90,26 @@ void CAnimatedMeshSceneNode::buildFrameNr(u32 timeMs)
// the last frame must be identical to first one with our current solution.
if (FramesPerSecond > 0.f) { // forwards...
if (CurrentFrameNr > EndFrame)
CurrentFrameNr = StartFrame + fmodf(CurrentFrameNr - StartFrame, (f32)(EndFrame - StartFrame));
CurrentFrameNr = StartFrame + fmodf(CurrentFrameNr - StartFrame, EndFrame - StartFrame);
} else // backwards...
{
if (CurrentFrameNr < StartFrame)
CurrentFrameNr = EndFrame - fmodf(EndFrame - CurrentFrameNr, (f32)(EndFrame - StartFrame));
CurrentFrameNr = EndFrame - fmodf(EndFrame - CurrentFrameNr, EndFrame - StartFrame);
}
} else {
// play animation non looped
CurrentFrameNr += timeMs * FramesPerSecond;
if (FramesPerSecond > 0.f) { // forwards...
if (CurrentFrameNr > (f32)EndFrame) {
CurrentFrameNr = (f32)EndFrame;
if (CurrentFrameNr > EndFrame) {
CurrentFrameNr = EndFrame;
if (LoopCallBack)
LoopCallBack->OnAnimationEnd(this);
}
} else // backwards...
{
if (CurrentFrameNr < (f32)StartFrame) {
CurrentFrameNr = (f32)StartFrame;
if (CurrentFrameNr < StartFrame) {
CurrentFrameNr = StartFrame;
if (LoopCallBack)
LoopCallBack->OnAnimationEnd(this);
}
@ -159,9 +160,7 @@ void CAnimatedMeshSceneNode::OnRegisterSceneNode()
IMesh *CAnimatedMeshSceneNode::getMeshForCurrentFrame()
{
if (Mesh->getMeshType() != EAMT_SKINNED) {
s32 frameNr = (s32)getFrameNr();
s32 frameBlend = (s32)(core::fract(getFrameNr()) * 1000.f);
return Mesh->getMesh(frameNr, frameBlend, StartFrame, EndFrame);
return Mesh->getMesh(getFrameNr());
} else {
// As multiple scene nodes may be sharing the same skinned mesh, we have to
// re-animate it every frame to ensure that this node gets the mesh that it needs.
@ -331,33 +330,33 @@ void CAnimatedMeshSceneNode::render()
}
//! Returns the current start frame number.
s32 CAnimatedMeshSceneNode::getStartFrame() const
f32 CAnimatedMeshSceneNode::getStartFrame() const
{
return StartFrame;
}
//! Returns the current start frame number.
s32 CAnimatedMeshSceneNode::getEndFrame() const
f32 CAnimatedMeshSceneNode::getEndFrame() const
{
return EndFrame;
}
//! sets the frames between the animation is looped.
//! the default is 0 - MaximalFrameCount of the mesh.
bool CAnimatedMeshSceneNode::setFrameLoop(s32 begin, s32 end)
bool CAnimatedMeshSceneNode::setFrameLoop(f32 begin, f32 end)
{
const s32 maxFrameCount = Mesh->getFrameCount() - 1;
const f32 maxFrame = Mesh->getMaxFrameNumber();
if (end < begin) {
StartFrame = core::s32_clamp(end, 0, maxFrameCount);
EndFrame = core::s32_clamp(begin, StartFrame, maxFrameCount);
StartFrame = std::clamp<f32>(end, 0, maxFrame);
EndFrame = std::clamp<f32>(begin, StartFrame, maxFrame);
} else {
StartFrame = core::s32_clamp(begin, 0, maxFrameCount);
EndFrame = core::s32_clamp(end, StartFrame, maxFrameCount);
StartFrame = std::clamp<f32>(begin, 0, maxFrame);
EndFrame = std::clamp<f32>(end, StartFrame, maxFrame);
}
if (FramesPerSecond < 0)
setCurrentFrame((f32)EndFrame);
setCurrentFrame(EndFrame);
else
setCurrentFrame((f32)StartFrame);
setCurrentFrame(StartFrame);
return true;
}
@ -532,7 +531,7 @@ void CAnimatedMeshSceneNode::setMesh(IAnimatedMesh *mesh)
// get materials and bounding box
Box = Mesh->getBoundingBox();
IMesh *m = Mesh->getMesh(0, 0);
IMesh *m = Mesh->getMesh(0);
if (m) {
Materials.clear();
Materials.reallocate(m->getMeshBufferCount());
@ -554,7 +553,7 @@ void CAnimatedMeshSceneNode::setMesh(IAnimatedMesh *mesh)
// get start and begin time
setAnimationSpeed(Mesh->getAnimationSpeed()); // NOTE: This had been commented out (but not removed!) in r3526. Which caused meshloader-values for speed to be ignored unless users specified explicitly. Missing a test-case where this could go wrong so I put the code back in.
setFrameLoop(0, Mesh->getFrameCount() - 1);
setFrameLoop(0, Mesh->getMaxFrameNumber());
}
//! updates the absolute position based on the relative and the parents position

View file

@ -45,7 +45,7 @@ public:
//! sets the frames between the animation is looped.
//! the default is 0 - MaximalFrameCount of the mesh.
//! NOTE: setMesh will also change this value and set it to the full range of animations of the mesh
bool setFrameLoop(s32 begin, s32 end) override;
bool setFrameLoop(f32 begin, f32 end) override;
//! Sets looping mode which is on by default. If set to false,
//! animations will not be looped.
@ -93,9 +93,9 @@ public:
//! Returns the current displayed frame number.
f32 getFrameNr() const override;
//! Returns the current start frame number.
s32 getStartFrame() const override;
f32 getStartFrame() const override;
//! Returns the current end frame number.
s32 getEndFrame() const override;
f32 getEndFrame() const override;
//! Sets if the scene node should not copy the materials of the mesh but use them in a read only style.
/* In this way it is possible to change the materials a mesh causing all mesh scene nodes
@ -148,8 +148,8 @@ private:
core::aabbox3d<f32> Box;
IAnimatedMesh *Mesh;
s32 StartFrame;
s32 EndFrame;
f32 StartFrame;
f32 EndFrame;
f32 FramesPerSecond;
f32 CurrentFrameNr;

View file

@ -389,7 +389,8 @@ bool CB3DMeshFileLoader::readChunkVRTS(CSkinnedMesh::SJoint *inJoint)
// Transform the Vertex position by nested node...
inJoint->GlobalMatrix.transformVect(Vertex.Pos);
inJoint->GlobalMatrix.rotateVect(Vertex.Normal);
Vertex.Normal = inJoint->GlobalMatrix.rotateAndScaleVect(Vertex.Normal);
Vertex.Normal.normalize(); // renormalize: normal might have been skewed by scaling
// Add it...
BaseVertices.push_back(Vertex);

View file

@ -80,7 +80,7 @@ u32 CFileList::addItem(const io::path &fullPath, u32 offset, u32 size, bool isDi
entry.FullName = entry.Name;
core::deletePathFromFilename(entry.Name);
entry.Name = core::deletePathFromFilename(entry.Name);
if (IgnorePaths)
entry.FullName = entry.Name;
@ -140,7 +140,7 @@ s32 CFileList::findFile(const io::path &filename, bool isDirectory = false) cons
entry.FullName.make_lower();
if (IgnorePaths)
core::deletePathFromFilename(entry.FullName);
entry.FullName = core::deletePathFromFilename(entry.FullName);
return Files.binary_search(entry);
}

View file

@ -3,19 +3,19 @@
#include "CGLTFMeshFileLoader.h"
#include "SMaterialLayer.h"
#include "coreutil.h"
#include "CSkinnedMesh.h"
#include "ISkinnedMesh.h"
#include "irrTypes.h"
#include "IAnimatedMesh.h"
#include "IReadFile.h"
#include "irrTypes.h"
#include "matrix4.h"
#include "path.h"
#include "quaternion.h"
#include "vector2d.h"
#include "vector3d.h"
#include "os.h"
#include "tiniergltf.hpp"
#include <array>
#include <cstddef>
#include <cstring>
@ -23,9 +23,11 @@
#include <memory>
#include <optional>
#include <stdexcept>
#include <tuple>
#include <utility>
#include <variant>
#include <vector>
#include <iostream>
namespace irr {
@ -51,6 +53,28 @@ core::vector3df convertHandedness(const core::vector3df &p)
return core::vector3df(p.X, p.Y, -p.Z);
}
template <>
core::quaternion convertHandedness(const core::quaternion &q)
{
return core::quaternion(q.X, q.Y, -q.Z, q.W);
}
template <>
core::matrix4 convertHandedness(const core::matrix4 &mat)
{
// Base transformation between left & right handed coordinate systems.
static const core::matrix4 invertZ = core::matrix4(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, -1, 0,
0, 0, 0, 1);
// Convert from left-handed to right-handed,
// then apply mat,
// then convert from right-handed to left-handed.
// Both conversions just invert Z.
return invertZ * mat * invertZ;
}
namespace scene {
using SelfType = CGLTFMeshFileLoader;
@ -196,6 +220,8 @@ ACCESSOR_PRIMITIVE(u16, UNSIGNED_SHORT)
ACCESSOR_PRIMITIVE(u32, UNSIGNED_INT)
ACCESSOR_TYPES(core::vector3df, VEC3, FLOAT)
ACCESSOR_TYPES(core::quaternion, VEC4, FLOAT)
ACCESSOR_TYPES(core::matrix4, MAT4, FLOAT)
template <class T>
T SelfType::Accessor<T>::get(std::size_t i) const
@ -303,13 +329,11 @@ std::array<f32, N> SelfType::getNormalizedValues(
return values;
}
/**
* The most basic portion of the code base. This tells irllicht if this file has a .gltf extension.
*/
bool SelfType::isALoadableFileExtension(
const io::path& filename) const
{
return core::hasFileExtension(filename, "gltf");
return core::hasFileExtension(filename, "gltf") ||
core::hasFileExtension(filename, "glb");
}
/**
@ -324,6 +348,11 @@ IAnimatedMesh* SelfType::createMesh(io::IReadFile* file)
if (!model.has_value()) {
return nullptr;
}
if (model->extensionsRequired) {
os::Printer::log("glTF loader",
"model requires extensions, but we support none", ELL_ERROR);
return nullptr;
}
if (!(model->buffers.has_value()
&& model->bufferViews.has_value()
@ -337,7 +366,7 @@ IAnimatedMesh* SelfType::createMesh(io::IReadFile* file)
auto *mesh = new CSkinnedMesh();
MeshExtractor parser(std::move(model.value()), mesh);
try {
parser.loadNodes();
parser.load();
} catch (std::runtime_error &e) {
os::Printer::log("glTF loader", e.what(), ELL_ERROR);
mesh->drop();
@ -354,8 +383,7 @@ static void transformVertices(std::vector<video::S3DVertex> &vertices, const cor
// Apply scaling, rotation and rotation (in that order) to the position.
transform.transformVect(vertex.Pos);
// For the normal, we do not want to apply the translation.
// TODO note that this also applies scaling; the Irrlicht method is misnamed.
transform.rotateVect(vertex.Normal);
vertex.Normal = transform.rotateAndScaleVect(vertex.Normal);
// Renormalize (length might have been affected by scaling).
vertex.Normal.normalize();
}
@ -381,23 +409,33 @@ static std::vector<u16> generateIndices(const std::size_t nVerts)
return indices;
}
/**
* Load up the rawest form of the model. The vertex positions and indices.
* Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes
* If material is undefined, then a default material MUST be used.
*/
void SelfType::MeshExtractor::loadMesh(
const std::size_t meshIdx,
ISkinnedMesh::SJoint *parent) const
using Wrap = tiniergltf::Sampler::Wrap;
static video::E_TEXTURE_CLAMP convertTextureWrap(const Wrap wrap) {
switch (wrap) {
case Wrap::REPEAT:
return video::ETC_REPEAT;
case Wrap::CLAMP_TO_EDGE:
return video::ETC_CLAMP_TO_EDGE;
case Wrap::MIRRORED_REPEAT:
return video::ETC_MIRROR;
default:
throw std::runtime_error("invalid sampler wrapping mode");
}
}
void SelfType::MeshExtractor::addPrimitive(
const tiniergltf::MeshPrimitive &primitive,
const std::optional<std::size_t> skinIdx,
CSkinnedMesh::SJoint *parent)
{
for (std::size_t pi = 0; pi < getPrimitiveCount(meshIdx); ++pi) {
const auto &primitive = m_gltf_model.meshes->at(meshIdx).primitives.at(pi);
auto vertices = getVertices(primitive);
if (!vertices.has_value())
continue; // "When positions are not specified, client implementations SHOULD skip primitives rendering"
return; // "When positions are not specified, client implementations SHOULD skip primitives rendering"
const auto n_vertices = vertices->size();
// Excludes the max value for consistency.
if (vertices->size() >= std::numeric_limits<u16>::max())
if (n_vertices >= std::numeric_limits<u16>::max())
throw std::runtime_error("too many vertices");
// Apply the global transform along the parent chain.
@ -415,18 +453,104 @@ void SelfType::MeshExtractor::loadMesh(
m_irr_model->addMeshBuffer(
new SSkinMeshBuffer(std::move(*vertices), std::move(indices)));
const auto meshbufNr = m_irr_model->getMeshBufferCount() - 1;
auto *meshbuf = m_irr_model->getMeshBuffer(meshbufNr);
if (primitive.material.has_value()) {
const auto &material = m_gltf_model.materials->at(*primitive.material);
if (material.pbrMetallicRoughness.has_value()) {
const auto &texture = material.pbrMetallicRoughness->baseColorTexture;
if (texture.has_value()) {
const auto meshbufNr = m_irr_model->getMeshBufferCount() - 1;
m_irr_model->setTextureSlot(meshbufNr, static_cast<u32>(texture->index));
const auto samplerIdx = m_gltf_model.textures->at(texture->index).sampler;
if (samplerIdx.has_value()) {
auto &sampler = m_gltf_model.samplers->at(*samplerIdx);
auto &layer = meshbuf->getMaterial().TextureLayers[0];
layer.TextureWrapU = convertTextureWrap(sampler.wrapS);
layer.TextureWrapV = convertTextureWrap(sampler.wrapT);
}
}
}
}
if (!skinIdx.has_value()) {
// No skin => all vertices belong entirely to their parent
for (std::size_t v = 0; v < n_vertices; ++v) {
auto *weight = m_irr_model->addWeight(parent);
weight->buffer_id = meshbufNr;
weight->vertex_id = v;
weight->strength = 1.0f;
}
return;
}
const auto &skin = m_gltf_model.skins->at(*skinIdx);
const auto &attrs = primitive.attributes;
const auto &joints = attrs.joints;
if (!joints.has_value())
return;
const auto &weights = attrs.weights;
for (std::size_t set = 0; set < joints->size(); ++set) {
const auto jointAccessor = ([&]() -> ArrayAccessorVariant<4, u8, u16> {
const auto idx = joints->at(set);
const auto &acc = m_gltf_model.accessors->at(idx);
switch (acc.componentType) {
case tiniergltf::Accessor::ComponentType::UNSIGNED_BYTE:
return Accessor<std::array<u8, 4>>::make(m_gltf_model, idx);
case tiniergltf::Accessor::ComponentType::UNSIGNED_SHORT:
return Accessor<std::array<u16, 4>>::make(m_gltf_model, idx);
default:
throw std::runtime_error("invalid component type");
}
})();
const auto weightAccessor = createNormalizedValuesAccessor<4>(m_gltf_model, weights->at(set));
for (std::size_t v = 0; v < n_vertices; ++v) {
std::array<u16, 4> jointIdxs;
if (std::holds_alternative<Accessor<std::array<u8, 4>>>(jointAccessor)) {
const auto jointIdxsU8 = std::get<Accessor<std::array<u8, 4>>>(jointAccessor).get(v);
jointIdxs = {jointIdxsU8[0], jointIdxsU8[1], jointIdxsU8[2], jointIdxsU8[3]};
} else if (std::holds_alternative<Accessor<std::array<u16, 4>>>(jointAccessor)) {
jointIdxs = std::get<Accessor<std::array<u16, 4>>>(jointAccessor).get(v);
}
std::array<f32, 4> strengths = getNormalizedValues(weightAccessor, v);
// 4 joints per set
for (std::size_t in_set = 0; in_set < 4; ++in_set) {
u16 jointIdx = jointIdxs[in_set];
f32 strength = strengths[in_set];
if (strength == 0)
continue;
CSkinnedMesh::SWeight *weight = m_irr_model->addWeight(m_loaded_nodes.at(skin.joints.at(jointIdx)));
weight->buffer_id = meshbufNr;
weight->vertex_id = v;
weight->strength = strength;
}
}
}
}
/**
* Load up the rawest form of the model. The vertex positions and indices.
* Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes
* If material is undefined, then a default material MUST be used.
*/
void SelfType::MeshExtractor::deferAddMesh(
const std::size_t meshIdx,
const std::optional<std::size_t> skinIdx,
CSkinnedMesh::SJoint *parent)
{
m_mesh_loaders.emplace_back([=] {
for (std::size_t pi = 0; pi < getPrimitiveCount(meshIdx); ++pi) {
const auto &primitive = m_gltf_model.meshes->at(meshIdx).primitives.at(pi);
addPrimitive(primitive, skinIdx, parent);
}
});
}
// Base transformation between left & right handed coordinate systems.
@ -439,51 +563,75 @@ static const core::matrix4 leftToRight = core::matrix4(
);
static const core::matrix4 rightToLeft = leftToRight;
static core::matrix4 loadTransform(const tiniergltf::Node::Matrix &m)
static core::matrix4 loadTransform(const tiniergltf::Node::Matrix &m, CSkinnedMesh::SJoint *joint)
{
// Note: Under the hood, this casts these doubles to floats.
return core::matrix4(
core::matrix4 mat = convertHandedness(core::matrix4(
m[0], m[1], m[2], m[3],
m[4], m[5], m[6], m[7],
m[8], m[9], m[10], m[11],
m[12], m[13], m[14], m[15]);
m[12], m[13], m[14], m[15]));
// Decompose the matrix into translation, scale, and rotation.
joint->Animatedposition = mat.getTranslation();
auto scale = mat.getScale();
joint->Animatedscale = scale;
core::matrix4 inverseScale;
inverseScale.setScale(core::vector3df(
scale.X == 0 ? 0 : 1 / scale.X,
scale.Y == 0 ? 0 : 1 / scale.Y,
scale.Z == 0 ? 0 : 1 / scale.Z));
core::matrix4 axisNormalizedMat = inverseScale * mat;
joint->Animatedrotation = axisNormalizedMat.getRotationDegrees();
// Invert the rotation because it is applied using `getMatrix_transposed`,
// which again inverts.
joint->Animatedrotation.makeInverse();
return mat;
}
static core::matrix4 loadTransform(const tiniergltf::Node::TRS &trs)
static core::matrix4 loadTransform(const tiniergltf::Node::TRS &trs, CSkinnedMesh::SJoint *joint)
{
const auto &trans = trs.translation;
const auto &rot = trs.rotation;
const auto &scale = trs.scale;
core::matrix4 transMat;
transMat.setTranslation(core::vector3df(trans[0], trans[1], trans[2]));
core::matrix4 rotMat = core::quaternion(rot[0], rot[1], rot[2], rot[3]).getMatrix();
joint->Animatedposition = convertHandedness(core::vector3df(trans[0], trans[1], trans[2]));
transMat.setTranslation(joint->Animatedposition);
core::matrix4 rotMat;
joint->Animatedrotation = convertHandedness(core::quaternion(rot[0], rot[1], rot[2], rot[3]));
core::quaternion(joint->Animatedrotation).getMatrix_transposed(rotMat);
joint->Animatedscale = core::vector3df(scale[0], scale[1], scale[2]);
core::matrix4 scaleMat;
scaleMat.setScale(core::vector3df(scale[0], scale[1], scale[2]));
scaleMat.setScale(joint->Animatedscale);
return transMat * rotMat * scaleMat;
}
static core::matrix4 loadTransform(std::optional<std::variant<tiniergltf::Node::Matrix, tiniergltf::Node::TRS>> transform) {
static core::matrix4 loadTransform(std::optional<std::variant<tiniergltf::Node::Matrix, tiniergltf::Node::TRS>> transform,
CSkinnedMesh::SJoint *joint) {
if (!transform.has_value()) {
return core::matrix4();
}
core::matrix4 mat = std::visit([](const auto &t) { return loadTransform(t); }, *transform);
return rightToLeft * mat * leftToRight;
return std::visit([joint](const auto &t) { return loadTransform(t, joint); }, *transform);
}
void SelfType::MeshExtractor::loadNode(
const std::size_t nodeIdx,
ISkinnedMesh::SJoint *parent) const
CSkinnedMesh::SJoint *parent)
{
const auto &node = m_gltf_model.nodes->at(nodeIdx);
auto *joint = m_irr_model->addJoint(parent);
const core::matrix4 transform = loadTransform(node.transform);
const core::matrix4 transform = loadTransform(node.transform, joint);
joint->LocalMatrix = transform;
joint->GlobalMatrix = parent ? parent->GlobalMatrix * joint->LocalMatrix : joint->LocalMatrix;
if (node.name.has_value()) {
joint->Name = node.name->c_str();
}
m_loaded_nodes[nodeIdx] = joint;
if (node.mesh.has_value()) {
loadMesh(*node.mesh, joint);
deferAddMesh(*node.mesh, node.skin, joint);
}
if (node.children.has_value()) {
for (const auto &child : *node.children) {
@ -492,8 +640,10 @@ void SelfType::MeshExtractor::loadNode(
}
}
void SelfType::MeshExtractor::loadNodes() const
void SelfType::MeshExtractor::loadNodes()
{
m_loaded_nodes = std::vector<CSkinnedMesh::SJoint *>(m_gltf_model.nodes->size());
std::vector<bool> isChild(m_gltf_model.nodes->size());
for (const auto &node : *m_gltf_model.nodes) {
if (!node.children.has_value())
@ -511,6 +661,92 @@ void SelfType::MeshExtractor::loadNodes() const
}
}
void SelfType::MeshExtractor::loadSkins()
{
if (!m_gltf_model.skins.has_value())
return;
for (const auto &skin : *m_gltf_model.skins) {
if (!skin.inverseBindMatrices.has_value())
continue;
const auto accessor = Accessor<core::matrix4>::make(m_gltf_model, *skin.inverseBindMatrices);
if (accessor.getCount() < skin.joints.size())
throw std::runtime_error("accessor contains too few matrices");
for (std::size_t i = 0; i < skin.joints.size(); ++i) {
m_loaded_nodes.at(skin.joints[i])->GlobalInversedMatrix = convertHandedness(accessor.get(i));
}
}
}
void SelfType::MeshExtractor::loadAnimation(const std::size_t animIdx)
{
const auto &anim = m_gltf_model.animations->at(animIdx);
for (const auto &channel : anim.channels) {
const auto &sampler = anim.samplers.at(channel.sampler);
if (sampler.interpolation != tiniergltf::AnimationSampler::Interpolation::LINEAR)
throw std::runtime_error("unsupported interpolation");
const auto inputAccessor = Accessor<f32>::make(m_gltf_model, sampler.input);
const auto n_frames = inputAccessor.getCount();
if (!channel.target.node.has_value())
throw std::runtime_error("no animated node");
const auto &joint = m_loaded_nodes.at(*channel.target.node);
switch (channel.target.path) {
case tiniergltf::AnimationChannelTarget::Path::TRANSLATION: {
const auto outputAccessor = Accessor<core::vector3df>::make(m_gltf_model, sampler.output);
for (std::size_t i = 0; i < n_frames; ++i) {
auto *key = m_irr_model->addPositionKey(joint);
key->frame = inputAccessor.get(i);
key->position = convertHandedness(outputAccessor.get(i));
}
break;
}
case tiniergltf::AnimationChannelTarget::Path::ROTATION: {
const auto outputAccessor = Accessor<core::quaternion>::make(m_gltf_model, sampler.output);
for (std::size_t i = 0; i < n_frames; ++i) {
auto *key = m_irr_model->addRotationKey(joint);
key->frame = inputAccessor.get(i);
key->rotation = convertHandedness(outputAccessor.get(i));
}
break;
}
case tiniergltf::AnimationChannelTarget::Path::SCALE: {
const auto outputAccessor = Accessor<core::vector3df>::make(m_gltf_model, sampler.output);
for (std::size_t i = 0; i < n_frames; ++i) {
auto *key = m_irr_model->addScaleKey(joint);
key->frame = inputAccessor.get(i);
key->scale = outputAccessor.get(i);
}
break;
}
case tiniergltf::AnimationChannelTarget::Path::WEIGHTS:
throw std::runtime_error("no support for morph animations");
}
}
}
void SelfType::MeshExtractor::load()
{
loadNodes();
for (const auto &load_mesh : m_mesh_loaders) {
load_mesh();
}
loadSkins();
// Load the first animation, if there is one.
if (m_gltf_model.animations.has_value()) {
if (m_gltf_model.animations->size() > 1) {
os::Printer::log("glTF loader",
"multiple animations are not supported", ELL_WARNING);
}
loadAnimation(0);
m_irr_model->setAnimationSpeed(1);
}
m_irr_model->finalize();
}
/**
* Extracts GLTF mesh indices.
*/
@ -650,11 +886,19 @@ void SelfType::MeshExtractor::copyTCoords(
const std::size_t accessorIdx,
std::vector<video::S3DVertex>& vertices) const
{
const auto componentType = m_gltf_model.accessors->at(accessorIdx).componentType;
if (componentType == tiniergltf::Accessor::ComponentType::FLOAT) {
// If floats are used, they need not be normalized: Wrapping may take effect.
const auto accessor = Accessor<std::array<f32, 2>>::make(m_gltf_model, accessorIdx);
for (std::size_t i = 0; i < accessor.getCount(); ++i) {
vertices[i].TCoords = core::vector2d<f32>(accessor.get(i));
}
} else {
const auto accessor = createNormalizedValuesAccessor<2>(m_gltf_model, accessorIdx);
const auto count = std::visit([](auto &&a) { return a.getCount(); }, accessor);
for (std::size_t i = 0; i < count; ++i) {
const auto vals = getNormalizedValues(accessor, i);
vertices[i].TCoords = core::vector2df(vals[0], vals[1]);
vertices[i].TCoords = core::vector2d<f32>(getNormalizedValues(accessor, i));
}
}
}
@ -663,6 +907,7 @@ void SelfType::MeshExtractor::copyTCoords(
*/
std::optional<tiniergltf::GlTF> SelfType::tryParseGLTF(io::IReadFile* file)
{
const bool isGlb = core::hasFileExtension(file->getFileName(), "glb");
auto size = file->getSize();
if (size < 0) // this can happen if `ftell` fails
return std::nullopt;
@ -671,15 +916,11 @@ std::optional<tiniergltf::GlTF> SelfType::tryParseGLTF(io::IReadFile* file)
return std::nullopt;
// We probably don't need this, but add it just to be sure.
buf[size] = '\0';
Json::CharReaderBuilder builder;
const std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
Json::Value json;
JSONCPP_STRING err;
if (!reader->parse(buf.get(), buf.get() + size, &json, &err)) {
return std::nullopt;
}
try {
return tiniergltf::GlTF(json);
if (isGlb)
return tiniergltf::readGlb(buf.get(), size);
else
return tiniergltf::readGlTF(buf.get(), size);
} catch (const std::runtime_error &e) {
os::Printer::log("glTF loader", e.what(), ELL_ERROR);
return std::nullopt;
@ -692,4 +933,3 @@ std::optional<tiniergltf::GlTF> SelfType::tryParseGLTF(io::IReadFile* file)
} // namespace scene
} // namespace irr

View file

@ -10,9 +10,11 @@
#include "path.h"
#include "S3DVertex.h"
#include <tiniergltf.hpp>
#include "tiniergltf.hpp"
#include <functional>
#include <cstddef>
#include <tuple>
#include <vector>
namespace irr
@ -94,11 +96,12 @@ private:
const NormalizedValuesAccessor<N> &accessor,
const std::size_t i);
class MeshExtractor {
class MeshExtractor
{
public:
MeshExtractor(tiniergltf::GlTF &&model,
CSkinnedMesh *mesh) noexcept
: m_gltf_model(model), m_irr_model(mesh) {};
: m_gltf_model(std::move(model)), m_irr_model(mesh) {};
/* Gets indices for the given mesh/primitive.
*
@ -114,12 +117,15 @@ private:
std::size_t getPrimitiveCount(const std::size_t meshIdx) const;
void loadNodes() const;
void load();
private:
const tiniergltf::GlTF m_gltf_model;
CSkinnedMesh *m_irr_model;
std::vector<std::function<void()>> m_mesh_loaders;
std::vector<CSkinnedMesh::SJoint *> m_loaded_nodes;
void copyPositions(const std::size_t accessorIdx,
std::vector<video::S3DVertex>& vertices) const;
@ -129,13 +135,21 @@ private:
void copyTCoords(const std::size_t accessorIdx,
std::vector<video::S3DVertex>& vertices) const;
void loadMesh(
std::size_t meshIdx,
ISkinnedMesh::SJoint *parentJoint) const;
void addPrimitive(const tiniergltf::MeshPrimitive &primitive,
const std::optional<std::size_t> skinIdx,
CSkinnedMesh::SJoint *parent);
void loadNode(
const std::size_t nodeIdx,
ISkinnedMesh::SJoint *parentJoint) const;
void deferAddMesh(const std::size_t meshIdx,
const std::optional<std::size_t> skinIdx,
CSkinnedMesh::SJoint *parentJoint);
void loadNode(const std::size_t nodeIdx, CSkinnedMesh::SJoint *parentJoint);
void loadNodes();
void loadSkins();
void loadAnimation(const std::size_t animIdx);
};
std::optional<tiniergltf::GlTF> tryParseGLTF(io::IReadFile *file);

View file

@ -721,12 +721,19 @@ bool CIrrDeviceSDL::run()
irrevent.EventType = irr::EET_MOUSE_INPUT_EVENT;
irrevent.MouseInput.Event = irr::EMIE_MOUSE_MOVED;
MouseX = irrevent.MouseInput.X =
static_cast<s32>(SDL_event.motion.x * ScaleX);
MouseY = irrevent.MouseInput.Y =
static_cast<s32>(SDL_event.motion.y * ScaleY);
MouseXRel = static_cast<s32>(SDL_event.motion.xrel * ScaleX);
MouseYRel = static_cast<s32>(SDL_event.motion.yrel * ScaleY);
if (!SDL_GetRelativeMouseMode()) {
MouseX = static_cast<s32>(SDL_event.motion.x * ScaleX);
MouseY = static_cast<s32>(SDL_event.motion.y * ScaleY);
} else {
MouseX += MouseXRel;
MouseY += MouseYRel;
}
irrevent.MouseInput.X = MouseX;
irrevent.MouseInput.Y = MouseY;
irrevent.MouseInput.ButtonStates = MouseButtonStates;
irrevent.MouseInput.Shift = (keymod & KMOD_SHIFT) != 0;
irrevent.MouseInput.Control = (keymod & KMOD_CTRL) != 0;

View file

@ -158,9 +158,13 @@ public:
//! Sets the new position of the cursor.
void setPosition(s32 x, s32 y) override
{
#ifndef __ANDROID__
// On Android, this somehow results in a camera jump when enabling
// relative mouse mode and it isn't supported anyway.
SDL_WarpMouseInWindow(Device->Window,
static_cast<int>(x / Device->ScaleX),
static_cast<int>(y / Device->ScaleY));
#endif
if (SDL_GetRelativeMouseMode()) {
// There won't be an event for this warp (details on libsdl-org/SDL/issues/6034)
@ -298,6 +302,7 @@ private:
#endif
s32 MouseX, MouseY;
// these two only continue to exist for some Emscripten stuff idk about
s32 MouseXRel, MouseYRel;
u32 MouseButtonStates;

View file

@ -480,8 +480,8 @@ add_library(IrrlichtMt::IrrlichtMt ALIAS IrrlichtMt)
target_include_directories(IrrlichtMt
PUBLIC
"$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include/>"
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>"
PRIVATE
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>"
${link_includes}
)

View file

@ -193,7 +193,7 @@ s32 CMeshManipulator::getPolyCount(scene::IMesh *mesh) const
//! Returns amount of polygons in mesh.
s32 CMeshManipulator::getPolyCount(scene::IAnimatedMesh *mesh) const
{
if (mesh && mesh->getFrameCount() != 0)
if (mesh && mesh->getMaxFrameNumber() != 0)
return getPolyCount(mesh->getMesh(0));
return 0;

View file

@ -111,11 +111,9 @@ CSkinnedMesh::~CSkinnedMesh()
}
}
//! returns the amount of frames in milliseconds.
//! If the amount is 1, it is a static (=non animated) mesh.
u32 CSkinnedMesh::getFrameCount() const
f32 CSkinnedMesh::getMaxFrameNumber() const
{
return core::floor32(EndFrame + 1.f);
return EndFrame;
}
//! Gets the default animation speed of the animated mesh.
@ -133,14 +131,14 @@ void CSkinnedMesh::setAnimationSpeed(f32 fps)
FramesPerSecond = fps;
}
//! returns the animated mesh based on a detail level. 0 is the lowest, 255 the highest detail. Note, that some Meshes will ignore the detail level.
IMesh *CSkinnedMesh::getMesh(s32 frame, s32 detailLevel, s32 startFrameLoop, s32 endFrameLoop)
//! returns the animated mesh based
IMesh *CSkinnedMesh::getMesh(f32 frame)
{
// animate(frame,startFrameLoop, endFrameLoop);
if (frame == -1)
return this;
animateMesh((f32)frame, 1.0f);
animateMesh(frame, 1.0f);
skinMesh();
return this;
}
@ -222,6 +220,7 @@ void CSkinnedMesh::buildAllLocalAnimatedMatrices()
// IRR_TEST_BROKEN_QUATERNION_USE: TODO - switched to getMatrix_transposed instead of getMatrix for downward compatibility.
// Not tested so far if this was correct or wrong before quaternion fix!
// Note that using getMatrix_transposed inverts the rotation.
joint->Animatedrotation.getMatrix_transposed(joint->LocalAnimatedMatrix);
// --- joint->LocalAnimatedMatrix *= joint->Animatedrotation.getMatrix() ---
@ -496,8 +495,8 @@ void CSkinnedMesh::skinJoint(SJoint *joint, SJoint *parentJoint)
{
if (joint->Weights.size()) {
// Find this joints pull on vertices...
core::matrix4 jointVertexPull(core::matrix4::EM4CONST_NOTHING);
jointVertexPull.setbyproduct(joint->GlobalAnimatedMatrix, joint->GlobalInversedMatrix);
// Note: It is assumed that the global inversed matrix has been calculated at this point.
core::matrix4 jointVertexPull = joint->GlobalAnimatedMatrix * joint->GlobalInversedMatrix.value();
core::vector3df thisVertexMove, thisNormalMove;
@ -510,8 +509,10 @@ void CSkinnedMesh::skinJoint(SJoint *joint, SJoint *parentJoint)
// Pull this vertex...
jointVertexPull.transformVect(thisVertexMove, weight.StaticPos);
if (AnimateNormals)
jointVertexPull.rotateVect(thisNormalMove, weight.StaticNormal);
if (AnimateNormals) {
thisNormalMove = jointVertexPull.rotateAndScaleVect(weight.StaticNormal);
thisNormalMove.normalize(); // must renormalize after potentially scaling
}
if (!(*(weight.Moved))) {
*(weight.Moved) = true;
@ -764,9 +765,9 @@ void CSkinnedMesh::calculateGlobalMatrices(SJoint *joint, SJoint *parentJoint)
joint->LocalAnimatedMatrix = joint->LocalMatrix;
joint->GlobalAnimatedMatrix = joint->GlobalMatrix;
if (joint->GlobalInversedMatrix.isIdentity()) { // might be pre calculated
if (!joint->GlobalInversedMatrix.has_value()) { // might be pre calculated
joint->GlobalInversedMatrix = joint->GlobalMatrix;
joint->GlobalInversedMatrix.makeInverse(); // slow
joint->GlobalInversedMatrix->makeInverse(); // slow
}
for (u32 j = 0; j < joint->Children.size(); ++j)

View file

@ -27,8 +27,8 @@ public:
//! destructor
virtual ~CSkinnedMesh();
//! returns the amount of frames. If the amount is 1, it is a static (=non animated) mesh.
u32 getFrameCount() const override;
//! If the duration is 0, it is a static (=non animated) mesh.
f32 getMaxFrameNumber() const override;
//! Gets the default animation speed of the animated mesh.
/** \return Amount of frames per second. If the amount is 0, it is a static, non animated mesh. */
@ -39,8 +39,8 @@ public:
The actual speed is set in the scene node the mesh is instantiated in.*/
void setAnimationSpeed(f32 fps) override;
//! returns the animated mesh based on a detail level (which is ignored)
IMesh *getMesh(s32 frame, s32 detailLevel = 255, s32 startFrameLoop = -1, s32 endFrameLoop = -1) override;
//! returns the animated mesh for the given frame
IMesh *getMesh(f32) override;
//! Animates this mesh's joints based on frame input
//! blend: {0-old position, 1-New position}

View file

@ -990,9 +990,9 @@ bool CXMeshFileLoader::parseDataObjectSkinWeights(SXMesh &mesh)
// transforms the mesh vertices to the space of the bone
// When concatenated to the bone's transform, this provides the
// world space coordinates of the mesh as affected by the bone
core::matrix4 &MatrixOffset = joint->GlobalInversedMatrix;
core::matrix4 MatrixOffset;
readMatrix(MatrixOffset);
joint->GlobalInversedMatrix = MatrixOffset;
if (!checkForOneFollowingSemicolons()) {
os::Printer::log("No finishing semicolon in Skin Weights found in x file", ELL_WARNING);

View file

@ -191,8 +191,7 @@ bool CZipReader::scanGZipHeader()
}
} else {
// no file name?
ZipFileName = Path;
core::deletePathFromFilename(ZipFileName);
ZipFileName = core::deletePathFromFilename(Path);
// rename tgz to tar or remove gz extension
if (core::hasFileExtension(ZipFileName, "tgz")) {

View file

@ -1,6 +1,9 @@
#pragma once
#include <json/json.h>
#include "util/base64.h"
#include <cstdint>
#include <functional>
#include <stack>
#include <string>
@ -13,7 +16,6 @@
#include <stdexcept>
#include <unordered_map>
#include <unordered_set>
#include "util/base64.h"
namespace tiniergltf {
@ -460,7 +462,8 @@ struct Buffer {
std::optional<std::string> name;
std::string data;
Buffer(const Json::Value &o,
const std::function<std::string(const std::string &uri)> &resolveURI)
const std::function<std::string(const std::string &uri)> &resolveURI,
std::optional<std::string> &&glbData = std::nullopt)
: byteLength(as<std::size_t>(o["byteLength"]))
{
check(o.isObject());
@ -468,6 +471,13 @@ struct Buffer {
if (o.isMember("name")) {
name = as<std::string>(o["name"]);
}
if (glbData.has_value()) {
check(!o.isMember("uri"));
data = *std::move(glbData);
// GLB allows padding, which need not be reflected in the JSON
check(byteLength + 3 >= data.size());
check(data.size() >= byteLength);
} else {
check(o.isMember("uri"));
bool dataURI = false;
const std::string uri = as<std::string>(o["uri"]);
@ -486,6 +496,7 @@ struct Buffer {
if (!dataURI)
data = resolveURI(uri);
check(data.size() >= byteLength);
}
data.resize(byteLength);
}
};
@ -969,21 +980,16 @@ struct Sampler {
};
std::optional<MinFilter> minFilter;
std::optional<std::string> name;
enum class WrapS {
enum class Wrap {
REPEAT,
CLAMP_TO_EDGE,
MIRRORED_REPEAT,
};
WrapS wrapS;
enum class WrapT {
REPEAT,
CLAMP_TO_EDGE,
MIRRORED_REPEAT,
};
WrapT wrapT;
Wrap wrapS;
Wrap wrapT;
Sampler(const Json::Value &o)
: wrapS(WrapS::REPEAT)
, wrapT(WrapT::REPEAT)
: wrapS(Wrap::REPEAT)
, wrapT(Wrap::REPEAT)
{
check(o.isObject());
if (o.isMember("magFilter")) {
@ -1009,21 +1015,16 @@ struct Sampler {
if (o.isMember("name")) {
name = as<std::string>(o["name"]);
}
if (o.isMember("wrapS")) {
static std::unordered_map<Json::UInt64, WrapS> map = {
{10497, WrapS::REPEAT},
{33071, WrapS::CLAMP_TO_EDGE},
{33648, WrapS::MIRRORED_REPEAT},
static std::unordered_map<Json::UInt64, Wrap> map = {
{10497, Wrap::REPEAT},
{33071, Wrap::CLAMP_TO_EDGE},
{33648, Wrap::MIRRORED_REPEAT},
};
if (o.isMember("wrapS")) {
const auto &v = o["wrapS"]; check(v.isUInt64());
wrapS = map.at(v.asUInt64());
}
if (o.isMember("wrapT")) {
static std::unordered_map<Json::UInt64, WrapT> map = {
{10497, WrapT::REPEAT},
{33071, WrapT::CLAMP_TO_EDGE},
{33648, WrapT::MIRRORED_REPEAT},
};
const auto &v = o["wrapT"]; check(v.isUInt64());
wrapT = map.at(v.asUInt64());
}
@ -1093,6 +1094,12 @@ struct Texture {
};
template<> Texture as(const Json::Value &o) { return o; }
using UriResolver = std::function<std::string(const std::string &uri)>;
static inline std::string uriError(const std::string &uri) {
// only base64 data URI support by default
throw std::runtime_error("unsupported URI: " + uri);
}
struct GlTF {
std::optional<std::vector<Accessor>> accessors;
std::optional<std::vector<Animation>> animations;
@ -1111,12 +1118,10 @@ struct GlTF {
std::optional<std::vector<Scene>> scenes;
std::optional<std::vector<Skin>> skins;
std::optional<std::vector<Texture>> textures;
static std::string uriError(const std::string &uri) {
// only base64 data URI support by default
throw std::runtime_error("unsupported URI: " + uri);
}
GlTF(const Json::Value &o,
const std::function<std::string(const std::string &uri)> &resolveURI = uriError)
const UriResolver &resolveUri = uriError,
std::optional<std::string> &&glbData = std::nullopt)
: asset(as<Asset>(o["asset"]))
{
check(o.isObject());
@ -1138,7 +1143,8 @@ struct GlTF {
std::vector<Buffer> bufs;
bufs.reserve(b.size());
for (Json::ArrayIndex i = 0; i < b.size(); ++i) {
bufs.emplace_back(b[i], resolveURI);
bufs.emplace_back(b[i], resolveUri,
i == 0 ? std::move(glbData) : std::nullopt);
}
check(bufs.size() >= 1);
buffers = std::move(bufs);
@ -1354,4 +1360,123 @@ struct GlTF {
}
};
// std::span is C++ 20, so we roll our own little struct here.
template <typename T>
struct Span {
T *ptr;
uint32_t len;
bool empty() const {
return len == 0;
}
T *end() const {
return ptr + len;
}
template <typename U>
Span<U> cast() const {
return {(U *) ptr, len};
}
};
static Json::Value readJson(Span<const char> span) {
Json::CharReaderBuilder builder;
const std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
Json::Value json;
JSONCPP_STRING err;
if (!reader->parse(span.ptr, span.end(), &json, &err))
throw std::runtime_error(std::string("invalid JSON: ") + err);
return json;
}
inline GlTF readGlb(const char *data, std::size_t len, const UriResolver &resolveUri = uriError) {
struct Chunk {
uint32_t type;
Span<const uint8_t> span;
};
struct Stream {
Span<const uint8_t> span;
bool eof() const {
return span.empty();
}
void advance(uint32_t n) {
span.len -= n;
span.ptr += n;
}
uint32_t readUint32() {
if (span.len < 4)
throw std::runtime_error("premature EOF");
uint32_t res = 0;
for (int i = 0; i < 4; ++i)
res += span.ptr[i] << (i * 8);
advance(4);
return res;
}
Chunk readChunk() {
const auto chunkLen = readUint32();
if (chunkLen % 4 != 0)
throw std::runtime_error("chunk length must be multiple of 4");
const auto chunkType = readUint32();
auto chunkPtr = span.ptr;
if (span.len < chunkLen)
throw std::runtime_error("premature EOF");
advance(chunkLen);
return {chunkType, {chunkPtr, chunkLen}};
}
};
constexpr uint32_t MAGIC_GLTF = 0x46546C67;
constexpr uint32_t MAGIC_JSON = 0x4E4F534A;
constexpr uint32_t MAGIC_BIN = 0x004E4942;
if (len > std::numeric_limits<uint32_t>::max())
throw std::runtime_error("too large");
Stream is{{(const uint8_t *) data, static_cast<uint32_t>(len)}};
const auto magic = is.readUint32();
if (magic != MAGIC_GLTF)
throw std::runtime_error("wrong magic number");
const auto version = is.readUint32();
if (version != 2)
throw std::runtime_error("wrong version");
const auto length = is.readUint32();
if (length != len)
throw std::runtime_error("wrong length");
const auto json = is.readChunk();
if (json.type != MAGIC_JSON)
throw std::runtime_error("expected JSON chunk");
std::optional<std::string> buffer;
if (!is.eof()) {
const auto chunk = is.readChunk();
if (chunk.type == MAGIC_BIN)
buffer = std::string((const char *) chunk.span.ptr, chunk.span.len);
else if (chunk.type == MAGIC_JSON)
throw std::runtime_error("unexpected chunk");
// Ignore all other chunks. We still want to validate that
// 1. These chunks are valid;
// 2. These chunks are *not* JSON or BIN chunks
while (!is.eof()) {
const auto type = is.readChunk().type;
if (type == MAGIC_JSON || type == MAGIC_BIN)
throw std::runtime_error("unexpected chunk");
}
}
return GlTF(readJson(json.span.cast<const char>()), resolveUri, std::move(buffer));
}
inline GlTF readGlTF(const char *data, std::size_t len, const UriResolver &resolveUri = uriError) {
if (len > std::numeric_limits<uint32_t>::max())
throw std::runtime_error("too large");
return GlTF(readJson({data, static_cast<uint32_t>(len)}), resolveUri);
}
}

View file

@ -405,10 +405,11 @@ void Camera::update(LocalPlayer* player, f32 frametime, f32 tool_reload_ratio)
// Compute absolute camera position and target
m_headnode->getAbsoluteTransformation().transformVect(m_camera_position, rel_cam_pos);
m_headnode->getAbsoluteTransformation().rotateVect(m_camera_direction, rel_cam_target - rel_cam_pos);
m_camera_direction = m_headnode->getAbsoluteTransformation()
.rotateAndScaleVect(rel_cam_target - rel_cam_pos);
v3f abs_cam_up;
m_headnode->getAbsoluteTransformation().rotateVect(abs_cam_up, rel_cam_up);
v3f abs_cam_up = m_headnode->getAbsoluteTransformation()
.rotateAndScaleVect(rel_cam_up);
// Separate camera position for calculation
v3f my_cp = m_camera_position;

View file

@ -827,7 +827,7 @@ bool Client::loadMedia(const std::string &data, const std::string &filename,
}
const char *model_ext[] = {
".x", ".b3d", ".obj", ".gltf",
".x", ".b3d", ".obj", ".gltf", ".glb",
NULL
};
name = removeStringEnd(filename, model_ext);
@ -1034,7 +1034,7 @@ void Client::Send(NetworkPacket* pkt)
m_con->Send(PEER_ID_SERVER, scf.channel, pkt, scf.reliable);
}
// Will fill up 12 + 12 + 4 + 4 + 4 + 1 + 1 + 1 bytes
// Will fill up 12 + 12 + 4 + 4 + 4 + 1 + 1 + 1 + 4 + 4 bytes
void writePlayerPos(LocalPlayer *myplayer, ClientMap *clientMap, NetworkPacket *pkt, bool camera_inverted)
{
v3f pf = myplayer->getPosition() * 100;
@ -1046,6 +1046,8 @@ void writePlayerPos(LocalPlayer *myplayer, ClientMap *clientMap, NetworkPacket *
u8 fov = std::fmin(255.0f, clientMap->getCameraFov() * 80.0f);
u8 wanted_range = std::fmin(255.0f,
std::ceil(clientMap->getWantedRange() * (1.0f / MAP_BLOCKSIZE)));
f32 movement_speed = myplayer->control.movement_speed;
f32 movement_dir = myplayer->control.movement_direction;
v3s32 position(pf.X, pf.Y, pf.Z);
v3s32 speed(sf.X, sf.Y, sf.Z);
@ -1060,10 +1062,13 @@ void writePlayerPos(LocalPlayer *myplayer, ClientMap *clientMap, NetworkPacket *
[12+12+4+4+4] u8 fov*80
[12+12+4+4+4+1] u8 ceil(wanted_range / MAP_BLOCKSIZE)
[12+12+4+4+4+1+1] u8 camera_inverted (bool)
[12+12+4+4+4+1+1+1] f32 movement_speed
[12+12+4+4+4+1+1+1+4] f32 movement_direction
*/
*pkt << position << speed << pitch << yaw << keyPressed;
*pkt << fov << wanted_range;
*pkt << camera_inverted;
*pkt << movement_speed << movement_dir;
}
void Client::interact(InteractAction action, const PointedThing& pointed)
@ -1142,7 +1147,7 @@ void Client::sendInit(const std::string &playerName)
NetworkPacket pkt(TOSERVER_INIT, 1 + 2 + 2 + (1 + playerName.size()));
pkt << (u8) SER_FMT_VER_HIGHEST_READ << (u16) 0;
pkt << (u16) CLIENT_PROTOCOL_VERSION_MIN << (u16) CLIENT_PROTOCOL_VERSION_MAX;
pkt << CLIENT_PROTOCOL_VERSION_MIN << LATEST_PROTOCOL_VERSION;
pkt << playerName;
Send(&pkt);
@ -1397,6 +1402,8 @@ void Client::sendPlayerPos()
u32 keyPressed = player->control.getKeysPressed();
bool camera_inverted = m_camera->getCameraMode() == CAMERA_MODE_THIRD_FRONT;
f32 movement_speed = player->control.movement_speed;
f32 movement_dir = player->control.movement_direction;
if (
player->last_position == player->getPosition() &&
@ -1406,7 +1413,9 @@ void Client::sendPlayerPos()
player->last_keyPressed == keyPressed &&
player->last_camera_fov == camera_fov &&
player->last_camera_inverted == camera_inverted &&
player->last_wanted_range == wanted_range)
player->last_wanted_range == wanted_range &&
player->last_movement_speed == movement_speed &&
player->last_movement_dir == movement_dir)
return;
player->last_position = player->getPosition();
@ -1417,8 +1426,10 @@ void Client::sendPlayerPos()
player->last_camera_fov = camera_fov;
player->last_camera_inverted = camera_inverted;
player->last_wanted_range = wanted_range;
player->last_movement_speed = movement_speed;
player->last_movement_dir = movement_dir;
NetworkPacket pkt(TOSERVER_PLAYERPOS, 12 + 12 + 4 + 4 + 4 + 1 + 1 + 1);
NetworkPacket pkt(TOSERVER_PLAYERPOS, 12 + 12 + 4 + 4 + 4 + 1 + 1 + 1 + 4 + 4);
writePlayerPos(player, &map, &pkt, camera_inverted);

View file

@ -35,6 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "gameparams.h"
#include "script/common/c_types.h" // LuaError
#include "util/numeric.h"
#include "util/string.h" // StringMap
#ifdef SERVER
#error Do not include in server builds

View file

@ -1015,8 +1015,7 @@ int ClientMap::getBackgroundBrightness(float max_d, u32 daylight_factor,
v3f z_dir = z_directions[i];
core::CMatrix4<f32> a;
a.buildRotateFromTo(v3f(0,1,0), z_dir);
v3f dir = m_camera_direction;
a.rotateVect(dir);
v3f dir = a.rotateAndScaleVect(m_camera_direction);
int br = 0;
float step = BS*1.5;
if(max_d > 35*BS)

View file

@ -1052,7 +1052,7 @@ void GenericCAO::step(float dtime, ClientEnvironment *env)
walking = true;
}
v2s32 new_anim = v2s32(0,0);
v2f new_anim(0,0);
bool allow_update = false;
// increase speed if using fast or flying fast
@ -1799,10 +1799,9 @@ void GenericCAO::processMessage(const std::string &data)
phys.speed_walk = override_speed_walk;
}
} else if (cmd == AO_CMD_SET_ANIMATION) {
// TODO: change frames send as v2s32 value
v2f range = readV2F32(is);
if (!m_is_local_player) {
m_animation_range = v2s32((s32)range.X, (s32)range.Y);
m_animation_range = range;
m_animation_speed = readF32(is);
m_animation_blend = readF32(is);
// these are sent inverted so we get true when the server sends nothing
@ -1812,7 +1811,7 @@ void GenericCAO::processMessage(const std::string &data)
LocalPlayer *player = m_env->getLocalPlayer();
if(player->last_animation == LocalPlayerAnimation::NO_ANIM)
{
m_animation_range = v2s32((s32)range.X, (s32)range.Y);
m_animation_range = range;
m_animation_speed = readF32(is);
m_animation_blend = readF32(is);
// these are sent inverted so we get true when the server sends nothing

View file

@ -99,7 +99,7 @@ private:
v2s16 m_tx_basepos;
bool m_initial_tx_basepos_set = false;
bool m_tx_select_horiz_by_yawpitch = false;
v2s32 m_animation_range;
v2f m_animation_range;
float m_animation_speed = 15.0f;
float m_animation_blend = 0.0f;
bool m_animation_loop = true;

View file

@ -1016,13 +1016,6 @@ void MapblockMeshGenerator::drawGlasslikeFramedNode()
}
}
void MapblockMeshGenerator::drawAllfacesNode()
{
static const aabb3f box(-BS / 2, -BS / 2, -BS / 2, BS / 2, BS / 2, BS / 2);
useTile(0, 0, 0);
drawAutoLightedCuboid(box);
}
void MapblockMeshGenerator::drawTorchlikeNode()
{
u8 wall = cur_node.n.getWallMounted(nodedef);
@ -1545,6 +1538,17 @@ namespace {
};
}
void MapblockMeshGenerator::drawAllfacesNode()
{
static const aabb3f box(-BS / 2, -BS / 2, -BS / 2, BS / 2, BS / 2, BS / 2);
TileSpec tiles[6];
for (int face = 0; face < 6; face++)
getTile(nodebox_tile_dirs[face], &tiles[face]);
if (data->m_smooth_lighting)
getSmoothLightFrame();
drawAutoLightedCuboid(box, nullptr, tiles, 6);
}
void MapblockMeshGenerator::drawNodeboxNode()
{
TileSpec tiles[6];

View file

@ -43,6 +43,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "gui/touchcontrols.h"
#include "itemdef.h"
#include "log.h"
#include "log_internal.h"
#include "filesys.h"
#include "gameparams.h"
#include "gettext.h"
@ -413,16 +414,8 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter
float m_user_exposure_compensation;
bool m_bloom_enabled;
CachedPixelShaderSetting<float> m_bloom_intensity_pixel{"bloomIntensity"};
float m_bloom_intensity;
CachedPixelShaderSetting<float> m_bloom_strength_pixel{"bloomStrength"};
float m_bloom_strength;
CachedPixelShaderSetting<float> m_bloom_radius_pixel{"bloomRadius"};
float m_bloom_radius;
CachedPixelShaderSetting<float> m_cloud_height_pixel{"cloudHeight"};
CachedPixelShaderSetting<float> m_cloud_thickness_pixel{"cloudThickness"};
CachedPixelShaderSetting<float> m_cloud_density_pixel{"cloudDensity"};
CachedPixelShaderSetting<float, 2> m_cloud_offset_pixel{"cloudOffset"};
CachedPixelShaderSetting<float> m_cloud_radius_pixel{"cloudRadius"};
CachedPixelShaderSetting<float> m_saturation_pixel{"saturation"};
float m_gamma;
CachedPixelShaderSetting<float> m_gamma_pixel{"gamma"};
@ -436,12 +429,8 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter
CachedPixelShaderSetting<float>
m_volumetric_light_strength_pixel{"volumetricLightStrength"};
static constexpr std::array<const char*, 5> SETTING_CALLBACKS = {
static constexpr std::array<const char*, 1> SETTING_CALLBACKS = {
"exposure_compensation",
"bloom_intensity",
"bloom_strength_factor",
"bloom_radius",
"gamma"
};
public:
@ -449,14 +438,6 @@ public:
{
if (name == "exposure_compensation")
m_user_exposure_compensation = g_settings->getFloat("exposure_compensation", -1.0f, 1.0f);
if (name == "bloom_intensity")
m_bloom_intensity = g_settings->getFloat("bloom_intensity", 0.01f, 1.0f);
if (name == "bloom_strength_factor")
m_bloom_strength = RenderingEngine::BASE_BLOOM_STRENGTH * g_settings->getFloat("bloom_strength_factor", 0.1f, 10.0f);
if (name == "bloom_radius")
m_bloom_radius = g_settings->getFloat("bloom_radius", 0.1f, 8.0f);
if (name == "gamma")
m_gamma = g_settings->getFloat("gamma", 1.0f, 5.0f);
}
static void settingsCallback(const std::string &name, void *userdata)
@ -475,10 +456,6 @@ public:
m_user_exposure_compensation = g_settings->getFloat("exposure_compensation", -1.0f, 1.0f);
m_bloom_enabled = g_settings->getBool("enable_bloom");
m_bloom_intensity = g_settings->getFloat("bloom_intensity", 0.01f, 1.0f);
m_bloom_strength = RenderingEngine::BASE_BLOOM_STRENGTH * g_settings->getFloat("bloom_strength_factor", 0.1f, 10.0f);
m_bloom_radius = g_settings->getFloat("bloom_radius", 0.1f, 8.0f);
m_gamma = g_settings->getFloat("gamma", 1.0f, 5.0f);
m_volumetric_light_enabled = g_settings->getBool("enable_volumetric_lighting") && m_bloom_enabled;
}
@ -547,7 +524,9 @@ public:
m_texel_size0_vertex.set(m_texel_size0, services);
m_texel_size0_pixel.set(m_texel_size0, services);
const AutoExposure &exposure_params = m_client->getEnv().getLocalPlayer()->getLighting().exposure;
const auto &lighting = m_client->getEnv().getLocalPlayer()->getLighting();
const AutoExposure &exposure_params = lighting.exposure;
std::array<float, 7> exposure_buffer = {
std::pow(2.0f, exposure_params.luminance_min),
std::pow(2.0f, exposure_params.luminance_max),
@ -560,14 +539,14 @@ public:
m_exposure_params_pixel.set(exposure_buffer.data(), services);
if (m_bloom_enabled) {
m_bloom_intensity_pixel.set(&m_bloom_intensity, services);
m_bloom_radius_pixel.set(&m_bloom_radius, services);
m_bloom_strength_pixel.set(&m_bloom_strength, services);
float intensity = std::max(lighting.bloom_intensity, 0.0f);
m_bloom_intensity_pixel.set(&intensity, services);
float strength_factor = std::max(lighting.bloom_strength_factor, 0.0f);
m_bloom_strength_pixel.set(&strength_factor, services);
float radius = std::max(lighting.bloom_radius, 0.0f);
m_bloom_radius_pixel.set(&radius, services);
}
m_gamma_pixel.set(&m_gamma, services);
const auto &lighting = m_client->getEnv().getLocalPlayer()->getLighting();
float saturation = lighting.saturation;
m_saturation_pixel.set(&saturation, services);
video::SColorf artificial_light = lighting.artificial_light_color;
@ -773,6 +752,7 @@ protected:
void processUserInput(f32 dtime);
void processKeyInput();
void processItemSelection(u16 *new_playeritem);
bool shouldShowTouchControls();
void dropSelectedItem(bool single_item = false);
void openInventory();
@ -1615,6 +1595,14 @@ bool Game::createClient(const GameStartData &start_data)
return true;
}
bool Game::shouldShowTouchControls()
{
const std::string &touch_controls = g_settings->get("touch_controls");
if (touch_controls == "auto")
return RenderingEngine::getLastPointerType() == PointerType::Touch;
return is_yes(touch_controls);
}
bool Game::initGui()
{
m_game_ui->init();
@ -1629,7 +1617,7 @@ bool Game::initGui()
gui_chat_console = make_irr<GUIChatConsole>(guienv, guienv->getRootGUIElement(),
-1, chat_backend, client, &g_menumgr);
if (g_settings->getBool("touch_controls")) {
if (shouldShowTouchControls()) {
g_touchcontrols = new TouchControls(device, texture_src);
g_touchcontrols->setUseCrosshair(!isTouchCrosshairDisabled());
}
@ -2081,6 +2069,15 @@ void Game::updateStats(RunStats *stats, const FpsControl &draw_times,
void Game::processUserInput(f32 dtime)
{
bool desired = shouldShowTouchControls();
if (desired && !g_touchcontrols) {
g_touchcontrols = new TouchControls(device, texture_src);
} else if (!desired && g_touchcontrols) {
delete g_touchcontrols;
g_touchcontrols = nullptr;
}
// Reset input if window not active or some menu is active
if (!device->isWindowActive() || isMenuActive() || guienv->hasFocus(gui_chat_console.get())) {
if (m_game_focused) {
@ -2711,7 +2708,7 @@ void Game::updateCameraDirection(CameraOrientation *cam, float dtime)
cur_control->setVisible(false);
}
if (m_first_loop_after_window_activation) {
if (m_first_loop_after_window_activation && !g_touchcontrols) {
m_first_loop_after_window_activation = false;
input->setMousePos(driver->getScreenSize().Width / 2,
@ -2727,6 +2724,8 @@ void Game::updateCameraDirection(CameraOrientation *cam, float dtime)
m_first_loop_after_window_activation = true;
}
if (g_touchcontrols)
m_first_loop_after_window_activation = true;
}
// Get the factor to multiply with sensitivity to get the same mouse/joystick
@ -2792,9 +2791,10 @@ void Game::updatePlayerControl(const CameraOrientation &cam)
isKeyDown(KeyType::PLACE),
cam.camera_pitch,
cam.camera_yaw,
input->getMovementSpeed(),
input->getMovementDirection()
input->getJoystickSpeed(),
input->getJoystickDirection()
);
control.setMovementFromKeys();
// autoforward if set: move at maximum speed
if (player->getPlayerSettings().continuous_forward &&

View file

@ -536,9 +536,9 @@ void Hud::drawLuaElements(const v3s16 &camera_offset)
return; // Avoid zero divides
// Angle according to camera view
v3f fore(0.f, 0.f, 1.f);
scene::ICameraSceneNode *cam = client->getSceneManager()->getActiveCamera();
cam->getAbsoluteTransformation().rotateVect(fore);
v3f fore = cam->getAbsoluteTransformation()
.rotateAndScaleVect(v3f(0.f, 0.f, 1.f));
int angle = - fore.getHorizontalAngle().Y;
// Limit angle and ajust with given offset

View file

@ -1447,6 +1447,8 @@ bool ImageSource::generateImagePart(std::string_view part_of_name,
video::IImage *img = generateImage(filename, source_image_names);
if (img) {
upscaleImagesToMatchLargest(baseimg, img);
apply_mask(img, baseimg, v2s32(0, 0), v2s32(0, 0),
img->getDimension());
img->drop();

View file

@ -24,6 +24,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "gui/mainmenumanager.h"
#include "gui/touchcontrols.h"
#include "hud.h"
#include "log_internal.h"
#include "client/renderingengine.h"
void KeyCache::populate_nonchanging()
{
@ -141,6 +143,11 @@ bool MyEventReceiver::OnEvent(const SEvent &event)
}
}
if (event.EventType == EET_MOUSE_INPUT_EVENT && !event.MouseInput.Simulated)
last_pointer_type = PointerType::Mouse;
else if (event.EventType == EET_TOUCH_INPUT_EVENT)
last_pointer_type = PointerType::Touch;
// Let the menu handle events, if one is active.
if (isMenuActive()) {
if (g_touchcontrols)
@ -220,51 +227,42 @@ bool MyEventReceiver::OnEvent(const SEvent &event)
/*
* RealInputHandler
*/
float RealInputHandler::getMovementSpeed()
float RealInputHandler::getJoystickSpeed()
{
bool f = m_receiver->IsKeyDown(keycache.key[KeyType::FORWARD]),
b = m_receiver->IsKeyDown(keycache.key[KeyType::BACKWARD]),
l = m_receiver->IsKeyDown(keycache.key[KeyType::LEFT]),
r = m_receiver->IsKeyDown(keycache.key[KeyType::RIGHT]);
if (f || b || l || r)
{
// if contradictory keys pressed, stay still
if (f && b && l && r)
return 0.0f;
else if (f && b && !l && !r)
return 0.0f;
else if (!f && !b && l && r)
return 0.0f;
return 1.0f; // If there is a keyboard event, assume maximum speed
}
if (g_touchcontrols && g_touchcontrols->getMovementSpeed())
return g_touchcontrols->getMovementSpeed();
if (g_touchcontrols && g_touchcontrols->getJoystickSpeed())
return g_touchcontrols->getJoystickSpeed();
return joystick.getMovementSpeed();
}
float RealInputHandler::getMovementDirection()
float RealInputHandler::getJoystickDirection()
{
float x = 0, z = 0;
/* Check keyboard for input */
if (m_receiver->IsKeyDown(keycache.key[KeyType::FORWARD]))
z += 1;
if (m_receiver->IsKeyDown(keycache.key[KeyType::BACKWARD]))
z -= 1;
if (m_receiver->IsKeyDown(keycache.key[KeyType::RIGHT]))
x += 1;
if (m_receiver->IsKeyDown(keycache.key[KeyType::LEFT]))
x -= 1;
if (x != 0 || z != 0) /* If there is a keyboard event, it takes priority */
return std::atan2(x, z);
// `getMovementDirection() == 0` means forward, so we cannot use
// `getMovementDirection()` as a condition.
else if (g_touchcontrols && g_touchcontrols->getMovementSpeed())
return g_touchcontrols->getMovementDirection();
// `getJoystickDirection() == 0` means forward, so we cannot use
// `getJoystickDirection()` as a condition.
if (g_touchcontrols && g_touchcontrols->getJoystickSpeed())
return g_touchcontrols->getJoystickDirection();
return joystick.getMovementDirection();
}
v2s32 RealInputHandler::getMousePos()
{
auto control = RenderingEngine::get_raw_device()->getCursorControl();
if (control) {
return control->getPosition();
}
return m_mousepos;
}
void RealInputHandler::setMousePos(s32 x, s32 y)
{
auto control = RenderingEngine::get_raw_device()->getCursorControl();
if (control) {
control->setPosition(x, y);
} else {
m_mousepos = v2s32(x, y);
}
}
/*
* RandomInputHandler
*/
@ -320,25 +318,11 @@ void RandomInputHandler::step(float dtime)
counterMovement -= dtime;
if (counterMovement < 0.0) {
counterMovement = 0.1 * Rand(1, 40);
movementSpeed = Rand(0,100)*0.01;
movementDirection = Rand(-100, 100)*0.01 * M_PI;
joystickSpeed = Rand(0,100)*0.01;
joystickDirection = Rand(-100, 100)*0.01 * M_PI;
}
} else {
bool f = keydown[keycache.key[KeyType::FORWARD]],
l = keydown[keycache.key[KeyType::LEFT]];
if (f || l) {
movementSpeed = 1.0f;
if (f && !l)
movementDirection = 0.0;
else if (!f && l)
movementDirection = -M_PI_2;
else if (f && l)
movementDirection = -M_PI_4;
else
movementDirection = 0.0;
} else {
movementSpeed = 0.0;
movementDirection = 0.0;
}
joystickSpeed = 0.0f;
joystickDirection = 0.0f;
}
}

View file

@ -23,10 +23,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "joystick_controller.h"
#include <list>
#include "keycode.h"
#include "renderingengine.h"
class InputHandler;
enum class PointerType {
Mouse,
Touch,
};
/****************************************************************************
Fast key cache for main game loop
****************************************************************************/
@ -199,6 +203,8 @@ public:
JoystickController *joystick = nullptr;
PointerType getLastPointerType() { return last_pointer_type; }
private:
s32 mouse_wheel = 0;
@ -223,6 +229,8 @@ private:
// Intentionally not reset by clearInput/releaseAllKeys.
bool fullscreen_is_down = false;
PointerType last_pointer_type = PointerType::Mouse;
};
class InputHandler
@ -247,8 +255,8 @@ public:
virtual bool wasKeyReleased(GameKeyType k) = 0;
virtual bool cancelPressed() = 0;
virtual float getMovementSpeed() = 0;
virtual float getMovementDirection() = 0;
virtual float getJoystickSpeed() = 0;
virtual float getJoystickDirection() = 0;
virtual void clearWasKeyPressed() {}
virtual void clearWasKeyReleased() {}
@ -304,9 +312,9 @@ public:
return m_receiver->WasKeyReleased(keycache.key[k]) || joystick.wasKeyReleased(k);
}
virtual float getMovementSpeed();
virtual float getJoystickSpeed();
virtual float getMovementDirection();
virtual float getJoystickDirection();
virtual bool cancelPressed()
{
@ -331,25 +339,8 @@ public:
m_receiver->dontListenForKeys();
}
virtual v2s32 getMousePos()
{
auto control = RenderingEngine::get_raw_device()->getCursorControl();
if (control) {
return control->getPosition();
}
return m_mousepos;
}
virtual void setMousePos(s32 x, s32 y)
{
auto control = RenderingEngine::get_raw_device()->getCursorControl();
if (control) {
control->setPosition(x, y);
} else {
m_mousepos = v2s32(x, y);
}
}
virtual v2s32 getMousePos();
virtual void setMousePos(s32 x, s32 y);
virtual s32 getMouseWheel()
{
@ -388,8 +379,8 @@ public:
virtual bool wasKeyPressed(GameKeyType k) { return false; }
virtual bool wasKeyReleased(GameKeyType k) { return false; }
virtual bool cancelPressed() { return false; }
virtual float getMovementSpeed() { return movementSpeed; }
virtual float getMovementDirection() { return movementDirection; }
virtual float getJoystickSpeed() { return joystickSpeed; }
virtual float getJoystickDirection() { return joystickDirection; }
virtual v2s32 getMousePos() { return mousepos; }
virtual void setMousePos(s32 x, s32 y) { mousepos = v2s32(x, y); }
@ -403,6 +394,6 @@ private:
KeyList keydown;
v2s32 mousepos;
v2s32 mousespeed;
float movementSpeed;
float movementDirection;
float joystickSpeed;
float joystickDirection;
};

View file

@ -105,6 +105,8 @@ public:
u8 last_camera_fov = 0;
u8 last_wanted_range = 0;
bool last_camera_inverted = false;
f32 last_movement_speed = 0.0f;
f32 last_movement_dir = 0.0f;
float camera_impact = 0.0f;

View file

@ -357,16 +357,18 @@ void ParticleSpawner::spawnParticle(ClientEnvironment *env, float radius,
if (attached_absolute_pos_rot_matrix) {
// Apply attachment rotation
attached_absolute_pos_rot_matrix->rotateVect(pp.vel);
attached_absolute_pos_rot_matrix->rotateVect(pp.acc);
pp.vel = attached_absolute_pos_rot_matrix->rotateAndScaleVect(pp.vel);
pp.acc = attached_absolute_pos_rot_matrix->rotateAndScaleVect(pp.acc);
}
if (attractor_obj)
attractor_origin += attractor_obj->getPosition() / BS;
if (attractor_direction_obj) {
auto *attractor_absolute_pos_rot_matrix = attractor_direction_obj->getAbsolutePosRotMatrix();
if (attractor_absolute_pos_rot_matrix)
attractor_absolute_pos_rot_matrix->rotateVect(attractor_direction);
if (attractor_absolute_pos_rot_matrix) {
attractor_direction = attractor_absolute_pos_rot_matrix
->rotateAndScaleVect(attractor_direction);
}
}
pp.expirationtime = r_exp.pickWithin();

View file

@ -41,7 +41,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
RenderingEngine *RenderingEngine::s_singleton = nullptr;
const video::SColor RenderingEngine::MENU_SKY_COLOR = video::SColor(255, 140, 186, 250);
const float RenderingEngine::BASE_BLOOM_STRENGTH = 1.0f;
/* Helper classes */
@ -173,7 +172,7 @@ static irr::IrrlichtDevice *createDevice(SIrrlichtCreationParameters params, std
/* RenderingEngine class */
RenderingEngine::RenderingEngine(IEventReceiver *receiver)
RenderingEngine::RenderingEngine(MyEventReceiver *receiver)
{
sanity_check(!s_singleton);
@ -226,6 +225,8 @@ RenderingEngine::RenderingEngine(IEventReceiver *receiver)
// This changes the minimum allowed number of vertices in a VBO. Default is 500.
driver->setMinHardwareBufferVertexCount(4);
m_receiver = receiver;
s_singleton = this;
g_settings->registerChangedCallback("fullscreen", settingChangedCallback, this);

View file

@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include <vector>
#include <memory>
#include <string>
#include "client/inputhandler.h"
#include "irrlichttypes_extrabloated.h"
#include "debug.h"
#include "client/shader.h"
@ -81,9 +82,8 @@ class RenderingEngine
{
public:
static const video::SColor MENU_SKY_COLOR;
static const float BASE_BLOOM_STRENGTH;
RenderingEngine(IEventReceiver *eventReceiver);
RenderingEngine(MyEventReceiver *eventReceiver);
~RenderingEngine();
void setResizable(bool resize);
@ -168,6 +168,12 @@ public:
const irr::core::dimension2d<u32> initial_screen_size,
const bool initial_window_maximized);
static PointerType getLastPointerType()
{
sanity_check(s_singleton && s_singleton->m_receiver);
return s_singleton->m_receiver->getLastPointerType();
}
private:
static void settingChangedCallback(const std::string &name, void *data);
v2u32 _getWindowSize() const;
@ -175,5 +181,6 @@ private:
std::unique_ptr<RenderingCore> core;
irr::IrrlichtDevice *m_device = nullptr;
irr::video::IVideoDriver *driver;
MyEventReceiver *m_receiver = nullptr;
static RenderingEngine *s_singleton;
};

View file

@ -322,6 +322,9 @@ public:
private:
// Are shaders even enabled?
bool m_enabled;
// The id of the thread that is allowed to use irrlicht directly
std::thread::id m_main_thread;
@ -360,6 +363,12 @@ ShaderSource::ShaderSource()
// Add a dummy ShaderInfo as the first index, named ""
m_shaderinfo_cache.emplace_back();
m_enabled = g_settings->getBool("enable_shaders");
if (!m_enabled) {
warningstream << "You are running " PROJECT_NAME_C " with shaders disabled, "
"this is not a recommended configuration." << std::endl;
}
// Add main global constant setter
addShaderConstantSetterFactory(new MainShaderConstantSetterFactory());
}
@ -368,9 +377,11 @@ ShaderSource::~ShaderSource()
{
MutexAutoLock lock(m_shaderinfo_cache_mutex);
if (!m_enabled)
return;
// Delete materials
video::IGPUProgrammingServices *gpu = RenderingEngine::get_video_driver()->
getGPUProgrammingServices();
auto *gpu = RenderingEngine::get_video_driver()->getGPUProgrammingServices();
for (ShaderInfo &i : m_shaderinfo_cache) {
if (!i.name.empty())
gpu->deleteShaderMaterial(i.material);
@ -499,9 +510,11 @@ void ShaderSource::rebuildShaders()
{
MutexAutoLock lock(m_shaderinfo_cache_mutex);
if (!m_enabled)
return;
// Delete materials
video::IGPUProgrammingServices *gpu = RenderingEngine::get_video_driver()->
getGPUProgrammingServices();
auto *gpu = RenderingEngine::get_video_driver()->getGPUProgrammingServices();
for (ShaderInfo &i : m_shaderinfo_cache) {
if (!i.name.empty()) {
gpu->deleteShaderMaterial(i.material);
@ -548,12 +561,11 @@ ShaderInfo ShaderSource::generateShader(const std::string &name,
}
shaderinfo.material = shaderinfo.base_material;
bool enable_shaders = g_settings->getBool("enable_shaders");
if (!enable_shaders)
if (!m_enabled)
return shaderinfo;
video::IVideoDriver *driver = RenderingEngine::get_video_driver();
video::IGPUProgrammingServices *gpu = driver->getGPUProgrammingServices();
auto *gpu = driver->getGPUProgrammingServices();
if (!driver->queryFeature(video::EVDF_ARB_GLSL) || !gpu) {
throw ShaderException(gettext("Shaders are enabled but GLSL is not "
"supported by the driver."));
@ -561,7 +573,7 @@ ShaderInfo ShaderSource::generateShader(const std::string &name,
// Create shaders header
bool fully_programmable = driver->getDriverType() == video::EDT_OGLES2 || driver->getDriverType() == video::EDT_OPENGL3;
std::stringstream shaders_header;
std::ostringstream shaders_header;
shaders_header
<< std::noboolalpha
<< std::showpoint // for GLSL ES
@ -588,10 +600,14 @@ ShaderInfo ShaderSource::generateShader(const std::string &name,
attribute mediump vec4 inVertexTangent;
attribute mediump vec4 inVertexBinormal;
)";
// Our vertex color has components reversed compared to what OpenGL
// normally expects, so we need to take that into account.
vertex_header += "#define inVertexColor (inVertexColor.bgra)\n";
fragment_header = R"(
precision mediump float;
)";
} else {
/* legacy OpenGL driver */
shaders_header << R"(
#version 120
#define lowp

View file

@ -137,8 +137,8 @@ void DirectionalLight::update_frustum(const Camera *cam, Client *client, bool fo
// when camera offset changes, adjust the current frustum view matrix to avoid flicker
v3s16 cam_offset = cam->getOffset();
if (cam_offset != shadow_frustum.camera_offset) {
v3f rotated_offset;
shadow_frustum.ViewMat.rotateVect(rotated_offset, intToFloat(cam_offset - shadow_frustum.camera_offset, BS));
v3f rotated_offset = shadow_frustum.ViewMat.rotateAndScaleVect(
intToFloat(cam_offset - shadow_frustum.camera_offset, BS));
shadow_frustum.ViewMat.setTranslation(shadow_frustum.ViewMat.getTranslation() + rotated_offset);
shadow_frustum.player += intToFloat(shadow_frustum.camera_offset - cam->getOffset(), BS);
shadow_frustum.camera_offset = cam_offset;

View file

@ -838,14 +838,10 @@ void Sky::updateStars()
);
core::CMatrix4<f32> a;
a.buildRotateFromTo(v3f(0, 1, 0), r);
v3f p = v3f(-d, 1, -d);
v3f p1 = v3f(d, 1, -d);
v3f p2 = v3f(d, 1, d);
v3f p3 = v3f(-d, 1, d);
a.rotateVect(p);
a.rotateVect(p1);
a.rotateVect(p2);
a.rotateVect(p3);
v3f p = a.rotateAndScaleVect(v3f(-d, 1, -d));
v3f p1 = a.rotateAndScaleVect(v3f(d, 1, -d));
v3f p2 = a.rotateAndScaleVect(v3f(d, 1, d));
v3f p3 = a.rotateAndScaleVect(v3f(-d, 1, d));
vertices.push_back(video::S3DVertex(p, {}, {}, {}));
vertices.push_back(video::S3DVertex(p1, {}, {}, {}));
vertices.push_back(video::S3DVertex(p2, {}, {}, {}));

View file

@ -26,6 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include <cassert>
#include <cstring> // memcpy
#include <memory>
namespace sound {

View file

@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#pragma once
#include <memory>
#include "al_helpers.h"
namespace sound {

View file

@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include <sstream>
#include <unordered_set>
#include <algorithm>
#include <queue>
#include "gamedef.h"
#include "inventory.h"
#include "util/serialize.h"

Some files were not shown because too many files have changed in this diff Show more