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:
commit
b6c099073f
183 changed files with 3919 additions and 1642 deletions
2
.gitattributes
vendored
2
.gitattributes
vendored
|
@ -3,3 +3,5 @@
|
|||
|
||||
*.cpp diff=cpp
|
||||
*.h diff=cpp
|
||||
|
||||
*.gltf binary
|
||||
|
|
7
.github/workflows/linux.yml
vendored
7
.github/workflows/linux.yml
vendored
|
@ -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
|
||||
|
|
4
.github/workflows/macos.yml
vendored
4
.github/workflows/macos.yml
vendored
|
@ -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
3
.gitignore
vendored
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
333
builtin/mainmenu/content/dlg_package.lua
Normal file
333
builtin/mainmenu/content/dlg_package.lua
Normal 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
|
|
@ -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")
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -7,9 +7,5 @@ void main(void)
|
|||
{
|
||||
varTexCoord = inTexCoord0.st;
|
||||
gl_Position = mWorldViewProj * inVertexPosition;
|
||||
#ifdef GL_ES
|
||||
varColor = inVertexColor.bgra;
|
||||
#else
|
||||
varColor = inVertexColor;
|
||||
#endif
|
||||
}
|
||||
|
|
|
@ -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 +
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -6,9 +6,5 @@ void main(void)
|
|||
varTexCoord = inTexCoord0.st;
|
||||
gl_Position = mWorldViewProj * inVertexPosition;
|
||||
|
||||
#ifdef GL_ES
|
||||
varColor = inVertexColor.bgra;
|
||||
#else
|
||||
varColor = inVertexColor;
|
||||
#endif
|
||||
}
|
||||
|
|
210
doc/lua_api.md
210
doc/lua_api.md
|
@ -274,7 +274,7 @@ Accepted formats are:
|
|||
|
||||
images: .png, .jpg, .tga, (deprecated:) .bmp
|
||||
sounds: .ogg vorbis
|
||||
models: .x, .b3d, .obj, .gltf (Minetest 5.10 or newer)
|
||||
models: .x, .b3d, .obj, (since version 5.10:) .gltf, .glb
|
||||
|
||||
Other formats won't be sent to the client (e.g. you can store .blend files
|
||||
in a folder for convenience, without the risk that such files are transferred)
|
||||
|
@ -294,7 +294,7 @@ depends on by supplying a file with an equal name.
|
|||
Only a subset of model file format features is supported:
|
||||
|
||||
Simple textured meshes (with multiple textures), optionally with normals.
|
||||
The .x and .b3d formats additionally support skeletal animation.
|
||||
The .x, .b3d and .gltf formats additionally support (a single) animation.
|
||||
|
||||
#### glTF
|
||||
|
||||
|
@ -302,9 +302,15 @@ The glTF model file format for now only serves as a
|
|||
more modern alternative to the other static model file formats;
|
||||
it unlocks no special rendering features.
|
||||
|
||||
Binary glTF (`.glb`) files are supported and recommended over `.gltf` files
|
||||
due to their space savings.
|
||||
|
||||
This means that many glTF features are not supported *yet*, including:
|
||||
|
||||
* Animation
|
||||
* Animations
|
||||
* Only a single animation is supported,
|
||||
use frame ranges within this animation.
|
||||
* Only integer frames are supported.
|
||||
* Cameras
|
||||
* Materials
|
||||
* Only base color textures are supported
|
||||
|
@ -490,6 +496,11 @@ to let the client generate textures on-the-fly.
|
|||
The modifiers are applied directly in sRGB colorspace,
|
||||
i.e. without gamma-correction.
|
||||
|
||||
### Notes
|
||||
|
||||
* `TEXMOD_UPSCALE`: The texture with the lower resolution will be automatically
|
||||
upscaled to the higher resolution texture.
|
||||
|
||||
### Texture overlaying
|
||||
|
||||
Textures can be overlaid by putting a `^` between them.
|
||||
|
@ -503,8 +514,9 @@ Example:
|
|||
default_dirt.png^default_grass_side.png
|
||||
|
||||
`default_grass_side.png` is overlaid over `default_dirt.png`.
|
||||
The texture with the lower resolution will be automatically upscaled to
|
||||
the higher resolution texture.
|
||||
|
||||
*See notes: `TEXMOD_UPSCALE`*
|
||||
|
||||
|
||||
### Texture grouping
|
||||
|
||||
|
@ -701,6 +713,8 @@ Apply a mask to the base image.
|
|||
|
||||
The mask is applied using binary AND.
|
||||
|
||||
*See notes: `TEXMOD_UPSCALE`*
|
||||
|
||||
#### `[sheet:<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 0–23 (i.e. in 15° steps)
|
||||
* The three most significant bits of `param2` tells which color is picked
|
||||
from the palette. The palette should have 8 pixels.
|
||||
* The five least significant bits store rotation in range 0–23 (i.e. in 15° steps)
|
||||
* `paramtype2 = "none"`
|
||||
* `param2` will not be used by the engine and can be used to store
|
||||
an arbitrary value
|
||||
|
@ -1470,7 +1493,8 @@ Look for examples in `games/devtest` or `games/minetest_game`.
|
|||
'Connected Glass'.
|
||||
* `allfaces`
|
||||
* Often used for partially-transparent nodes.
|
||||
* External and internal sides of textures are visible.
|
||||
* External sides of textures, and unlike other drawtypes, the external sides
|
||||
of other blocks, are visible from the inside.
|
||||
* `allfaces_optional`
|
||||
* Often used for leaves nodes.
|
||||
* This switches between `normal`, `glasslike` and `allfaces` according to
|
||||
|
@ -2729,6 +2753,8 @@ Version History
|
|||
* Formspec version 7 (5.8.0):
|
||||
* style[]: Add focused state for buttons
|
||||
* Add field_enter_after_edit[] (experimental)
|
||||
* Formspec version 8 (5.10.0)
|
||||
* scroll_container[]: content padding parameter
|
||||
|
||||
Elements
|
||||
--------
|
||||
|
@ -2812,7 +2838,7 @@ Elements
|
|||
* End of a container, following elements are no longer relative to this
|
||||
container.
|
||||
|
||||
### `scroll_container[<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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
BIN
games/devtest/mods/gltf/models/gltf_blender_cube.glb
Normal file
BIN
games/devtest/mods/gltf/models/gltf_blender_cube.glb
Normal file
Binary file not shown.
1
games/devtest/mods/gltf/models/gltf_simple_skin.gltf
Normal file
1
games/devtest/mods/gltf/models/gltf_simple_skin.gltf
Normal 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"}}
|
1
games/devtest/mods/gltf/models/gltf_spider_animated.gltf
Normal file
1
games/devtest/mods/gltf/models/gltf_spider_animated.gltf
Normal file
File diff suppressed because one or more lines are too long
|
@ -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})
|
||||
|
|
6
games/devtest/mods/testabms/README.md
Normal file
6
games/devtest/mods/testabms/README.md
Normal 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.
|
12
games/devtest/mods/testabms/after_node.lua
Normal file
12
games/devtest/mods/testabms/after_node.lua
Normal 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 },
|
||||
})
|
||||
|
56
games/devtest/mods/testabms/chances.lua
Normal file
56
games/devtest/mods/testabms/chances.lua
Normal 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
|
||||
})
|
||||
|
7
games/devtest/mods/testabms/init.lua
Normal file
7
games/devtest/mods/testabms/init.lua
Normal 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")
|
56
games/devtest/mods/testabms/intervals.lua
Normal file
56
games/devtest/mods/testabms/intervals.lua
Normal 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
|
||||
})
|
||||
|
58
games/devtest/mods/testabms/min_max.lua
Normal file
58
games/devtest/mods/testabms/min_max.lua
Normal 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
|
||||
})
|
||||
|
2
games/devtest/mods/testabms/mod.conf
Normal file
2
games/devtest/mods/testabms/mod.conf
Normal file
|
@ -0,0 +1,2 @@
|
|||
name = testabms
|
||||
description = Contains some nodes for test ABMs.
|
99
games/devtest/mods/testabms/neighbors.lua
Normal file
99
games/devtest/mods/testabms/neighbors.lua
Normal 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
|
||||
})
|
||||
|
BIN
games/devtest/mods/testabms/textures/testabms_after_node.png
Normal file
BIN
games/devtest/mods/testabms/textures/testabms_after_node.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 179 B |
BIN
games/devtest/mods/testabms/textures/testabms_wait_node.png
Normal file
BIN
games/devtest/mods/testabms/textures/testabms_wait_node.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 183 B |
|
@ -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
|
||||
|
|
|
@ -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"..
|
||||
|
|
|
@ -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 }
|
||||
|
|
BIN
games/devtest/mods/testnodes/textures/testnodes_128x128_rgb.png
Normal file
BIN
games/devtest/mods/testnodes/textures/testnodes_128x128_rgb.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
BIN
games/devtest/mods/testnodes/textures/testnodes_mask_WRGBKW.png
Normal file
BIN
games/devtest/mods/testnodes/textures/testnodes_mask_WRGBKW.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 148 B |
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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})
|
||||
|
||||
--
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -410,6 +410,7 @@ public:
|
|||
{
|
||||
bool different =
|
||||
MaterialType != b.MaterialType ||
|
||||
ColorParam != b.ColorParam ||
|
||||
MaterialTypeParam != b.MaterialTypeParam ||
|
||||
Thickness != b.Thickness ||
|
||||
Wireframe != b.Wireframe ||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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); }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 primitive’s rendering"
|
||||
return; // "When positions are not specified, client implementations SHOULD skip primitive’s 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
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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}
|
||||
)
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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")) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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, {}, {}, {}));
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue