diff --git a/.gitattributes b/.gitattributes index 06b76c6c8..ecd9a7a29 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,3 +3,5 @@ *.cpp diff=cpp *.h diff=cpp + +*.gltf binary diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 2d0c72907..a2ec10a0a 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -88,7 +88,7 @@ jobs: - name: Install deps run: | source ./util/ci/common.sh - install_linux_deps clang-7 llvm + install_linux_deps clang-7 llvm-7 - name: Build run: | @@ -102,6 +102,11 @@ jobs: run: | ./bin/minetest --run-unittests + # Do this here because we have ASan and error paths are sensitive to dangling pointers + - name: Test error cases + run: | + ./util/test_error_cases.sh + # Current clang version clang_18: runs-on: ubuntu-24.04 diff --git a/.github/workflows/lua.yml b/.github/workflows/lua.yml index 0073eca2e..1f83c7688 100644 --- a/.github/workflows/lua.yml +++ b/.github/workflows/lua.yml @@ -35,7 +35,7 @@ jobs: - name: Integration test + devtest run: | - ./util/test_multiplayer.sh + serverconf="profiler.load=true" ./util/test_multiplayer.sh luacheck: name: "Builtin Luacheck and Unit Tests" diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 34556ce8c..731d7d719 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -29,7 +29,7 @@ on: jobs: build: - # use macOS 13 since it's the last one that still runs on x86 + # use lowest possible macOS running on x86_64 supported by brew to support more users runs-on: macos-13 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/whitespace_checks.yml b/.github/workflows/whitespace_checks.yml new file mode 100644 index 000000000..cc16e7b22 --- /dev/null +++ b/.github/workflows/whitespace_checks.yml @@ -0,0 +1,45 @@ +name: whitespace_checks + +# Check whitespaces of the following file types +# Not checked: .lua, .yml, .properties, .conf, .java, .py, .svg, .gradle, .xml, ... +# (luacheck already checks .lua files) +on: + push: + paths: + - '**.txt' + - '**.md' + - '**.[ch]' + - '**.cpp' + - '**.hpp' + - '**.sh' + - '**.cmake' + - '**.glsl' + pull_request: + paths: + - '**.txt' + - '**.md' + - '**.[ch]' + - '**.cpp' + - '**.hpp' + - '**.sh' + - '**.cmake' + - '**.glsl' + +jobs: + trailing_whitespaces: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # Line endings are already ensured by .gitattributes + - name: Check trailing whitespaces + run: if git ls-files | grep -E '\.txt$|\.md$|\.[ch]$|\.cpp$|\.hpp$|\.sh$|\.cmake$|\.glsl$' | xargs grep -n '\s$'; then echo -e "\033[0;31mFound trailing whitespace"; (exit 1); else (exit 0); fi + + tabs_lua_api_files: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # Some files should not contain tabs + - name: Check tabs in Lua API files + run: if grep -n $'\t' doc/lua_api.md doc/client_lua_api.md; then echo -e "\033[0;31mFound tab in markdown file"; (exit 1); else (exit 0); fi + + diff --git a/.gitignore b/.gitignore index 8ff758720..c7879380b 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ tags !tags/ gtags.files .idea +.qtcreator/ # Codelite *.project # Visual Studio Code & plugins @@ -109,6 +110,8 @@ src/cmake_config_githash.h *.layout *.o *.a +*.dump +*.dmp *.ninja .ninja* *.gch diff --git a/.luacheckrc b/.luacheckrc index f184e6d59..afc136c7c 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -37,6 +37,12 @@ files["builtin/client/register.lua"] = { } } +files["builtin/common/math.lua"] = { + globals = { + "math", + }, +} + files["builtin/common/misc_helpers.lua"] = { globals = { "dump", "dump2", "table", "math", "string", @@ -46,7 +52,7 @@ files["builtin/common/misc_helpers.lua"] = { } files["builtin/common/vector.lua"] = { - globals = { "vector" }, + globals = { "vector", "math" }, } files["builtin/game/voxelarea.lua"] = { diff --git a/CMakeLists.txt b/CMakeLists.txt index 6623fa828..a9a0ef000 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,6 +56,11 @@ if((WIN32 AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") OR APPLE) endif() set(ENABLE_LTO ${DEFAULT_ENABLE_LTO} CACHE BOOL "Use Link Time Optimization") +set(BUILD_WITH_TRACY FALSE CACHE BOOL + "Fetch and build with the Tracy profiler client") +set(FETCH_TRACY_GIT_TAG "master" CACHE STRING + "Git tag for fetching Tracy client. Match with your server (gui) version") + set(DEFAULT_RUN_IN_PLACE FALSE) if(WIN32) set(DEFAULT_RUN_IN_PLACE TRUE) @@ -283,6 +288,8 @@ if(BUILD_UNITTESTS OR BUILD_BENCHMARKS) add_subdirectory(lib/catch2) endif() +add_subdirectory(lib/tiniergltf) + # Subdirectories # Be sure to add all relevant definitions above this add_subdirectory(src) @@ -368,3 +375,19 @@ if(BUILD_DOCUMENTATION) ) endif() endif() + +# Fetch Tracy +if(BUILD_WITH_TRACY) + include(FetchContent) + + message(STATUS "Fetching Tracy (${FETCH_TRACY_GIT_TAG})...") + FetchContent_Declare( + tracy + GIT_REPOSITORY https://github.com/wolfpld/tracy.git + GIT_TAG ${FETCH_TRACY_GIT_TAG} + GIT_SHALLOW TRUE + GIT_PROGRESS TRUE + ) + FetchContent_MakeAvailable(tracy) + message(STATUS "Fetching Tracy - done") +endif() diff --git a/LICENSE.txt b/LICENSE.txt index de76c7a80..03ca35100 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -57,12 +57,10 @@ srifqi: textures/base/pack/minimap_btn.png Zughy: - textures/base/pack/cdb_add.png textures/base/pack/cdb_downloading.png textures/base/pack/cdb_queued.png textures/base/pack/cdb_update.png textures/base/pack/cdb_update_cropped.png - textures/base/pack/cdb_viewonline.png textures/base/pack/settings_btn.png textures/base/pack/settings_info.png textures/base/pack/settings_reset.png @@ -79,7 +77,6 @@ kilbith: textures/base/pack/progress_bar_bg.png SmallJoker: - textures/base/pack/cdb_clear.png textures/base/pack/server_favorite_delete.png (based on server_favorite.png) DS: diff --git a/README.md b/README.md index 5724359d6..919cb144c 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,7 @@ Command-line options Compiling --------- +- [Compiling - common information](doc/compiling/README.md) - [Compiling on GNU/Linux](doc/compiling/linux.md) - [Compiling on Windows](doc/compiling/windows.md) - [Compiling on MacOS](doc/compiling/macos.md) diff --git a/android/app/build.gradle b/android/app/build.gradle index fe6c4ab0d..cefc473af 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -8,7 +8,7 @@ android { compileSdk 34 targetSdkVersion 34 versionName "${versionMajor}.${versionMinor}.${versionPatch}" - versionCode project.versionCode + versionCode versionMajor * 1000000 + versionMinor * 10000 + versionPatch * 100 + versionBuild } buildFeatures { @@ -116,18 +116,6 @@ clean { delete new File("src/main/assets", "Minetest.zip") } -// Map for the version code that gives each ABI a value. -import com.android.build.OutputFile - -def abiCodes = ['armeabi-v7a': 0, 'arm64-v8a': 1] -android.applicationVariants.all { variant -> - variant.outputs.each { - output -> - def abiName = output.getFilter(OutputFile.ABI) - output.versionCodeOverride = abiCodes.get(abiName, 0) + variant.versionCode - } -} - dependencies { implementation project(':native') implementation 'androidx.appcompat:appcompat:1.6.1' diff --git a/android/build.gradle b/android/build.gradle index 69fd26625..2017c536c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,13 +1,12 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. project.ext.set("versionMajor", 5) // Version Major -project.ext.set("versionMinor", 10) // Version Minor +project.ext.set("versionMinor", 10) // Version Minor project.ext.set("versionPatch", 0) // Version Patch // ^ keep in sync with cmake -project.ext.set("versionCode", 48) // Android Version Code -// NOTE: +2 after each release! -// +1 for ARM and +1 for ARM64 APK's, because -// each APK must have a larger `versionCode` than the previous + +project.ext.set("versionBuild", 0) // Version Build +// ^ fourth version number to allow releasing Android-only fixes and beta versions buildscript { ext.ndk_version = '26.2.11394342' diff --git a/android/native/build.gradle b/android/native/build.gradle index 0be7c1d99..806dda2f0 100644 --- a/android/native/build.gradle +++ b/android/native/build.gradle @@ -8,7 +8,7 @@ android { compileSdk 34 targetSdkVersion 34 externalNativeBuild { - cmake { + cmake { arguments "-DANDROID_STL=c++_shared", "-DENABLE_CURL=1", "-DENABLE_SOUND=1", "-DENABLE_GETTEXT=1", diff --git a/builtin/client/death_formspec.lua b/builtin/client/death_formspec.lua deleted file mode 100644 index c25c799ab..000000000 --- a/builtin/client/death_formspec.lua +++ /dev/null @@ -1,15 +0,0 @@ --- CSM death formspec. Only used when clientside modding is enabled, otherwise --- handled by the engine. - -core.register_on_death(function() - local formspec = "size[11,5.5]bgcolor[#320000b4;true]" .. - "label[4.85,1.35;" .. fgettext("You died") .. - "]button_exit[4,3;3,0.5;btn_respawn;".. fgettext("Respawn") .."]" - core.show_formspec("bultin:death", formspec) -end) - -core.register_on_formspec_input(function(formname, fields) - if formname == "bultin:death" then - core.send_respawn() - end -end) diff --git a/builtin/client/init.lua b/builtin/client/init.lua index 301a8050c..8d01c99a4 100644 --- a/builtin/client/init.lua +++ b/builtin/client/init.lua @@ -9,6 +9,5 @@ dofile(commonpath .. "mod_storage.lua") dofile(commonpath .. "chatcommands.lua") dofile(commonpath .. "information_formspecs.lua") dofile(clientpath .. "chatcommands.lua") -dofile(clientpath .. "death_formspec.lua") dofile(clientpath .. "misc.lua") assert(loadfile(commonpath .. "item_s.lua"))({}) -- Just for push/read node functions diff --git a/builtin/common/information_formspecs.lua b/builtin/common/information_formspecs.lua index 3fa397d25..270631fc9 100644 --- a/builtin/common/information_formspecs.lua +++ b/builtin/common/information_formspecs.lua @@ -69,7 +69,7 @@ local function build_chatcommands_formspec(name, sel, copy) description = cmds[2].description if copy then local msg = S("Command: @1 @2", - core.colorize("#0FF", "/" .. cmds[1]), cmds[2].params) + core.colorize("#0FF", (INIT == "client" and "." or "/") .. cmds[1]), cmds[2].params) if INIT == "client" then core.display_chat_message(msg) else diff --git a/builtin/common/item_s.lua b/builtin/common/item_s.lua index 72a722ed1..673c83877 100644 --- a/builtin/common/item_s.lua +++ b/builtin/common/item_s.lua @@ -166,20 +166,19 @@ function core.is_colored_paramtype(ptype) end function core.strip_param2_color(param2, paramtype2) - if not core.is_colored_paramtype(paramtype2) then + if paramtype2 == "color" then + return param2 + elseif paramtype2 == "colorfacedir" then + return math.floor(param2 / 32) * 32 + elseif paramtype2 == "color4dir" then + return math.floor(param2 / 4) * 4 + elseif paramtype2 == "colorwallmounted" then + return math.floor(param2 / 8) * 8 + elseif paramtype2 == "colordegrotate" then + return math.floor(param2 / 32) * 32 + else return nil end - if paramtype2 == "colorfacedir" then - param2 = math.floor(param2 / 32) * 32 - elseif paramtype2 == "color4dir" then - param2 = math.floor(param2 / 4) * 4 - elseif paramtype2 == "colorwallmounted" then - param2 = math.floor(param2 / 8) * 8 - elseif paramtype2 == "colordegrotate" then - param2 = math.floor(param2 / 32) * 32 - end - -- paramtype2 == "color" requires no modification. - return param2 end -- Content ID caching diff --git a/builtin/common/math.lua b/builtin/common/math.lua new file mode 100644 index 000000000..84c6c619b --- /dev/null +++ b/builtin/common/math.lua @@ -0,0 +1,41 @@ +--[[ + Math utils. +--]] + +function math.hypot(x, y) + return math.sqrt(x * x + y * y) +end + +function math.sign(x, tolerance) + tolerance = tolerance or 0 + if x > tolerance then + return 1 + elseif x < -tolerance then + return -1 + end + return 0 +end + +function math.factorial(x) + assert(x % 1 == 0 and x >= 0, "factorial expects a non-negative integer") + if x >= 171 then + -- 171! is greater than the biggest double, no need to calculate + return math.huge + end + local v = 1 + for k = 2, x do + v = v * k + end + return v +end + +function math.round(x) + if x < 0 then + local int = math.ceil(x) + local frac = x - int + return int - ((frac <= -0.5) and 1 or 0) + end + local int = math.floor(x) + local frac = x - int + return int + ((frac >= 0.5) and 1 or 0) +end diff --git a/builtin/common/misc_helpers.lua b/builtin/common/misc_helpers.lua index fb38c1b35..d0942b2d2 100644 --- a/builtin/common/misc_helpers.lua +++ b/builtin/common/misc_helpers.lua @@ -3,6 +3,7 @@ -------------------------------------------------------------------------------- -- Localize functions to avoid table lookups (better performance). local string_sub, string_find = string.sub, string.find +local math = math -------------------------------------------------------------------------------- local function basic_dump(o) @@ -220,47 +221,6 @@ function string:trim() return self:match("^%s*(.-)%s*$") end --------------------------------------------------------------------------------- -function math.hypot(x, y) - return math.sqrt(x * x + y * y) -end - --------------------------------------------------------------------------------- -function math.sign(x, tolerance) - tolerance = tolerance or 0 - if x > tolerance then - return 1 - elseif x < -tolerance then - return -1 - end - return 0 -end - --------------------------------------------------------------------------------- -function math.factorial(x) - assert(x % 1 == 0 and x >= 0, "factorial expects a non-negative integer") - if x >= 171 then - -- 171! is greater than the biggest double, no need to calculate - return math.huge - end - local v = 1 - for k = 2, x do - v = v * k - end - return v -end - -function math.round(x) - if x < 0 then - local int = math.ceil(x) - local frac = x - int - return int - ((frac <= -0.5) and 1 or 0) - end - local int = math.floor(x) - local frac = x - int - return int + ((frac >= 0.5) and 1 or 0) -end - local formspec_escapes = { ["\\"] = "\\\\", ["["] = "\\[", @@ -275,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 = {} @@ -604,12 +574,14 @@ function core.strip_colors(str) return (str:gsub(ESCAPE_CHAR .. "%([bc]@[^)]+%)", "")) end -function core.translate(textdomain, str, ...) +local function translate(textdomain, str, num, ...) local start_seq - if textdomain == "" then + if textdomain == "" and num == "" then start_seq = ESCAPE_CHAR .. "T" - else + elseif num == "" then start_seq = ESCAPE_CHAR .. "(T@" .. textdomain .. ")" + else + start_seq = ESCAPE_CHAR .. "(T@" .. textdomain .. "@" .. num .. ")" end local arg = {n=select('#', ...), ...} local end_seq = ESCAPE_CHAR .. "E" @@ -640,8 +612,31 @@ function core.translate(textdomain, str, ...) return start_seq .. translated .. end_seq end +function core.translate(textdomain, str, ...) + return translate(textdomain, str, "", ...) +end + +function core.translate_n(textdomain, str, str_plural, n, ...) + assert (type(n) == "number") + assert (n >= 0) + assert (math.floor(n) == n) + + -- Truncate n if too large + local max = 1000000 + if n >= 2 * max then + n = n % max + max + end + if n == 1 then + return translate(textdomain, str, "1", ...) + else + return translate(textdomain, str_plural, tostring(n), ...) + end +end + function core.get_translator(textdomain) - return function(str, ...) return core.translate(textdomain or "", str, ...) end + return + (function(str, ...) return core.translate(textdomain or "", str, ...) end), + (function(str, str_plural, n, ...) return core.translate_n(textdomain or "", str, str_plural, n, ...) end) end -------------------------------------------------------------------------------- @@ -702,6 +697,7 @@ function core.privs_to_string(privs, delim) list[#list + 1] = priv end end + table.sort(list) return table.concat(list, delim) end diff --git a/builtin/common/tests/after_spec.lua b/builtin/common/tests/after_spec.lua index cca1871a5..f23bbd70d 100644 --- a/builtin/common/tests/after_spec.lua +++ b/builtin/common/tests/after_spec.lua @@ -1,6 +1,7 @@ _G.core = {} _G.vector = {metatable = {}} +dofile("builtin/common/math.lua") dofile("builtin/common/vector.lua") dofile("builtin/common/misc_helpers.lua") diff --git a/builtin/common/tests/math_spec.lua b/builtin/common/tests/math_spec.lua new file mode 100644 index 000000000..108ab2b6a --- /dev/null +++ b/builtin/common/tests/math_spec.lua @@ -0,0 +1,16 @@ +_G.core = {} +dofile("builtin/common/math.lua") + +describe("math", function() + it("round()", function() + assert.equal(0, math.round(0)) + assert.equal(10, math.round(10.3)) + assert.equal(11, math.round(10.5)) + assert.equal(11, math.round(10.7)) + assert.equal(-10, math.round(-10.3)) + assert.equal(-11, math.round(-10.5)) + assert.equal(-11, math.round(-10.7)) + assert.equal(0, math.round(0.49999999999999994)) + assert.equal(0, math.round(-0.49999999999999994)) + end) +end) diff --git a/builtin/common/tests/misc_helpers_spec.lua b/builtin/common/tests/misc_helpers_spec.lua index 611c4b20f..10e2bf277 100644 --- a/builtin/common/tests/misc_helpers_spec.lua +++ b/builtin/common/tests/misc_helpers_spec.lua @@ -1,4 +1,5 @@ _G.core = {} +dofile("builtin/common/math.lua") dofile("builtin/common/vector.lua") dofile("builtin/common/misc_helpers.lua") diff --git a/builtin/common/tests/vector_spec.lua b/builtin/common/tests/vector_spec.lua index 67dbd2d6b..9a0458be4 100644 --- a/builtin/common/tests/vector_spec.lua +++ b/builtin/common/tests/vector_spec.lua @@ -1,4 +1,5 @@ _G.vector = {} +dofile("builtin/common/math.lua") dofile("builtin/common/vector.lua") describe("vector", function() @@ -113,12 +114,35 @@ describe("vector", function() assert.equal(vector.new(0, 1, -1), a:round()) end) + it("ceil()", function() + local a = vector.new(0.1, 0.9, -0.5) + assert.equal(vector.new(1, 1, 0), vector.ceil(a)) + assert.equal(vector.new(1, 1, 0), a:ceil()) + end) + + it("sign()", function() + local a = vector.new(-120.3, 0, 231.5) + assert.equal(vector.new(-1, 0, 1), vector.sign(a)) + assert.equal(vector.new(-1, 0, 1), a:sign()) + assert.equal(vector.new(0, 0, 1), vector.sign(a, 200)) + assert.equal(vector.new(0, 0, 1), a:sign(200)) + end) + + it("abs()", function() + local a = vector.new(-123.456, 0, 13) + assert.equal(vector.new(123.456, 0, 13), vector.abs(a)) + assert.equal(vector.new(123.456, 0, 13), a:abs()) + end) + it("apply()", function() local i = 0 local f = function(x) i = i + 1 return x + i end + local f2 = function(x, opt1, opt2, opt3) + return x + opt1 + opt2 + opt3 + end local a = vector.new(0.1, 0.9, -0.5) assert.equal(vector.new(1, 1, 0), vector.apply(a, math.ceil)) assert.equal(vector.new(1, 1, 0), a:apply(math.ceil)) @@ -126,6 +150,9 @@ describe("vector", function() assert.equal(vector.new(0.1, 0.9, 0.5), a:apply(math.abs)) assert.equal(vector.new(1.1, 2.9, 2.5), vector.apply(a, f)) assert.equal(vector.new(4.1, 5.9, 5.5), a:apply(f)) + local b = vector.new(1, 2, 3) + assert.equal(vector.new(4, 5, 6), vector.apply(b, f2, 1, 1, 1)) + assert.equal(vector.new(4, 5, 6), b:apply(f2, 1, 1, 1)) end) it("combine()", function() @@ -469,4 +496,13 @@ describe("vector", function() assert.True(vector.in_area(vector.new(-10, -10, -10), vector.new(-10, -10, -10), vector.new(10, 10, 10))) assert.False(vector.in_area(vector.new(-10, -10, -10), vector.new(10, 10, 10), vector.new(-11, -10, -10))) end) + + it("random_in_area()", function() + local min = vector.new(-100, -100, -100) + local max = vector.new(100, 100, 100) + for i = 1, 1000 do + local random = vector.random_in_area(min, max) + assert.True(vector.in_area(random, min, max)) + end + end) end) diff --git a/builtin/common/vector.lua b/builtin/common/vector.lua index be82401c3..fb1d2a7d9 100644 --- a/builtin/common/vector.lua +++ b/builtin/common/vector.lua @@ -5,6 +5,7 @@ Note: The vector.*-functions must be able to accept old vectors that had no meta -- localize functions local setmetatable = setmetatable +local math = math vector = {} @@ -97,18 +98,26 @@ function vector.floor(v) end function vector.round(v) - return fast_new( - math.round(v.x), - math.round(v.y), - math.round(v.z) - ) + return vector.apply(v, math.round) end -function vector.apply(v, func) +function vector.ceil(v) + return vector.apply(v, math.ceil) +end + +function vector.sign(v, tolerance) + return vector.apply(v, math.sign, tolerance) +end + +function vector.abs(v) + return vector.apply(v, math.abs) +end + +function vector.apply(v, func, ...) return fast_new( - func(v.x), - func(v.y), - func(v.z) + func(v.x, ...), + func(v.y, ...), + func(v.z, ...) ) end @@ -387,6 +396,14 @@ function vector.random_direction() return fast_new(x/l, y/l, z/l) end +function vector.random_in_area(min, max) + return fast_new( + math.random(min.x, max.x), + math.random(min.y, max.y), + math.random(min.z, max.z) + ) +end + if rawget(_G, "core") and core.set_read_vector and core.set_push_vector then local function read_vector(v) return v.x, v.y, v.z diff --git a/builtin/emerge/env.lua b/builtin/emerge/env.lua index 2b32a0339..5beb6285c 100644 --- a/builtin/emerge/env.lua +++ b/builtin/emerge/env.lua @@ -23,6 +23,8 @@ core.add_node = core.set_node -- we don't deal with metadata currently core.swap_node = core.set_node +core.bulk_swap_node = core.bulk_set_node + function core.remove_node(pos) return core.vmanip:set_node_at(pos, {name="air"}) end diff --git a/builtin/fstk/buttonbar.lua b/builtin/fstk/buttonbar.lua index 59b7a586f..e53814751 100644 --- a/builtin/fstk/buttonbar.lua +++ b/builtin/fstk/buttonbar.lua @@ -19,7 +19,7 @@ local BASE_SPACING = 0.1 local function get_scroll_btn_width() - return core.settings:get_bool("enable_touch") and 0.8 or 0.5 + return core.settings:get_bool("touch_gui") and 0.8 or 0.5 end local function buttonbar_formspec(self) @@ -28,10 +28,8 @@ local function buttonbar_formspec(self) end local formspec = { - "style_type[box;noclip=true]", string.format("box[%f,%f;%f,%f;%s]", self.pos.x, self.pos.y, self.size.x, self.size.y, self.bgcolor), - "style_type[box;noclip=false]", } local btn_size = self.size.y - 2*BASE_SPACING @@ -71,7 +69,7 @@ local function buttonbar_formspec(self) y = self.pos.y + BASE_SPACING, } - table.insert(formspec, string.format("image_button[%f,%f;%f,%f;%s;%s;%s;true;false]tooltip[%s;%s]", + table.insert(formspec, string.format("image_button[%f,%f;%f,%f;%s;%s;%s;false;false]tooltip[%s;%s]", btn_pos.x, btn_pos.y, btn_size, btn_size, btn.image, btn.name, btn.caption, btn.name, btn.tooltip)) end @@ -86,9 +84,6 @@ local function buttonbar_formspec(self) y = self.pos.y + BASE_SPACING, } - table.insert(formspec, string.format("style[%s,%s;noclip=true]", - self.btn_prev_name, self.btn_next_name)) - table.insert(formspec, string.format("button[%f,%f;%f,%f;%s;<]", btn_prev_pos.x, btn_prev_pos.y, get_scroll_btn_width(), btn_size, self.btn_prev_name)) diff --git a/builtin/fstk/tabview.lua b/builtin/fstk/tabview.lua index f09c4df2d..42fc9ac18 100644 --- a/builtin/fstk/tabview.lua +++ b/builtin/fstk/tabview.lua @@ -66,11 +66,22 @@ local function get_formspec(self) local content, prepend = tab.get_formspec(self, tab.name, tab.tabdata, tab.tabsize) - local tsize = tab.tabsize or { width = self.width, height = self.height } + 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 + + (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 prepend = string.format("size[%f,%f,%s]", tsize.width, tsize.height, dump(self.fixed_size)) + local anchor_pos = TABHEADER_H + orig_tsize.height / 2 + prepend = prepend .. ("anchor[0.5,%f]"):format(anchor_pos / tsize.height) + if tab.formspec_version then prepend = ("formspec_version[%d]"):format(tab.formspec_version) .. prepend end @@ -78,12 +89,15 @@ local function get_formspec(self) local end_button_size = 0.75 - local tab_header_size = { width = tsize.width, height = 0.85 } + local tab_header_size = { width = tsize.width, height = TABHEADER_H } if self.end_button then tab_header_size.width = tab_header_size.width - end_button_size - 0.1 end - local formspec = (prepend or "") .. self:tab_header(tab_header_size) .. content + local formspec = (prepend or "") + formspec = formspec .. ("bgcolor[;neither]container[0,%f]box[0,0;%f,%f;#0000008C]"):format( + TABHEADER_H, orig_tsize.width, orig_tsize.height) + formspec = formspec .. self:tab_header(tab_header_size) .. content if self.end_button then formspec = formspec .. @@ -98,6 +112,8 @@ local function get_formspec(self) self.end_button.name) end + formspec = formspec .. "container_end[]" + return formspec end diff --git a/builtin/game/chat.lua b/builtin/game/chat.lua index baab5212f..b7e2aea50 100644 --- a/builtin/game/chat.lua +++ b/builtin/game/chat.lua @@ -221,6 +221,7 @@ core.register_chatcommand("haspriv", { return true, S("No online player has the \"@1\" privilege.", param) else + table.sort(players_with_priv) return true, S("Players online with the \"@1\" privilege: @2", param, table.concat(players_with_priv, ", ")) diff --git a/builtin/game/death_screen.lua b/builtin/game/death_screen.lua new file mode 100644 index 000000000..77f56aaff --- /dev/null +++ b/builtin/game/death_screen.lua @@ -0,0 +1,31 @@ +local F = core.formspec_escape +local S = core.get_translator("__builtin") + +function core.show_death_screen(player, _reason) + local fs = { + "formspec_version[1]", + "size[11,5.5,true]", + "bgcolor[#320000b4;true]", + "label[4.85,1.35;", F(S("You died")), "]", + "button_exit[4,3;3,0.5;btn_respawn;", F(S("Respawn")), "]", + } + core.show_formspec(player:get_player_name(), "__builtin:death", table.concat(fs, "")) +end + +core.register_on_dieplayer(function(player, reason) + core.show_death_screen(player, reason) +end) + +core.register_on_joinplayer(function(player) + if player:get_hp() == 0 then + core.show_death_screen(player, nil) + end +end) + +core.register_on_player_receive_fields(function(player, formname, fields) + if formname == "__builtin:death" and fields.quit and player:get_hp() == 0 then + player:respawn() + core.log("action", player:get_player_name() .. " respawns at " .. + player:get_pos():to_string()) + end +end) diff --git a/builtin/game/features.lua b/builtin/game/features.lua index 22bf1859d..10884497c 100644 --- a/builtin/game/features.lua +++ b/builtin/game/features.lua @@ -42,6 +42,9 @@ core.features = { node_interaction_actor = true, moveresult_new_pos = true, override_item_remove_fields = true, + hotbar_hud_element = true, + bulk_lbms = true, + abm_without_neighbors = true, } function core.has_feature(arg) diff --git a/builtin/game/hud.lua b/builtin/game/hud.lua index 1bc4b48d7..eaf037e68 100644 --- a/builtin/game/hud.lua +++ b/builtin/game/hud.lua @@ -251,11 +251,31 @@ register_builtin_hud_element("minimap", { position = {x = 1, y = 0}, alignment = {x = -1, y = 1}, offset = {x = -10, y = 10}, - size = {x = 256, y = 256}, + size = {x = 0, y = -25}, }, show_elem = function(player, flags) + local proto_ver = core.get_player_information(player:get_player_name()).protocol_version -- Don't add a minimap for clients which already have it hardcoded in C++. - return flags.minimap and - core.get_player_information(player:get_player_name()).protocol_version >= 44 + return flags.minimap and proto_ver >= 44 + end, + update_def = function(player, elem_def) + local proto_ver = core.get_player_information(player:get_player_name()).protocol_version + -- Only use percentage when the client supports it. + elem_def.size = proto_ver >= 45 and {x = 0, y = -25} or {x = 256, y = 256} + end, +}) + +--- Hotbar + +register_builtin_hud_element("hotbar", { + elem_def = { + type = "hotbar", + position = {x = 0.5, y = 1}, + direction = 0, + alignment = {x = 0, y = -1}, + offset = {x = 0, y = -4}, -- Extra padding below. + }, + show_elem = function(player, flags) + return flags.hotbar end, }) diff --git a/builtin/game/init.lua b/builtin/game/init.lua index 4e16c686d..b3c64e729 100644 --- a/builtin/game/init.lua +++ b/builtin/game/init.lua @@ -38,6 +38,7 @@ dofile(gamepath .. "forceloading.lua") dofile(gamepath .. "hud.lua") dofile(gamepath .. "knockback.lua") dofile(gamepath .. "async.lua") +dofile(gamepath .. "death_screen.lua") core.after(0, builtin_shared.cache_content_ids) diff --git a/builtin/game/misc.lua b/builtin/game/misc.lua index a8a6700f9..25e19ddb0 100644 --- a/builtin/game/misc.lua +++ b/builtin/game/misc.lua @@ -6,14 +6,14 @@ local S = core.get_translator("__builtin") -- Misc. API functions -- --- @spec core.kick_player(String, String) :: Boolean -function core.kick_player(player_name, reason) +-- @spec core.kick_player(String, String, Boolean) :: Boolean +function core.kick_player(player_name, reason, reconnect) if type(reason) == "string" then reason = "Kicked: " .. reason else reason = "Kicked." end - return core.disconnect_player(player_name, reason) + return core.disconnect_player(player_name, reason, reconnect) end function core.check_player_privs(name, ...) @@ -298,3 +298,28 @@ do return valid_object_iterator(core.get_objects_in_area(min_pos, max_pos)) end end + +-- +-- Helper for LBM execution, called from C++ +-- + +function core.run_lbm(id, pos_list, dtime_s) + local lbm = core.registered_lbms[id] + assert(lbm, "Entry with given id not found in registered_lbms table") + core.set_last_run_mod(lbm.mod_origin) + if lbm.bulk_action then + return lbm.bulk_action(pos_list, dtime_s) + end + -- emulate non-bulk LBMs + local expect = core.get_node(pos_list[1]).name + -- engine guarantees that + -- 1) all nodes are the same content type + -- 2) the list is up-to-date when we're called + assert(expect ~= "ignore") + for _, pos in ipairs(pos_list) do + local n = core.get_node(pos) + if n.name == expect then -- might have been changed by previous call + lbm.action(pos, n, dtime_s) + end + end +end diff --git a/builtin/game/register.lua b/builtin/game/register.lua index 76cb72f87..3d99a6925 100644 --- a/builtin/game/register.lua +++ b/builtin/game/register.lua @@ -105,7 +105,12 @@ function core.register_lbm(spec) -- Add to core.registered_lbms check_modname_prefix(spec.name) check_node_list(spec.nodenames, "nodenames") - assert(type(spec.action) == "function", "Required field 'action' of type function") + local have = spec.action ~= nil + local have_bulk = spec.bulk_action ~= nil + assert(not have or type(spec.action) == "function", "Field 'action' must be a function") + assert(not have_bulk or type(spec.bulk_action) == "function", "Field 'bulk_action' must be a function") + assert(have ~= have_bulk, "Either 'action' or 'bulk_action' must be present") + core.registered_lbms[#core.registered_lbms + 1] = spec spec.mod_origin = core.get_current_modname() or "??" end diff --git a/builtin/init.lua b/builtin/init.lua index 49df70971..415087a54 100644 --- a/builtin/init.lua +++ b/builtin/init.lua @@ -42,6 +42,7 @@ local scriptdir = core.get_builtin_path() local commonpath = scriptdir .. "common" .. DIR_DELIM local asyncpath = scriptdir .. "async" .. DIR_DELIM +dofile(commonpath .. "math.lua") dofile(commonpath .. "vector.lua") dofile(commonpath .. "strict.lua") dofile(commonpath .. "serialize.lua") diff --git a/builtin/locale/__builtin.be.tr b/builtin/locale/__builtin.be.tr new file mode 100644 index 000000000..bd8ea999e --- /dev/null +++ b/builtin/locale/__builtin.be.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Вы загінулі +Respawn=Адрадзіцца diff --git a/builtin/locale/__builtin.bg.tr b/builtin/locale/__builtin.bg.tr new file mode 100644 index 000000000..43fe8f31e --- /dev/null +++ b/builtin/locale/__builtin.bg.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Умряхте +Respawn=Прераждане diff --git a/builtin/locale/__builtin.ca.tr b/builtin/locale/__builtin.ca.tr new file mode 100644 index 000000000..5cde155f9 --- /dev/null +++ b/builtin/locale/__builtin.ca.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Has mort +Respawn=Reaparèixer diff --git a/builtin/locale/__builtin.cs.tr b/builtin/locale/__builtin.cs.tr new file mode 100644 index 000000000..4f4b592e4 --- /dev/null +++ b/builtin/locale/__builtin.cs.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Zemřel jsi +Respawn=Oživit diff --git a/builtin/locale/__builtin.cy.tr b/builtin/locale/__builtin.cy.tr new file mode 100644 index 000000000..372da1a89 --- /dev/null +++ b/builtin/locale/__builtin.cy.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Buest ti farw +Respawn=Atgyfodi diff --git a/builtin/locale/__builtin.da.tr b/builtin/locale/__builtin.da.tr new file mode 100644 index 000000000..c34eceeb9 --- /dev/null +++ b/builtin/locale/__builtin.da.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Du døde +Respawn=Genopstå diff --git a/builtin/locale/__builtin.de.tr b/builtin/locale/__builtin.de.tr index 0552ef88b..3665a3f54 100644 --- a/builtin/locale/__builtin.de.tr +++ b/builtin/locale/__builtin.de.tr @@ -244,3 +244,5 @@ A total of @1 sample(s) were taken.=Es wurden insgesamt @1 Datenpunkt(e) aufgeze The output is limited to '@1'.=Die Ausgabe ist beschränkt auf „@1“. Saving of profile failed: @1=Speichern des Profils fehlgeschlagen: @1 Profile saved to @1=Profil abgespeichert nach @1 +You died=Sie sind gestorben +Respawn=Wiederbeleben diff --git a/builtin/locale/__builtin.el.tr b/builtin/locale/__builtin.el.tr new file mode 100644 index 000000000..c14180a62 --- /dev/null +++ b/builtin/locale/__builtin.el.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Πέθανες +Respawn=Επανεμφάνηση diff --git a/builtin/locale/__builtin.eo.tr b/builtin/locale/__builtin.eo.tr index c3fec6c37..f1fe5333b 100644 --- a/builtin/locale/__builtin.eo.tr +++ b/builtin/locale/__builtin.eo.tr @@ -244,3 +244,5 @@ A total of @1 sample(s) were taken.=Sume @1 ekzemplero(j) konserviĝis. The output is limited to '@1'.=La eligo estas limigita al «@1». Saving of profile failed: @1=Konservado de profilo malsukcesis: @1 Profile saved to @1=Profilo konservita al @1 +You died=Vi mortis +Respawn=Renaskiĝi diff --git a/builtin/locale/__builtin.es.tr b/builtin/locale/__builtin.es.tr new file mode 100644 index 000000000..3c4a66933 --- /dev/null +++ b/builtin/locale/__builtin.es.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Has muerto +Respawn=Reaparecer diff --git a/builtin/locale/__builtin.et.tr b/builtin/locale/__builtin.et.tr new file mode 100644 index 000000000..e0745c24a --- /dev/null +++ b/builtin/locale/__builtin.et.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Said surma +Respawn=Ärka ellu diff --git a/builtin/locale/__builtin.eu.tr b/builtin/locale/__builtin.eu.tr new file mode 100644 index 000000000..77f510855 --- /dev/null +++ b/builtin/locale/__builtin.eu.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Hil zara +Respawn=Birsortu diff --git a/builtin/locale/__builtin.fi.tr b/builtin/locale/__builtin.fi.tr new file mode 100644 index 000000000..f7c0ee038 --- /dev/null +++ b/builtin/locale/__builtin.fi.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Kuolit +Respawn=Synny uudelleen diff --git a/builtin/locale/__builtin.fil.tr b/builtin/locale/__builtin.fil.tr new file mode 100644 index 000000000..0755234a1 --- /dev/null +++ b/builtin/locale/__builtin.fil.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Namatay ka +Respawn=Mag-respawn diff --git a/builtin/locale/__builtin.fr.tr b/builtin/locale/__builtin.fr.tr index 6c3a244dc..d36574afd 100644 --- a/builtin/locale/__builtin.fr.tr +++ b/builtin/locale/__builtin.fr.tr @@ -244,3 +244,5 @@ A total of @1 sample(s) were taken.=@1 échantillons ont été collectés. The output is limited to '@1'.=La sortie est limitée à '@1'. Saving of profile failed: @1=La sauvegarde du profil a échoué : @1 Profile saved to @1=Le profil a été sauvegardé dans @1 +You died=Vous êtes mort +Respawn=Réapparaître diff --git a/builtin/locale/__builtin.ga.tr b/builtin/locale/__builtin.ga.tr new file mode 100644 index 000000000..e19959b10 --- /dev/null +++ b/builtin/locale/__builtin.ga.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Fuair tú bás +Respawn=Athsceith diff --git a/builtin/locale/__builtin.gl.tr b/builtin/locale/__builtin.gl.tr new file mode 100644 index 000000000..7f692f633 --- /dev/null +++ b/builtin/locale/__builtin.gl.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Morreches +Respawn=Reaparecer diff --git a/builtin/locale/__builtin.hu.tr b/builtin/locale/__builtin.hu.tr new file mode 100644 index 000000000..d7eebf7a0 --- /dev/null +++ b/builtin/locale/__builtin.hu.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Meghaltál +Respawn=Újraéledés diff --git a/builtin/locale/__builtin.id.tr b/builtin/locale/__builtin.id.tr index a541b0f8c..a7eba8c1c 100644 --- a/builtin/locale/__builtin.id.tr +++ b/builtin/locale/__builtin.id.tr @@ -244,3 +244,5 @@ A total of @1 sample(s) were taken.=Total @1 sampel yang diambil. The output is limited to '@1'.=Keluaran dibatasi ke '@1'. Saving of profile failed: @1=Penyimpanan profil gagal: @1 Profile saved to @1=Profil disimpan ke @1 +You died=Anda mati +Respawn=Bangkit kembali diff --git a/builtin/locale/__builtin.it.tr b/builtin/locale/__builtin.it.tr index aecb80a9a..24cf38104 100644 --- a/builtin/locale/__builtin.it.tr +++ b/builtin/locale/__builtin.it.tr @@ -245,3 +245,5 @@ A total of @1 sample(s) were taken.=Son stati ottenuti campioni per un totale di The output is limited to '@1'.=L'output è limitato a '@1'. Saving of profile failed: @1=Errore nel salvare il profilo: @1 Profile saved to @1=Profilo salvato in @1 +You died=Sei morto +Respawn=Rinasci diff --git a/builtin/locale/__builtin.ja.tr b/builtin/locale/__builtin.ja.tr new file mode 100644 index 000000000..1138edddd --- /dev/null +++ b/builtin/locale/__builtin.ja.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=死んでしまった +Respawn=リスポーン diff --git a/builtin/locale/__builtin.jbo.tr b/builtin/locale/__builtin.jbo.tr new file mode 100644 index 000000000..fe492bcdb --- /dev/null +++ b/builtin/locale/__builtin.jbo.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=.i do morsi +Respawn=tolcanci diff --git a/builtin/locale/__builtin.jv.tr b/builtin/locale/__builtin.jv.tr new file mode 100644 index 000000000..282cc5476 --- /dev/null +++ b/builtin/locale/__builtin.jv.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Panjenengan pejah +Respawn=Bangkit Malilh diff --git a/builtin/locale/__builtin.ko.tr b/builtin/locale/__builtin.ko.tr new file mode 100644 index 000000000..0d5d35a90 --- /dev/null +++ b/builtin/locale/__builtin.ko.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=사망했습니다 +Respawn=리스폰 diff --git a/builtin/locale/__builtin.kv.tr b/builtin/locale/__builtin.kv.tr new file mode 100644 index 000000000..f985d2ac4 --- /dev/null +++ b/builtin/locale/__builtin.kv.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Кулінныд +Respawn=Ловзьыны diff --git a/builtin/locale/__builtin.ky.tr b/builtin/locale/__builtin.ky.tr new file mode 100644 index 000000000..9ad468aa0 --- /dev/null +++ b/builtin/locale/__builtin.ky.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Сиз өлдүңүз. +Respawn=Кайтадан жаралуу diff --git a/builtin/locale/__builtin.lt.tr b/builtin/locale/__builtin.lt.tr new file mode 100644 index 000000000..cc613162c --- /dev/null +++ b/builtin/locale/__builtin.lt.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Jūs numirėte +Respawn=Prisikelti diff --git a/builtin/locale/__builtin.lv.tr b/builtin/locale/__builtin.lv.tr new file mode 100644 index 000000000..1da1fe3d8 --- /dev/null +++ b/builtin/locale/__builtin.lv.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Jūs nomirāt +Respawn=Atdzīvoties diff --git a/builtin/locale/__builtin.lzh.tr b/builtin/locale/__builtin.lzh.tr new file mode 100644 index 000000000..1af6aba98 --- /dev/null +++ b/builtin/locale/__builtin.lzh.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=尔死矣 +Respawn=复生 diff --git a/builtin/locale/__builtin.mn.tr b/builtin/locale/__builtin.mn.tr new file mode 100644 index 000000000..dd9a3465b --- /dev/null +++ b/builtin/locale/__builtin.mn.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Та үхсэн +Respawn=Дахин төрөх diff --git a/builtin/locale/__builtin.mr.tr b/builtin/locale/__builtin.mr.tr new file mode 100644 index 000000000..e4fcfb9b1 --- /dev/null +++ b/builtin/locale/__builtin.mr.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=तू मेलास +Respawn=पुनर्जन्म diff --git a/builtin/locale/__builtin.ms.tr b/builtin/locale/__builtin.ms.tr index ebf794e97..65ac557bf 100644 --- a/builtin/locale/__builtin.ms.tr +++ b/builtin/locale/__builtin.ms.tr @@ -244,3 +244,5 @@ A total of @1 sample(s) were taken.=Sebanyak @1 sampel telah diambil secara kese The output is limited to '@1'.=Output dihadkan kepada '@1'. Saving of profile failed: @1=Penyimpanan profil telah gagal: @1 Profile saved to @1=Profil telah disimpan ke @1 +You died=Anda telah meninggal +Respawn=Jelma semula diff --git a/builtin/locale/__builtin.nb.tr b/builtin/locale/__builtin.nb.tr new file mode 100644 index 000000000..b02a2d282 --- /dev/null +++ b/builtin/locale/__builtin.nb.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Du døde +Respawn=Gjenoppstå diff --git a/builtin/locale/__builtin.nl.tr b/builtin/locale/__builtin.nl.tr new file mode 100644 index 000000000..bd23c04b0 --- /dev/null +++ b/builtin/locale/__builtin.nl.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Je bent gestorven +Respawn=Herboren worden diff --git a/builtin/locale/__builtin.nn.tr b/builtin/locale/__builtin.nn.tr new file mode 100644 index 000000000..240191f73 --- /dev/null +++ b/builtin/locale/__builtin.nn.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Du døydde +Respawn=Kom opp att diff --git a/builtin/locale/__builtin.oc.tr b/builtin/locale/__builtin.oc.tr new file mode 100644 index 000000000..34b6577ad --- /dev/null +++ b/builtin/locale/__builtin.oc.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Setz mòrt·a +Respawn=Tornar diff --git a/builtin/locale/__builtin.pl.tr b/builtin/locale/__builtin.pl.tr new file mode 100644 index 000000000..ec0a9b7c1 --- /dev/null +++ b/builtin/locale/__builtin.pl.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Nie żyjesz +Respawn=Wróć do gry diff --git a/builtin/locale/__builtin.pt.tr b/builtin/locale/__builtin.pt.tr new file mode 100644 index 000000000..fbdb461e5 --- /dev/null +++ b/builtin/locale/__builtin.pt.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Você morreu +Respawn=Renascer diff --git a/builtin/locale/__builtin.pt_BR.tr b/builtin/locale/__builtin.pt_BR.tr index e12e9fae8..e48425bda 100644 --- a/builtin/locale/__builtin.pt_BR.tr +++ b/builtin/locale/__builtin.pt_BR.tr @@ -244,3 +244,5 @@ A total of @1 sample(s) were taken.=Um total de @1 amostra(s) foi coletada. The output is limited to '@1'.=A saída é limitada a '@1'. Saving of profile failed: @1=Falha ao salvar o perfil: @1 Profile saved to @1=Perfil salvo em @1 +You died=Você morreu +Respawn=Reviver diff --git a/builtin/locale/__builtin.ro.tr b/builtin/locale/__builtin.ro.tr new file mode 100644 index 000000000..7ff5e3c38 --- /dev/null +++ b/builtin/locale/__builtin.ro.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Ai murit +Respawn=Reînviere diff --git a/builtin/locale/__builtin.ru.tr b/builtin/locale/__builtin.ru.tr index d43fbc589..863cdd638 100644 --- a/builtin/locale/__builtin.ru.tr +++ b/builtin/locale/__builtin.ru.tr @@ -244,3 +244,5 @@ A total of @1 sample(s) were taken.=Всего было взято @1 образ The output is limited to '@1'.=Вывод ограничен значением '@1'. Saving of profile failed: @1=Не удалось сохранить данные профилирования: @1 Profile saved to @1=Данные профилирования сохранены в @1 +You died=Вы умерли +Respawn=Возродиться diff --git a/builtin/locale/__builtin.sk.tr b/builtin/locale/__builtin.sk.tr new file mode 100644 index 000000000..e9e93f6a3 --- /dev/null +++ b/builtin/locale/__builtin.sk.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Zomrel si +Respawn=Oživiť diff --git a/builtin/locale/__builtin.sl.tr b/builtin/locale/__builtin.sl.tr new file mode 100644 index 000000000..6dc77c3b2 --- /dev/null +++ b/builtin/locale/__builtin.sl.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Umrl si +Respawn=Ponovno oživi diff --git a/builtin/locale/__builtin.sr_Cyrl.tr b/builtin/locale/__builtin.sr_Cyrl.tr new file mode 100644 index 000000000..68551e77a --- /dev/null +++ b/builtin/locale/__builtin.sr_Cyrl.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Умро си +Respawn=Врати се у живот diff --git a/builtin/locale/__builtin.sr_Latn.tr b/builtin/locale/__builtin.sr_Latn.tr new file mode 100644 index 000000000..4adc496b4 --- /dev/null +++ b/builtin/locale/__builtin.sr_Latn.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Umro/la si. +Respawn=Vrati se u zivot diff --git a/builtin/locale/__builtin.sv.tr b/builtin/locale/__builtin.sv.tr new file mode 100644 index 000000000..115014506 --- /dev/null +++ b/builtin/locale/__builtin.sv.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Du dog +Respawn=Återuppstå diff --git a/builtin/locale/__builtin.sw.tr b/builtin/locale/__builtin.sw.tr new file mode 100644 index 000000000..92aa1f3e1 --- /dev/null +++ b/builtin/locale/__builtin.sw.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Umekufa. +Respawn=Respawn diff --git a/builtin/locale/__builtin.tok.tr b/builtin/locale/__builtin.tok.tr new file mode 100644 index 000000000..14d131d6a --- /dev/null +++ b/builtin/locale/__builtin.tok.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=sina moli +Respawn=o kama sin diff --git a/builtin/locale/__builtin.tr.tr b/builtin/locale/__builtin.tr.tr new file mode 100644 index 000000000..dafec5630 --- /dev/null +++ b/builtin/locale/__builtin.tr.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Öldün +Respawn=Yeniden Canlan diff --git a/builtin/locale/__builtin.tt.tr b/builtin/locale/__builtin.tt.tr new file mode 100644 index 000000000..a162cd9b7 --- /dev/null +++ b/builtin/locale/__builtin.tt.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Сез үлдегез +Respawn=Тергезелергә diff --git a/builtin/locale/__builtin.uk.tr b/builtin/locale/__builtin.uk.tr new file mode 100644 index 000000000..e82a10bde --- /dev/null +++ b/builtin/locale/__builtin.uk.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Ви загинули +Respawn=Відродитися diff --git a/builtin/locale/__builtin.vi.tr b/builtin/locale/__builtin.vi.tr new file mode 100644 index 000000000..53caac93c --- /dev/null +++ b/builtin/locale/__builtin.vi.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=Bạn đã bị chết +Respawn=Hồi sinh diff --git a/builtin/locale/__builtin.zh_CN.tr b/builtin/locale/__builtin.zh_CN.tr new file mode 100644 index 000000000..5b1077429 --- /dev/null +++ b/builtin/locale/__builtin.zh_CN.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=您已死亡 +Respawn=重生 diff --git a/builtin/locale/__builtin.zh_TW.tr b/builtin/locale/__builtin.zh_TW.tr new file mode 100644 index 000000000..5b1077429 --- /dev/null +++ b/builtin/locale/__builtin.zh_TW.tr @@ -0,0 +1,3 @@ +# textdomain: __builtin +You died=您已死亡 +Respawn=重生 diff --git a/builtin/mainmenu/content/contentdb.lua b/builtin/mainmenu/content/contentdb.lua index e0479cb4c..5d6d6c482 100644 --- a/builtin/mainmenu/content/contentdb.lua +++ b/builtin/mainmenu/content/contentdb.lua @@ -182,6 +182,23 @@ function contentdb.get_package_by_id(id) end +function contentdb.calculate_package_id(type, author, name) + local id = author:lower() .. "/" + if (type == nil or type == "game") and #name > 5 and name:sub(#name - 4) == "_game" then + id = id .. name:sub(1, #name - 5) + else + id = id .. name + end + return id +end + + +function contentdb.get_package_by_info(author, name) + local id = contentdb.calculate_package_id(nil, author, name) + return contentdb.package_by_id[id] +end + + -- Create a coroutine from `fn` and provide results to `callback` when complete (dead). -- Returns a resumer function. local function make_callback_coroutine(fn, callback) @@ -415,15 +432,7 @@ local function fetch_pkgs(params) local aliases = {} for _, package in pairs(packages) do - local name_len = #package.name - -- This must match what contentdb.update_paths() does! - package.id = package.author:lower() .. "/" - if package.type == "game" and name_len > 5 and package.name:sub(name_len - 4) == "_game" then - package.id = package.id .. package.name:sub(1, name_len - 5) - else - package.id = package.id .. package.name - end - + package.id = params.calculate_package_id(package.type, package.author, package.name) package.url_part = core.urlencode(package.author) .. "/" .. core.urlencode(package.name) if package.aliases then @@ -443,7 +452,7 @@ end function contentdb.fetch_pkgs(callback) contentdb.loading = true - core.handle_async(fetch_pkgs, nil, function(result) + core.handle_async(fetch_pkgs, { calculate_package_id = contentdb.calculate_package_id }, function(result) if result then contentdb.load_ok = true contentdb.load_error = false @@ -581,3 +590,78 @@ function contentdb.filter_packages(query, by_type) end end end + + +function contentdb.get_full_package_info(package, callback) + assert(package) + if package.full_info then + callback(package.full_info) + return + end + + local function fetch(params) + local version = core.get_version() + local base_url = core.settings:get("contentdb_url") + + local languages + local current_language = core.get_language() + if current_language ~= "" then + languages = { current_language, "en;q=0.8" } + else + languages = { "en" } + end + + local url = base_url .. + "/api/packages/" .. params.package.url_part .. "/for-client/?" .. + "protocol_version=" .. core.urlencode(core.get_max_supp_proto()) .. + "&engine_version=" .. core.urlencode(version.string) .. + "&formspec_version=" .. core.urlencode(core.get_formspec_version()) .. + "&include_images=false" + local http = core.get_http_api() + local response = http.fetch_sync({ + url = url, + extra_headers = { + "Accept-Language: " .. table.concat(languages, ", ") + }, + }) + if not response.succeeded then + return nil + end + + return core.parse_json(response.data) + end + + local function my_callback(value) + package.full_info = value + callback(value) + end + + if not core.handle_async(fetch, { package = package }, my_callback) then + core.log("error", "ERROR: async event failed") + callback(nil) + end +end + + +function contentdb.get_formspec_padding() + -- Padding is increased on Android to account for notches + -- TODO: use Android API to determine size of cut outs + return { x = PLATFORM == "Android" and 1 or 0.5, y = PLATFORM == "Android" and 0.25 or 0.5 } +end + + +function contentdb.get_formspec_size() + local window = core.get_window_info() + local size = { x = window.max_formspec_size.x, y = window.max_formspec_size.y } + + -- Minimum formspec size + local min_x = 15.5 + local min_y = 10 + if size.x < min_x or size.y < min_y then + local scale = math.max(min_x / size.x, min_y / size.y) + size.x = size.x * scale + size.y = size.y * scale + end + + return size +end diff --git a/builtin/mainmenu/content/dlg_contentdb.lua b/builtin/mainmenu/content/dlg_contentdb.lua index 84ef96800..8f232e490 100644 --- a/builtin/mainmenu/content/dlg_contentdb.lua +++ b/builtin/mainmenu/content/dlg_contentdb.lua @@ -26,68 +26,20 @@ end -- Filter local search_string = "" local cur_page = 1 -local num_per_page = 5 -local filter_type = 1 -local filter_types_titles = { - fgettext("All packages"), - fgettext("Games"), - fgettext("Mods"), - fgettext("Texture packs"), -} +local filter_type -- Automatic package installation local auto_install_spec = nil -local filter_types_type = { - nil, - "game", - "mod", - "txp", + +local filter_type_names = { + { "type_all", nil }, + { "type_game", "game" }, + { "type_mod", "mod" }, + { "type_txp", "txp" }, } -local function install_or_update_package(this, package) - local install_parent - if package.type == "mod" then - install_parent = core.get_modpath() - elseif package.type == "game" then - install_parent = core.get_gamepath() - elseif package.type == "txp" then - install_parent = core.get_texturepath() - else - error("Unknown package type: " .. package.type) - end - - if package.queued or package.downloading then - return - end - - local function on_confirm() - local dlg = create_install_dialog(package) - dlg:set_parent(this) - this:hide() - dlg:show() - - dlg:load_deps() - end - - if package.type == "mod" and #pkgmgr.games == 0 then - local dlg = messagebox("install_game", - fgettext("You need to install a game before you can install a mod")) - dlg:set_parent(this) - this:hide() - dlg:show() - elseif not package.path and core.is_dir(install_parent .. DIR_DELIM .. package.name) then - local dlg = create_confirm_overwrite(package, on_confirm) - dlg:set_parent(this) - this:hide() - dlg:show() - else - on_confirm() - end -end - - -- Resolves the package specification stored in auto_install_spec into an actual package. -- May only be called after the package list has been loaded successfully. local function resolve_auto_install_spec() @@ -145,7 +97,7 @@ end local function sort_and_filter_pkgs() contentdb.update_paths() contentdb.sort_packages() - contentdb.filter_packages(search_string, filter_types_type[filter_type]) + contentdb.filter_packages(search_string, filter_type) local auto_install_pkg = resolve_auto_install_spec() if auto_install_pkg then @@ -176,72 +128,151 @@ local function load() end -local function get_info_formspec(text) - local H = 9.5 +local function get_info_formspec(size, padding, text) return table.concat({ "formspec_version[6]", - "size[15.75,9.5]", - core.settings:get_bool("enable_touch") 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("enable_touch") and "padding[0.01,0.01]" or "position[0.5,0.55]", + "formspec_version[7]", + "size[", size.x, ",", size.y, "]", + "padding[0,0]", + "bgcolor[;true]", - "style[status,downloading,queued;border=false]", + "container[", window_padding.x, ",", window_padding.y, "]", - "container[0.375,0.375]", - "field[0,0;7.225,0.8;search_string;;", core.formspec_escape(search_string), "]", + -- Top-left: categories + make_category_button("type_all", fgettext("All"), selected_type == nil), + make_category_button("type_game", fgettext("Games"), selected_type == "game"), + make_category_button("type_mod", fgettext("Mods"), selected_type == "mod"), + make_category_button("type_txp", fgettext("Texture Packs"), selected_type == "txp"), + + -- Top-right: Search + "container[", W - search_box_width - 0.8*2, ",0]", + "field[0,0;", search_box_width, ",0.8;search_string;;", core.formspec_escape(search_string), "]", "field_enter_after_edit[search_string;true]", - "image_button[7.3,0;0.8,0.8;", core.formspec_escape(defaulttexturedir .. "search.png"), ";search;]", - "image_button[8.125,0;0.8,0.8;", core.formspec_escape(defaulttexturedir .. "clear.png"), ";clear;]", - "dropdown[9.175,0;2.7875,0.8;type;", table.concat(filter_types_titles, ","), ";", filter_type, "]", + "image_button[", search_box_width, ",0;0.8,0.8;", + core.formspec_escape(defaulttexturedir .. "search.png"), ";search;]", + "image_button[", search_box_width + 0.8, ",0;0.8,0.8;", + core.formspec_escape(defaulttexturedir .. "clear.png"), ";clear;]", "container_end[]", - -- Page nav buttons - "container[0,", H - 0.8 - 0.375, "]", - "button[0.375,0;5,0.8;back;", fgettext("Back to Main Menu"), "]", + -- Bottom strip start + "container[0,", H - 0.8, "]", + "button[0,0;2,0.8;back;", fgettext("Back"), "]", - "container[", W - 0.375 - 0.8*4 - 2, ",0]", - "image_button[0,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "start_icon.png;pstart;]", - "image_button[0.8,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "prev_icon.png;pback;]", + -- Bottom-center: Page nav buttons + "container[", (W - 1*4 - 2) / 2, ",0]", + "image_button[0,0;1,0.8;", core.formspec_escape(defaulttexturedir), "start_icon.png;pstart;]", + "image_button[1,0;1,0.8;", core.formspec_escape(defaulttexturedir), "prev_icon.png;pback;]", "style[pagenum;border=false]", - "button[1.6,0;2,0.8;pagenum;", tonumber(cur_page), " / ", tonumber(dlgdata.pagemax), "]", - "image_button[3.6,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "next_icon.png;pnext;]", - "image_button[4.4,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "end_icon.png;pend;]", - "container_end[]", + "button[2,0;2,0.8;pagenum;", tonumber(cur_page), " / ", tonumber(dlgdata.pagemax), "]", + "image_button[4,0;1,0.8;", core.formspec_escape(defaulttexturedir), "next_icon.png;pnext;]", + "image_button[5,0;1,0.8;", core.formspec_escape(defaulttexturedir), "end_icon.png;pend;]", + "container_end[]", -- page nav end - "container_end[]", + -- Bottom-right: updating + "container[", W - 3, ",0]", + "style[status,downloading,queued;border=false]", } if contentdb.number_downloading > 0 then - formspec[#formspec + 1] = "button[12.5875,0.375;2.7875,0.8;downloading;" + formspec[#formspec + 1] = "button[0,0;3,0.8;downloading;" if #contentdb.download_queue > 0 then formspec[#formspec + 1] = fgettext("$1 downloading,\n$2 queued", contentdb.number_downloading, #contentdb.download_queue) @@ -260,16 +291,19 @@ local function get_formspec(dlgdata) end if num_avail_updates == 0 then - formspec[#formspec + 1] = "button[12.5875,0.375;2.7875,0.8;status;" + formspec[#formspec + 1] = "button[0,0;3,0.8;status;" formspec[#formspec + 1] = fgettext("No updates") formspec[#formspec + 1] = "]" else - formspec[#formspec + 1] = "button[12.5875,0.375;2.7875,0.8;update_all;" + formspec[#formspec + 1] = "button[0,0;3,0.8;update_all;" formspec[#formspec + 1] = fgettext("Update All [$1]", num_avail_updates) formspec[#formspec + 1] = "]" end end + formspec[#formspec + 1] = "container_end[]" -- updating end + formspec[#formspec + 1] = "container_end[]" -- bottom strip end + if #contentdb.packages == 0 then formspec[#formspec + 1] = "label[4,4.75;" formspec[#formspec + 1] = fgettext("No results") @@ -281,81 +315,85 @@ local function get_formspec(dlgdata) formspec[#formspec + 1] = "tooltip[downloading;" .. fgettext("Downloading...") .. tooltip_colors formspec[#formspec + 1] = "tooltip[queued;" .. fgettext("Queued") .. tooltip_colors + formspec[#formspec + 1] = "container[0,1.425]" + + local cell_spacing, columns, cell_w, cell_h = fit_cells(num_per_page, { + x = W, + y = H - 1.425 - 0.25 - 0.8 + }) + local img_w = cell_h * 3 / 2 + local start_idx = (cur_page - 1) * num_per_page + 1 for i=start_idx, math.min(#contentdb.packages, start_idx+num_per_page-1) do local package = contentdb.packages[i] - local container_y = (i - start_idx) * 1.375 + (2*0.375 + 0.8) - formspec[#formspec + 1] = "container[0.375," - formspec[#formspec + 1] = container_y - formspec[#formspec + 1] = "]" - -- image - formspec[#formspec + 1] = "image[0,0;1.5,1;" - formspec[#formspec + 1] = core.formspec_escape(get_screenshot(package)) - formspec[#formspec + 1] = "]" + table.insert_all(formspec, { + "container[", + (cell_w + cell_spacing) * ((i - start_idx) % columns), + ",", + (cell_h + cell_spacing) * math.floor((i - start_idx) / columns), + "]", - -- title - formspec[#formspec + 1] = "label[1.875,0.1;" - formspec[#formspec + 1] = core.formspec_escape( - core.colorize(mt_color_green, package.title) .. - core.colorize("#BFBFBF", " by " .. package.author)) - formspec[#formspec + 1] = "]" + "box[0,0;", cell_w, ",", cell_h, ";#ffffff11]", - -- buttons - local description_width = W - 2.625 - 2 * 0.7 - 2 * 0.15 + -- image, + "image[0,0;", img_w, ",", cell_h, ";", + core.formspec_escape(get_screenshot(package, package.thumbnail, 2)), "]", - local second_base = "image_button[-1.55,0;0.7,0.7;" .. core.formspec_escape(defaulttexturedir) - local third_base = "image_button[-2.4,0;0.7,0.7;" .. core.formspec_escape(defaulttexturedir) - formspec[#formspec + 1] = "container[" - formspec[#formspec + 1] = W - 0.375*2 - formspec[#formspec + 1] = ",0.1]" + "label[", img_w + 0.25 + 0.05, ",0.5;", + core.formspec_escape( + core.colorize(mt_color_green, package.title) .. + core.colorize("#BFBFBF", " by " .. package.author)), "]", - if package.downloading then - formspec[#formspec + 1] = "animated_image[-1.7,-0.15;1,1;downloading;" - formspec[#formspec + 1] = core.formspec_escape(defaulttexturedir) - formspec[#formspec + 1] = "cdb_downloading.png;3;400;]" - elseif package.queued then - formspec[#formspec + 1] = second_base - formspec[#formspec + 1] = "cdb_queued.png;queued;]" - elseif not package.path then - local elem_name = "install_" .. i .. ";" - formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#71aa34]" - formspec[#formspec + 1] = second_base .. "cdb_add.png;" .. elem_name .. "]" - formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Install") .. tooltip_colors - else - if package.installed_release < package.release then - -- The install_ action also handles updating - local elem_name = "install_" .. i .. ";" - formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#28ccdf]" - formspec[#formspec + 1] = third_base .. "cdb_update.png;" .. elem_name .. "]" - formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Update") .. tooltip_colors + "textarea[", img_w + 0.25, ",0.75;", cell_w - img_w - 0.25, ",", cell_h - 0.75, ";;;", + core.formspec_escape(package.short_description), "]", - description_width = description_width - 0.7 - 0.15 - end + "style[view_", i, ";border=false]", + "style[view_", i, ":hovered;bgimg=", core.formspec_escape(defaulttexturedir .. "button_hover_semitrans.png"), "]", + "style[view_", i, ":pressed;bgimg=", core.formspec_escape(defaulttexturedir .. "button_press_semitrans.png"), "]", + "button[0,0;", cell_w, ",", cell_h, ";view_", i, ";]", + }) - local elem_name = "uninstall_" .. i .. ";" - formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#a93b3b]" - formspec[#formspec + 1] = second_base .. "cdb_clear.png;" .. elem_name .. "]" - formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Uninstall") .. tooltip_colors + if package.featured then + table.insert_all(formspec, { + "tooltip[0,0;0.8,0.8;", fgettext("Featured"), "]", + "image[0.2,0.2;0.4,0.4;", defaulttexturedir, "server_favorite.png]", + }) end - local web_elem_name = "view_" .. i .. ";" - formspec[#formspec + 1] = "image_button[-0.7,0;0.7,0.7;" .. - core.formspec_escape(defaulttexturedir) .. "cdb_viewonline.png;" .. web_elem_name .. "]" - formspec[#formspec + 1] = "tooltip[" .. web_elem_name .. - fgettext("View more information in a web browser") .. tooltip_colors - formspec[#formspec + 1] = "container_end[]" + table.insert_all(formspec, { + "container[", cell_w - 0.625,",", 0.25, "]", + }) - -- description - formspec[#formspec + 1] = "textarea[1.855,0.3;" - formspec[#formspec + 1] = tostring(description_width) - formspec[#formspec + 1] = ",0.8;;;" - formspec[#formspec + 1] = core.formspec_escape(package.short_description) - formspec[#formspec + 1] = "]" + if package.downloading then + table.insert_all(formspec, { + "animated_image[0,0;0.5,0.5;downloading;", defaulttexturedir, "cdb_downloading.png;3;400;;]", + }) + elseif package.queued then + table.insert_all(formspec, { + "image[0,0;0.5,0.5;", defaulttexturedir, "cdb_queued.png]", + }) + elseif package.path then + if package.installed_release < package.release then + table.insert_all(formspec, { + "image[0,0;0.5,0.5;", defaulttexturedir, "cdb_update.png]", + }) + else + table.insert_all(formspec, { + "image[0.1,0.1;0.3,0.3;", defaulttexturedir, "checkbox_64.png]", + }) + end + end - formspec[#formspec + 1] = "container_end[]" + table.insert_all(formspec, { + "container_end[]", + "container_end[]", + }) end + formspec[#formspec + 1] = "container_end[]" + formspec[#formspec + 1] = "container_end[]" + return table.concat(formspec) end @@ -364,14 +402,14 @@ local function handle_submit(this, fields) if fields.search or fields.key_enter_field == "search_string" then search_string = fields.search_string:trim() cur_page = 1 - contentdb.filter_packages(search_string, filter_types_type[filter_type]) + contentdb.filter_packages(search_string, filter_type) return true end if fields.clear then search_string = "" cur_page = 1 - contentdb.filter_packages("", filter_types_type[filter_type]) + contentdb.filter_packages("", filter_type) return true end @@ -407,12 +445,11 @@ local function handle_submit(this, fields) return true end - if fields.type then - local new_type = table.indexof(filter_types_titles, fields.type) - if new_type ~= filter_type then - filter_type = new_type + for _, pair in ipairs(filter_type_names) do + if fields[pair[1]] then + filter_type = pair[2] cur_page = 1 - contentdb.filter_packages(search_string, filter_types_type[filter_type]) + contentdb.filter_packages(search_string, filter_type) return true end end @@ -428,32 +465,20 @@ local function handle_submit(this, fields) return true end + local num_per_page = this.data.num_per_page local start_idx = (cur_page - 1) * num_per_page + 1 assert(start_idx ~= nil) for i=start_idx, math.min(#contentdb.packages, start_idx+num_per_page-1) do local package = contentdb.packages[i] assert(package) - if fields["install_" .. i] then - install_or_update_package(this, package) - return true - end - - if fields["uninstall_" .. i] then - local dlg = create_delete_content_dlg(package) + if fields["view_" .. i] or fields["title_" .. i] or fields["author_" .. i] then + local dlg = create_package_dialog(package) dlg:set_parent(this) this:hide() dlg:show() return true end - - if fields["view_" .. i] then - local url = ("%s/packages/%s?protocol_version=%d"):format( - core.settings:get("contentdb_url"), package.url_part, - core.get_max_supp_proto()) - core.open_url(url) - return true - end end return false @@ -462,8 +487,8 @@ end local function handle_events(event) if event == "DialogShow" then - -- On touchscreen, don't show the "MINETEST" header behind the dialog. - mm_game_theme.set_engine(core.settings:get_bool("enable_touch")) + -- Don't show the "MINETEST" header behind the dialog. + mm_game_theme.set_engine(true) -- If ContentDB is already loaded, auto-install packages here. do_auto_install() @@ -471,6 +496,11 @@ local function handle_events(event) return true end + if event == "WindowInfoChange" then + ui.update() + return true + end + return false end @@ -485,17 +515,7 @@ end function create_contentdb_dlg(type, install_spec) search_string = "" cur_page = 1 - if type then - -- table.indexof does not work on tables that contain `nil` - for i, v in pairs(filter_types_type) do - if v == type then - filter_type = i - break - end - end - else - filter_type = 1 - end + filter_type = type -- Keep the old auto_install_spec if the caller doesn't specify one. if install_spec then @@ -504,8 +524,10 @@ function create_contentdb_dlg(type, install_spec) load() - return dialog_create("contentdb", + local dlg = dialog_create("contentdb", get_formspec, handle_submit, handle_events) + dlg.data.num_per_page = calculate_num_per_page() + return dlg end diff --git a/builtin/mainmenu/content/dlg_install.lua b/builtin/mainmenu/content/dlg_install.lua index 0549e23be..3f43bd23c 100644 --- a/builtin/mainmenu/content/dlg_install.lua +++ b/builtin/mainmenu/content/dlg_install.lua @@ -22,13 +22,13 @@ end local function get_loading_formspec() - local ENABLE_TOUCH = core.settings:get_bool("enable_touch") - local w = ENABLE_TOUCH and 14 or 7 + local TOUCH_GUI = core.settings:get_bool("touch_gui") + local w = TOUCH_GUI and 14 or 7 local formspec = { "formspec_version[3]", "size[", w, ",9.05]", - ENABLE_TOUCH and "padding[0.01,0.01]" or "position[0.5,0.55]", + TOUCH_GUI and "padding[0.01,0.01]" or "position[0.5,0.55]", "label[3,4.525;", fgettext("Loading..."), "]", } return table.concat(formspec) @@ -110,18 +110,18 @@ local function get_formspec(data) message_bg = mt_color_orange end - local ENABLE_TOUCH = core.settings:get_bool("enable_touch") + local TOUCH_GUI = core.settings:get_bool("touch_gui") - local w = ENABLE_TOUCH and 14 or 7 + local w = TOUCH_GUI and 14 or 7 local padded_w = w - 2*0.375 - local dropdown_w = ENABLE_TOUCH and 10.2 or 4.25 + local dropdown_w = TOUCH_GUI and 10.2 or 4.25 local button_w = (padded_w - 0.25) / 3 local button_pad = button_w / 2 local formspec = { "formspec_version[3]", "size[", w, ",9.05]", - ENABLE_TOUCH and "padding[0.01,0.01]" or "position[0.5,0.55]", + TOUCH_GUI and "padding[0.01,0.01]" or "position[0.5,0.55]", "style[title;border=false]", "box[0,0;", w, ",0.8;#3333]", "button[0,0;", w, ",0.8;title;", fgettext("Install $1", package.title) , "]", @@ -244,3 +244,45 @@ function create_install_dialog(package) return dlg end + + +function install_or_update_package(parent, package) + local install_parent + if package.type == "mod" then + install_parent = core.get_modpath() + elseif package.type == "game" then + install_parent = core.get_gamepath() + elseif package.type == "txp" then + install_parent = core.get_texturepath() + else + error("Unknown package type: " .. package.type) + end + + if package.queued or package.downloading then + return + end + + local function on_confirm() + local dlg = create_install_dialog(package) + dlg:set_parent(parent) + parent:hide() + dlg:show() + + dlg:load_deps() + end + + if package.type == "mod" and #pkgmgr.games == 0 then + local dlg = messagebox("install_game", + fgettext("You need to install a game before you can install a mod")) + dlg:set_parent(parent) + parent:hide() + dlg:show() + elseif not package.path and core.is_dir(install_parent .. DIR_DELIM .. package.name) then + local dlg = create_confirm_overwrite(package, on_confirm) + dlg:set_parent(parent) + parent:hide() + dlg:show() + else + on_confirm() + end +end diff --git a/builtin/mainmenu/content/dlg_package.lua b/builtin/mainmenu/content/dlg_package.lua new file mode 100644 index 000000000..404e950c4 --- /dev/null +++ b/builtin/mainmenu/content/dlg_package.lua @@ -0,0 +1,333 @@ +--Minetest +--Copyright (C) 2018-24 rubenwardy +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +local function get_info_formspec(size, padding, text) + return table.concat({ + "formspec_version[6]", + "size[", size.x, ",", size.y, "]", + "padding[0,0]", + "bgcolor[;true]", + + "label[4,4.35;", text, "]", + "container[", padding.x, ",", size.y - 0.8 - padding.y, "]", + "button[0,0;2,0.8;back;", fgettext("Back"), "]", + "container_end[]", + }) +end + + +local function get_formspec(data) + local window_padding = contentdb.get_formspec_padding() + local size = contentdb.get_formspec_size() + size.x = math.min(size.x, 20) + local W = size.x - window_padding.x * 2 + local H = size.y - window_padding.y * 2 + + if not data.info then + if not data.loading and not data.loading_error then + data.loading = true + + contentdb.get_full_package_info(data.package, function(info) + data.loading = false + + if info == nil then + data.loading_error = true + ui.update() + return + end + + if info.forums then + info.forums = "https://forum.minetest.net/viewtopic.php?t=" .. info.forums + end + + assert(data.package.name == info.name) + data.info = info + ui.update() + end) + end + + -- get_full_package_info can return cached info immediately, so + -- check to see if that happened + if not data.info then + if data.loading_error then + return get_info_formspec(size, window_padding, fgettext("No packages could be retrieved")) + end + return get_info_formspec(size, window_padding, fgettext("Loading...")) + end + end + + -- Check installation status + contentdb.update_paths() + + local info = data.info + + local info_line = + fgettext("by $1 — $2 downloads — +$3 / $4 / -$5", + info.author, info.downloads, + info.reviews.positive, info.reviews.neutral, info.reviews.negative) + + local bottom_buttons_y = H - 0.8 + + local formspec = { + "formspec_version[7]", + "size[", size.x, ",", size.y, "]", + "padding[0,0]", + "bgcolor[;true]", + + "container[", window_padding.x, ",", window_padding.y, "]", + + "button[0,", bottom_buttons_y, ";2,0.8;back;", fgettext("Back"), "]", + "button[", W - 3, ",", bottom_buttons_y, ";3,0.8;open_contentdb;", fgettext("ContentDB page"), "]", + + "style_type[label;font_size=+24;font=bold]", + "label[0,0.4;", core.formspec_escape(info.title), "]", + "style_type[label;font_size=;font=]", + + "label[0,1.2;", core.formspec_escape(info_line), "]", + } + + table.insert_all(formspec, { + "container[", W - 6, ",0]" + }) + + local left_button_rect = "0,0;2.875,1" + local right_button_rect = "3.125,0;2.875,1" + if data.package.downloading then + formspec[#formspec + 1] = "animated_image[5,0;1,1;downloading;" + formspec[#formspec + 1] = core.formspec_escape(defaulttexturedir) + formspec[#formspec + 1] = "cdb_downloading.png;3;400;]" + elseif data.package.queued then + formspec[#formspec + 1] = "style[queued;border=false]" + formspec[#formspec + 1] = "image_button[5,0;1,1;" .. core.formspec_escape(defaulttexturedir) + formspec[#formspec + 1] = "cdb_queued.png;queued;]" + elseif not data.package.path then + formspec[#formspec + 1] = "style[install;bgcolor=green]" + formspec[#formspec + 1] = "button[" + formspec[#formspec + 1] = right_button_rect + formspec[#formspec + 1] =";install;" + formspec[#formspec + 1] = fgettext("Install [$1]", info.download_size) + formspec[#formspec + 1] = "]" + else + if data.package.installed_release < data.package.release then + -- The install_ action also handles updating + formspec[#formspec + 1] = "style[install;bgcolor=#28ccdf]" + formspec[#formspec + 1] = "button[" + formspec[#formspec + 1] = left_button_rect + formspec[#formspec + 1] = ";install;" + formspec[#formspec + 1] = fgettext("Update") + formspec[#formspec + 1] = "]" + end + + formspec[#formspec + 1] = "style[uninstall;bgcolor=#a93b3b]" + formspec[#formspec + 1] = "button[" + formspec[#formspec + 1] = right_button_rect + formspec[#formspec + 1] = ";uninstall;" + formspec[#formspec + 1] = fgettext("Uninstall") + formspec[#formspec + 1] = "]" + end + + local current_tab = data.current_tab or 1 + local tab_titles = { + fgettext("Description"), + fgettext("Information"), + } + + local tab_body_height = bottom_buttons_y - 2.8 + + table.insert_all(formspec, { + "container_end[]", + + "box[0,2.55;", W, ",", tab_body_height, ";#ffffff11]", + + "tabheader[0,2.55;", W, ",0.8;tabs;", + table.concat(tab_titles, ","), ";", current_tab, ";true;true]", + + "container[0,2.8]", + }) + + if current_tab == 1 then + -- Screenshots and description + local hypertext = "" .. core.hypertext_escape(info.short_description) .. "\n" + local winfo = core.get_window_info() + local fs_to_px = winfo.size.x / winfo.max_formspec_size.x + for i, ss in ipairs(info.screenshots) do + local path = get_screenshot(data.package, ss.url, 2) + hypertext = hypertext .. "" + if i ~= #info.screenshots then + hypertext = hypertext .. "" + end + end + hypertext = hypertext .. "\n" .. info.long_description.head + + local first = true + local function add_link_button(label, name) + if info[name] then + if not first then + hypertext = hypertext .. " | " + end + hypertext = hypertext .. "" .. core.hypertext_escape(label) .. "" + info.long_description.links["link_" .. name] = info[name] + first = false + end + end + + add_link_button(fgettext("Donate"), "donate_url") + add_link_button(fgettext("Website"), "website") + add_link_button(fgettext("Source"), "repo") + add_link_button(fgettext("Issue Tracker"), "issue_tracker") + add_link_button(fgettext("Translate"), "translation_url") + add_link_button(fgettext("Forum Topic"), "forums") + + hypertext = hypertext .. "\n\n" .. info.long_description.body + + hypertext = hypertext:gsub(" [Project founder]", + "sfan5 ", + "ShadowNinja ", + "Nathanaëlle Courant (Nore/Ekdohibs) ", + "Loic Blot (nerzhul/nrz) ", + "Andrew Ward (rubenwardy) ", + "Krock/SmallJoker ", + "Lars Hofhansl ", + "v-rob ", + "Desour/DS", + "srifqi", + "Gregor Parzefall (grorp)", + "Lars Müller (luatic)" + ], + "previous_core_developers": [ + "BlockMen", + "Maciej Kasatkin (RealBadAngel) [RIP]", + "Lisa Milne (darkrose) ", + "proller", + "Ilya Zhuravlev (xyz) ", + "PilzAdam ", + "est31 ", + "kahrl ", + "Ryan Kwolek (kwolekr) ", + "sapier", + "Zeno", + "Auke Kok (sofar) ", + "Aaron Suen ", + "paramat", + "Pierre-Yves Rollo ", + "hecks", + "Jude Melton-Houghton (TurkeyMcMac) [RIP]", + "Hugues Ross ", + "Dmitry Kostenko (x2048) " + ], + "#": "Currently only https://github.com/orgs/minetest/teams/triagers/members", + "core_team": [ + "Zughy [Issue triager]", + "wsor [Issue triager]", + "Hugo Locurcio (Calinou) [Issue triager]" + ], + "#": "For updating active/previous contributors, see the script in ./util/gather_git_credits.py", + "contributors": [ + "cx384", + "numzero", + "AFCMS", + "sfence", + "Wuzzy", + "ROllerozxa", + "JosiahWI", + "OgelGames", + "David Heidelberg", + "1F616EMO", + "HybridDog", + "Bradley Pierce (Thresher)", + "savilli", + "Stvk imension", + "y5nw", + "chmodsayshello", + "jordan4ibanez", + "superfloh247" + ], + "previous_contributors": [ + "Nils Dagsson Moskopp (erlehmann) [Minetest logo]", + "red-001 ", + "Giuseppe Bilotta", + "HybridDog", + "ClobberXD", + "Dániel Juhász (juhdanad) ", + "MirceaKitsune ", + "Jean-Patrick Guerrero (kilbith)", + "MoNTE48", + "Constantin Wenger (SpeedProg)", + "Ciaran Gultnieks (CiaranG)", + "Paul Ouellette (pauloue)", + "stujones11", + "Rogier ", + "Gregory Currie (gregorycu)", + "JacobF", + "Jeija " + ] +} diff --git a/builtin/mainmenu/dlg_config_world.lua b/builtin/mainmenu/dlg_config_world.lua index e8f49b230..a8c5221de 100644 --- a/builtin/mainmenu/dlg_config_world.lua +++ b/builtin/mainmenu/dlg_config_world.lua @@ -126,7 +126,7 @@ local function get_formspec(data) local retval = "size[11.5,7.5,true]" .. "label[0.5,0;" .. fgettext("World:") .. "]" .. - "label[1.75,0;" .. data.worldspec.name .. "]" + "label[1.75,0;" .. core.formspec_escape(data.worldspec.name) .. "]" if mod.is_modpack or mod.type == "game" then local info = core.formspec_escape( diff --git a/builtin/mainmenu/init.lua b/builtin/mainmenu/init.lua index 41885e298..dd35334c2 100644 --- a/builtin/mainmenu/init.lua +++ b/builtin/mainmenu/init.lua @@ -23,6 +23,13 @@ mt_color_dark_green = "#25C191" mt_color_orange = "#FF8800" mt_color_red = "#FF3300" +MAIN_TAB_W = 15.5 +MAIN_TAB_H = 7.1 +TABHEADER_H = 0.85 +GAMEBAR_H = 1.25 +GAMEBAR_OFFSET_DESKTOP = 0.375 +GAMEBAR_OFFSET_TOUCH = 0.15 + local menupath = core.get_mainmenu_path() local basepath = core.get_builtin_path() defaulttexturedir = core.get_texturepath_share() .. DIR_DELIM .. "base" .. @@ -89,7 +96,7 @@ local function init_globals() mm_game_theme.set_engine() -- This is just a fallback. -- Create main tabview - local tv_main = tabview_create("maintab", {x = 15.5, y = 7.1}, {x = 0, y = 0}) + local tv_main = tabview_create("maintab", {x = MAIN_TAB_W, y = MAIN_TAB_H}, {x = 0, y = 0}) tv_main:set_autosave_tab(true) tv_main:add(tabs.local_game) diff --git a/builtin/mainmenu/settings/components.lua b/builtin/mainmenu/settings/components.lua index bfe64285c..79253558b 100644 --- a/builtin/mainmenu/settings/components.lua +++ b/builtin/mainmenu/settings/components.lua @@ -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. diff --git a/builtin/mainmenu/settings/dlg_settings.lua b/builtin/mainmenu/settings/dlg_settings.lua index 73a72769b..d668fba50 100644 --- a/builtin/mainmenu/settings/dlg_settings.lua +++ b/builtin/mainmenu/settings/dlg_settings.lua @@ -110,7 +110,7 @@ local function load() local change_keys = { query_text = "Controls", requires = { - keyboard_mouse = true, + touch_controls = false, }, get_formspec = function(self, avail_w) local btn_w = math.min(avail_w, 3) @@ -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,11 +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", - touchscreen_gui = core.settings:get_bool("enable_touch"), - keyboard_mouse = not core.settings:get_bool("enable_touch"), + -- 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", @@ -435,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 @@ -457,13 +468,13 @@ local function get_formspec(dialogdata) local extra_h = 1 -- not included in tabsize.height local tabsize = { - width = core.settings:get_bool("enable_touch") and 16.5 or 15.5, - height = core.settings:get_bool("enable_touch") and (10 - extra_h) or 12, + width = core.settings:get_bool("touch_gui") and 16.5 or 15.5, + height = core.settings:get_bool("touch_gui") and (10 - extra_h) or 12, } - local scrollbar_w = core.settings:get_bool("enable_touch") and 0.6 or 0.4 + local scrollbar_w = core.settings:get_bool("touch_gui") and 0.6 or 0.4 - local left_pane_width = core.settings:get_bool("enable_touch") and 4.5 or 4.25 + local left_pane_width = core.settings:get_bool("touch_gui") and 4.5 or 4.25 local left_pane_padding = 0.25 local search_width = left_pane_width + scrollbar_w - (0.75 * 2) @@ -477,7 +488,7 @@ local function get_formspec(dialogdata) local fs = { "formspec_version[6]", "size[", tostring(tabsize.width), ",", tostring(tabsize.height + extra_h), "]", - core.settings:get_bool("enable_touch") and "padding[0.01,0.01]" or "", + core.settings:get_bool("touch_gui") and "padding[0.01,0.01]" or "", "bgcolor[#0000]", -- HACK: this is needed to allow resubmitting the same formspec @@ -509,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]", } @@ -540,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 @@ -552,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 @@ -608,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 @@ -626,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 @@ -650,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 - - -- enable_touch 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.enable_touch ~= nil then - local value = core.is_yes(fields.enable_touch) - core.settings:set_bool("enable_touch", value) - write_settings_early() - end - - if fields.show_advanced ~= nil or fields.enable_touch ~= 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 @@ -703,20 +704,26 @@ local function buttonhandler(this, fields) end end - for i, comp in ipairs(dialogdata.components) do - if comp.on_submit and comp:on_submit(fields, this) then - write_settings_early() - + local function after_setting_change(comp) + write_settings_early() + if comp.setting.name == "touch_controls" then + -- Changing the "touch_controls" setting may result in a different + -- page list. + regenerate_page_list(dialogdata) + else -- Clear components so they regenerate dialogdata.components = nil + end + end + + for i, comp in ipairs(dialogdata.components) do + if comp.on_submit and comp:on_submit(fields, this) then + after_setting_change(comp) return true end if comp.setting and fields["reset_" .. i] then core.settings:remove(comp.setting.name) - write_settings_early() - - -- Clear components so they regenerate - dialogdata.components = nil + after_setting_change(comp) return true end end diff --git a/builtin/mainmenu/tab_about.lua b/builtin/mainmenu/tab_about.lua index d798b5b09..ab3edbddc 100644 --- a/builtin/mainmenu/tab_about.lua +++ b/builtin/mainmenu/tab_about.lua @@ -15,111 +15,23 @@ --with this program; if not, write to the Free Software Foundation, Inc., --51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. --- https://github.com/orgs/minetest/teams/engine/members - -local core_developers = { - "Perttu Ahola (celeron55) [Project founder]", - "sfan5 ", - "ShadowNinja ", - "Nathanaëlle Courant (Nore/Ekdohibs) ", - "Loic Blot (nerzhul/nrz) ", - "Andrew Ward (rubenwardy) ", - "Krock/SmallJoker ", - "Lars Hofhansl ", - "v-rob ", - "Desour/DS", - "srifqi", - "Gregor Parzefall (grorp)", - "Lars Müller (luatic)", -} - --- currently only https://github.com/orgs/minetest/teams/triagers/members - -local core_team = { - "Zughy [Issue triager]", - "wsor [Issue triager]", - "Hugo Locurcio (Calinou) [Issue triager]", -} - --- For updating active/previous contributors, see the script in ./util/gather_git_credits.py - -local active_contributors = { - "cx384", - "numzero", - "AFCMS", - "sfence", - "Wuzzy", - "ROllerozxa", - "JosiahWI", - "OgelGames", - "David Heidelberg", - "1F616EMO", - "HybridDog", - "Bradley Pierce (Thresher)", - "savilli", - "Stvk imension", - "y5nw", - "chmodsayshello", - "jordan4ibanez", - "superfloh247", -} - -local previous_core_developers = { - "BlockMen", - "Maciej Kasatkin (RealBadAngel) [RIP]", - "Lisa Milne (darkrose) ", - "proller", - "Ilya Zhuravlev (xyz) ", - "PilzAdam ", - "est31 ", - "kahrl ", - "Ryan Kwolek (kwolekr) ", - "sapier", - "Zeno", - "Auke Kok (sofar) ", - "Aaron Suen ", - "paramat", - "Pierre-Yves Rollo ", - "hecks", - "Jude Melton-Houghton (TurkeyMcMac) [RIP]", - "Hugues Ross ", - "Dmitry Kostenko (x2048) ", -} - -local previous_contributors = { - "Nils Dagsson Moskopp (erlehmann) [Minetest logo]", - "red-001 ", - "Giuseppe Bilotta", - "HybridDog", - "ClobberXD", - "Dániel Juhász (juhdanad) ", - "MirceaKitsune ", - "Jean-Patrick Guerrero (kilbith)", - "MoNTE48", - "Constantin Wenger (SpeedProg)", - "Ciaran Gultnieks (CiaranG)", - "Paul Ouellette (pauloue)", - "stujones11", - "Rogier ", - "Gregory Currie (gregorycu)", - "JacobF", - "Jeija ", -} local function prepare_credits(dest, source) local string = table.concat(source, "\n") .. "\n" - local hypertext_escapes = { - ["\\"] = "\\\\", - ["<"] = "\\<", - [">"] = "\\>", - } - string = string:gsub("[\\<>]", hypertext_escapes) + string = core.hypertext_escape(string) string = string:gsub("%[.-%]", "%1") table.insert(dest, string) end +local function get_credits() + local f = assert(io.open(core.get_mainmenu_path() .. "/credits.json")) + local json = core.parse_json(f:read("*all")) + f:close() + return json +end + return { name = "about", caption = fgettext("About"), @@ -133,30 +45,32 @@ return { "", } + local credits = get_credits() + table.insert_all(hypertext, { "", fgettext_ne("Core Developers"), "\n", }) - prepare_credits(hypertext, core_developers) + prepare_credits(hypertext, credits.core_developers) table.insert_all(hypertext, { "\n", "", fgettext_ne("Core Team"), "\n", }) - prepare_credits(hypertext, core_team) + prepare_credits(hypertext, credits.core_team) table.insert_all(hypertext, { "\n", "", fgettext_ne("Active Contributors"), "\n", }) - prepare_credits(hypertext, active_contributors) + prepare_credits(hypertext, credits.contributors) table.insert_all(hypertext, { "\n", "", fgettext_ne("Previous Core Developers"), "\n", }) - prepare_credits(hypertext, previous_core_developers) + prepare_credits(hypertext, credits.previous_core_developers) table.insert_all(hypertext, { "\n", "", fgettext_ne("Previous Contributors"), "\n", }) - prepare_credits(hypertext, previous_contributors) + prepare_credits(hypertext, credits.previous_contributors) hypertext = table.concat(hypertext):sub(1, -2) diff --git a/builtin/mainmenu/tab_content.lua b/builtin/mainmenu/tab_content.lua index b38f12884..9cfb96d54 100644 --- a/builtin/mainmenu/tab_content.lua +++ b/builtin/mainmenu/tab_content.lua @@ -118,7 +118,7 @@ local function get_formspec(tabview, name, tabdata) local title_and_name if selected_pkg.type == "game" then - title_and_name = selected_pkg.name + title_and_name = selected_pkg.title or selected_pkg.name else title_and_name = (selected_pkg.title or selected_pkg.name) .. "\n" .. core.colorize("#BFBFBF", selected_pkg.name) diff --git a/builtin/mainmenu/tab_local.lua b/builtin/mainmenu/tab_local.lua index 1ed08d825..8d807cc79 100644 --- a/builtin/mainmenu/tab_local.lua +++ b/builtin/mainmenu/tab_local.lua @@ -92,10 +92,16 @@ function singleplayer_refresh_gamebar() end end + local TOUCH_GUI = core.settings:get_bool("touch_gui") + + local gamebar_pos_y = MAIN_TAB_H + + TABHEADER_H -- tabheader included in formspec size + + (TOUCH_GUI and GAMEBAR_OFFSET_TOUCH or GAMEBAR_OFFSET_DESKTOP) + local btnbar = buttonbar_create( "game_button_bar", - core.settings:get_bool("enable_touch") and {x = 0, y = 7.25} or {x = 0, y = 7.475}, - {x = 15.5, y = 1.25}, + {x = 0, y = gamebar_pos_y}, + {x = MAIN_TAB_W, y = GAMEBAR_H}, "#000000", game_buttonbar_button_handler) diff --git a/builtin/profiler/instrumentation.lua b/builtin/profiler/instrumentation.lua index 0ffd9e6a9..c4feda7b4 100644 --- a/builtin/profiler/instrumentation.lua +++ b/builtin/profiler/instrumentation.lua @@ -217,8 +217,9 @@ local function init() -- Wrap register_lbm() to automatically instrument lbms. local orig_register_lbm = core.register_lbm core.register_lbm = function(spec) - spec.action = instrument { - func = spec.action, + local k = spec.bulk_action ~= nil and "bulk_action" or "action" + spec[k] = instrument { + func = spec[k], class = "LBM", label = spec.label or spec.name, } diff --git a/builtin/settingtypes.txt b/builtin/settingtypes.txt index 8828fc1df..f7225ef62 100644 --- a/builtin/settingtypes.txt +++ b/builtin/settingtypes.txt @@ -61,7 +61,7 @@ # # # This is a comment # # -# # Requires: shaders, enable_dynamic_shadows, !touchscreen_gui +# # 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,7 +72,7 @@ # * shaders_support (a video driver that supports shaders, may not be enabled) # * shaders (both enable_shaders and shaders_support) # * desktop / android -# * touchscreen_gui / keyboard_mouse +# * touchscreen / keyboard_mouse # * opengl / gles # * You can negate any requirement by prepending with ! # @@ -114,7 +114,7 @@ always_fly_fast (Always fly fast) bool true # the place button. # # Requires: keyboard_mouse -repeat_place_time (Place repetition interval) float 0.25 0.16 2.0 +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. @@ -152,42 +152,42 @@ invert_hotbar_mouse_wheel (Hotbar: Invert mouse wheel direction) bool false [*Touchscreen] -# Enables touchscreen mode, allowing you to play the game with a touchscreen. -# -# Requires: !android -enable_touch (Enable touchscreen) bool true +# Enables the touchscreen controls, allowing you to play the game with a touchscreen. +# "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: touchscreen_gui +# 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: touchscreen_gui +# 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: touchscreen_gui +# 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: touchscreen_gui +# 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: touchscreen_gui +# 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: touchscreen_gui +# Requires: touchscreen virtual_joystick_triggers_aux1 (Virtual joystick triggers Aux1 button) bool false # The gesture for punching players/entities. @@ -200,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: touchscreen_gui +# Requires: touchscreen touch_punch_gesture (Punch gesture) enum short_tap short_tap,long_tap @@ -265,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. @@ -406,6 +381,11 @@ enable_clouds (Clouds) bool true # Requires: enable_clouds enable_3d_clouds (3D clouds) bool true +# Use smooth cloud shading. +# +# Requires: enable_3d_clouds, enable_clouds +soft_clouds (Soft clouds) bool false + [**Filtering and Antialiasing] # Use mipmaps when scaling textures. May slightly increase performance, @@ -464,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] @@ -623,47 +619,34 @@ exposure_compensation (Exposure compensation) float 0.0 -1.0 1.0 # 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 enable_volumetric_lighting (Volumetric lighting) bool false +[**Other Effects] + +# Simulate translucency when looking at foliage in the sunlight. +# +# Requires: shaders, enable_dynamic_shadows +enable_translucent_foliage (Translucent foliage) bool false + +# Apply specular shading to nodes. +# +# 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. @@ -687,6 +670,10 @@ language (Language) enum ,be,bg,ca,cs,da,de,el,en,eo,es,et,eu,fi,fr,gd,gl,hu,i [**GUI] +# When enabled, the GUI is optimized to be more usable on touchscreens. +# Whether this is enabled by default depends on your hardware form-factor. +touch_gui (Optimize GUI for touchscreens) bool false + # Scale GUI by a user specified value. # Use a nearest-neighbor-anti-alias filter to scale the GUI. # This will smooth over some of the rough edges, and blend @@ -735,6 +722,12 @@ hud_scaling (HUD scaling) float 1.0 0.5 20 # Mods may still set a background. show_nametag_backgrounds (Show name tag backgrounds by default) bool true +# Whether to show the client debug info (has the same effect as hitting F5). +show_debug (Show debug info) bool false + +# Radius to use when the block bounds HUD feature is set to near blocks. +show_block_bounds_radius_near (Block bounds HUD radius) int 4 0 1000 + [**Chat] # Maximum number of recent chat messages to show @@ -891,8 +884,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. @@ -1020,7 +1018,7 @@ mapgen_limit (Map generation limit) int 31007 0 31007 # Global map generation attributes. # In Mapgen v6 the 'decorations' flag controls all decorations except trees # and jungle grass, in all other mapgens this flag controls all decorations. -mg_flags (Mapgen flags) flags caves,dungeons,light,decorations,biomes,ores caves,dungeons,light,decorations,biomes,ores,nocaves,nodungeons,nolight,nodecorations,nobiomes,noores +mg_flags (Mapgen flags) flags caves,dungeons,light,decorations,biomes,ores caves,dungeons,light,decorations,biomes,ores [*Biome API] @@ -1039,7 +1037,7 @@ mg_biome_np_humidity_blend (Humidity blend noise) noise_params_2d 0, 1.5, (8, 8, [*Mapgen V5] # Map generation attributes specific to Mapgen v5. -mgv5_spflags (Mapgen V5 specific flags) flags caverns caverns,nocaverns +mgv5_spflags (Mapgen V5 specific flags) flags caverns caverns # Controls width of tunnels, a smaller value creates wider tunnels. # Value >= 10.0 completely disables generation of tunnels and avoids the @@ -1113,7 +1111,7 @@ mgv5_np_dungeons (Dungeon noise) noise_params_3d 0.9, 0.5, (500, 500, 500), 0, 2 # When the 'snowbiomes' flag is enabled jungles are automatically enabled and # the 'jungles' flag is ignored. # The 'temples' flag disables generation of desert temples. Normal dungeons will appear instead. -mgv6_spflags (Mapgen V6 specific flags) flags jungles,biomeblend,mudflow,snowbiomes,noflat,trees,temples jungles,biomeblend,mudflow,snowbiomes,flat,trees,temples,nojungles,nobiomeblend,nomudflow,nosnowbiomes,noflat,notrees,notemples +mgv6_spflags (Mapgen V6 specific flags) flags jungles,biomeblend,mudflow,snowbiomes,noflat,trees,temples jungles,biomeblend,mudflow,snowbiomes,flat,trees,temples # Deserts occur when np_biome exceeds this value. # When the 'snowbiomes' flag is enabled, this is ignored. @@ -1169,7 +1167,7 @@ mgv6_np_apple_trees (Apple trees noise) noise_params_2d 0, 1, (100, 100, 100), 3 # 'ridges': Rivers. # 'floatlands': Floating land masses in the atmosphere. # 'caverns': Giant caves deep underground. -mgv7_spflags (Mapgen V7 specific flags) flags mountains,ridges,nofloatlands,caverns mountains,ridges,floatlands,caverns,nomountains,noridges,nofloatlands,nocaverns +mgv7_spflags (Mapgen V7 specific flags) flags mountains,ridges,nofloatlands,caverns mountains,ridges,floatlands,caverns # Y of mountain density gradient zero level. Used to shift mountains vertically. mgv7_mount_zero_level (Mountain zero level) int 0 -31000 31000 @@ -1303,7 +1301,7 @@ mgv7_np_dungeons (Dungeon noise) noise_params_3d 0.9, 0.5, (500, 500, 500), 0, 2 [*Mapgen Carpathian] # Map generation attributes specific to Mapgen Carpathian. -mgcarpathian_spflags (Mapgen Carpathian specific flags) flags caverns,norivers caverns,rivers,nocaverns,norivers +mgcarpathian_spflags (Mapgen Carpathian specific flags) flags caverns,norivers caverns,rivers # Defines the base ground level. mgcarpathian_base_level (Base ground level) float 12.0 @@ -1412,7 +1410,7 @@ mgcarpathian_np_dungeons (Dungeon noise) noise_params_3d 0.9, 0.5, (500, 500, 50 # Map generation attributes specific to Mapgen Flat. # Occasional lakes and hills can be added to the flat world. -mgflat_spflags (Mapgen Flat specific flags) flags nolakes,nohills,nocaverns lakes,hills,caverns,nolakes,nohills,nocaverns +mgflat_spflags (Mapgen Flat specific flags) flags nolakes,nohills,nocaverns lakes,hills,caverns # Y of flat ground. mgflat_ground_level (Ground level) int 8 -31000 31000 @@ -1496,7 +1494,7 @@ mgflat_np_dungeons (Dungeon noise) noise_params_3d 0.9, 0.5, (500, 500, 500), 0, # Map generation attributes specific to Mapgen Fractal. # 'terrain' enables the generation of non-fractal terrain: # ocean, islands and underground. -mgfractal_spflags (Mapgen Fractal specific flags) flags terrain terrain,noterrain +mgfractal_spflags (Mapgen Fractal specific flags) flags terrain terrain # Controls width of tunnels, a smaller value creates wider tunnels. # Value >= 10.0 completely disables generation of tunnels and avoids the @@ -1630,7 +1628,7 @@ mgfractal_np_dungeons (Dungeon noise) noise_params_3d 0.9, 0.5, (500, 500, 500), # 'vary_river_depth': If enabled, low humidity and high heat causes rivers # to become shallower and occasionally dry. # 'altitude_dry': Reduces humidity with altitude. -mgvalleys_spflags (Mapgen Valleys specific flags) flags altitude_chill,humid_rivers,vary_river_depth,altitude_dry altitude_chill,humid_rivers,vary_river_depth,altitude_dry,noaltitude_chill,nohumid_rivers,novary_river_depth,noaltitude_dry +mgvalleys_spflags (Mapgen Valleys specific flags) flags altitude_chill,humid_rivers,vary_river_depth,altitude_dry altitude_chill,humid_rivers,vary_river_depth,altitude_dry # The vertical distance over which heat drops by 20 if 'altitude_chill' is # enabled. Also, the vertical distance over which humidity drops by 10 if @@ -1837,6 +1835,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 @@ -1845,11 +1848,11 @@ shader_path (Shader path) path # The rendering back-end. # Note: A restart is required after changing this! # OpenGL is the default for desktop, and OGLES2 for Android. -# Shaders are supported by everything but OGLES1. -video_driver (Video driver) enum ,opengl,opengl3,ogles1,ogles2 +video_driver (Video driver) enum ,opengl,opengl3,ogles2 -# Distance in nodes at which transparency depth sorting is enabled -# Use this to limit the performance impact of transparency depth sorting +# Distance in nodes at which transparency depth sorting is enabled. +# Use this to limit the performance impact of transparency depth sorting. +# Set to 0 to disable it entirely. transparency_sorting_distance (Transparency Sorting Distance) int 16 0 128 # Radius of cloud area stated in number of 64 node cloud squares. @@ -1860,6 +1863,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 @@ -1911,6 +1915,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. @@ -2009,9 +2021,6 @@ client_unload_unused_data_timeout (Mapblock unload timeout) float 600.0 0.0 # Set to -1 for unlimited amount. client_mapblock_limit (Mapblock limit) int 7500 -1 2147483647 -# Whether to show the client debug info (has the same effect as hitting F5). -show_debug (Show debug info) bool false - # Maximum number of blocks that are simultaneously sent per client. # The maximum total count is calculated dynamically: # max_total = ceil((#clients + max_users) * per_client / 4) @@ -2021,9 +2030,8 @@ max_simultaneous_block_sends_per_client (Maximum simultaneous block sends per cl # This determines how long they are slowed down after placing or removing a node. full_block_send_enable_min_time_from_building (Delay in sending blocks after building) float 2.0 0.0 -# Maximum number of packets sent per send step, if you have a slow connection -# try reducing it, but don't reduce it to a number below double of targeted -# client number. +# Maximum number of packets sent per send step in the low-level networking code. +# You generally don't need to change this, however busy servers may benefit from a higher number. max_packets_per_iteration (Max. packets per iteration) int 1024 1 65535 # Compression level to use when sending mapblocks to the client. diff --git a/client/shaders/Irrlicht b/client/shaders/Irrlicht deleted file mode 120000 index 9349d3073..000000000 --- a/client/shaders/Irrlicht +++ /dev/null @@ -1 +0,0 @@ -../../irr/media/Shaders \ No newline at end of file diff --git a/irr/media/Shaders/OneTextureBlend.fsh b/client/shaders/Irrlicht/OneTextureBlend.fsh similarity index 100% rename from irr/media/Shaders/OneTextureBlend.fsh rename to client/shaders/Irrlicht/OneTextureBlend.fsh diff --git a/irr/media/Shaders/Renderer2D.fsh b/client/shaders/Irrlicht/Renderer2D.fsh similarity index 100% rename from irr/media/Shaders/Renderer2D.fsh rename to client/shaders/Irrlicht/Renderer2D.fsh diff --git a/irr/media/Shaders/Renderer2D.vsh b/client/shaders/Irrlicht/Renderer2D.vsh similarity index 100% rename from irr/media/Shaders/Renderer2D.vsh rename to client/shaders/Irrlicht/Renderer2D.vsh diff --git a/irr/media/Shaders/Renderer2D_noTex.fsh b/client/shaders/Irrlicht/Renderer2D_noTex.fsh similarity index 100% rename from irr/media/Shaders/Renderer2D_noTex.fsh rename to client/shaders/Irrlicht/Renderer2D_noTex.fsh diff --git a/irr/media/Shaders/Solid.fsh b/client/shaders/Irrlicht/Solid.fsh similarity index 100% rename from irr/media/Shaders/Solid.fsh rename to client/shaders/Irrlicht/Solid.fsh diff --git a/irr/media/Shaders/Solid.vsh b/client/shaders/Irrlicht/Solid.vsh similarity index 96% rename from irr/media/Shaders/Solid.vsh rename to client/shaders/Irrlicht/Solid.vsh index 7379e5bb4..fd7467f5c 100644 --- a/irr/media/Shaders/Solid.vsh +++ b/client/shaders/Irrlicht/Solid.vsh @@ -11,7 +11,6 @@ attribute vec2 inTexCoord0; uniform mat4 uWVPMatrix; uniform mat4 uWVMatrix; -uniform mat4 uNMatrix; uniform mat4 uTMatrix0; uniform float uThickness; diff --git a/irr/media/Shaders/TransparentAlphaChannel.fsh b/client/shaders/Irrlicht/TransparentAlphaChannel.fsh similarity index 100% rename from irr/media/Shaders/TransparentAlphaChannel.fsh rename to client/shaders/Irrlicht/TransparentAlphaChannel.fsh diff --git a/irr/media/Shaders/TransparentAlphaChannelRef.fsh b/client/shaders/Irrlicht/TransparentAlphaChannelRef.fsh similarity index 100% rename from irr/media/Shaders/TransparentAlphaChannelRef.fsh rename to client/shaders/Irrlicht/TransparentAlphaChannelRef.fsh diff --git a/irr/media/Shaders/TransparentVertexAlpha.fsh b/client/shaders/Irrlicht/TransparentVertexAlpha.fsh similarity index 100% rename from irr/media/Shaders/TransparentVertexAlpha.fsh rename to client/shaders/Irrlicht/TransparentVertexAlpha.fsh diff --git a/client/shaders/cloud_shader/opengl_vertex.glsl b/client/shaders/cloud_shader/opengl_vertex.glsl index 3f2e7d9b3..92f5de64b 100644 --- a/client/shaders/cloud_shader/opengl_vertex.glsl +++ b/client/shaders/cloud_shader/opengl_vertex.glsl @@ -1,4 +1,4 @@ -uniform lowp vec4 emissiveColor; +uniform lowp vec4 materialColor; varying lowp vec4 varColor; @@ -8,13 +8,9 @@ void main(void) { gl_Position = mWorldViewProj * inVertexPosition; -#ifdef GL_ES - vec4 color = inVertexColor.bgra; -#else vec4 color = inVertexColor; -#endif - color *= emissiveColor; + color *= materialColor; varColor = color; eyeVec = -(mWorldView * inVertexPosition).xyz; diff --git a/client/shaders/default_shader/opengl_vertex.glsl b/client/shaders/default_shader/opengl_vertex.glsl index a908ac953..d95a3c2d3 100644 --- a/client/shaders/default_shader/opengl_vertex.glsl +++ b/client/shaders/default_shader/opengl_vertex.glsl @@ -3,9 +3,5 @@ varying lowp vec4 varColor; void main(void) { gl_Position = mWorldViewProj * inVertexPosition; -#ifdef GL_ES - varColor = inVertexColor.bgra; -#else varColor = inVertexColor; -#endif } diff --git a/client/shaders/fxaa/opengl_fragment.glsl b/client/shaders/fxaa/opengl_fragment.glsl index 130e689ea..f70064b6d 100644 --- a/client/shaders/fxaa/opengl_fragment.glsl +++ b/client/shaders/fxaa/opengl_fragment.glsl @@ -58,11 +58,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #define FXAA_SPAN_MAX 8.0 #endif -//optimized version for mobile, where dependent +//optimized version for mobile, where dependent //texture reads can be a bottleneck vec4 fxaa(sampler2D tex, vec2 fragCoord, vec2 inverseVP, - vec2 v_rgbNW, vec2 v_rgbNE, - vec2 v_rgbSW, vec2 v_rgbSE, + vec2 v_rgbNW, vec2 v_rgbNE, + vec2 v_rgbSW, vec2 v_rgbSE, vec2 v_rgbM) { vec4 color; vec3 rgbNW = texture2D(tex, v_rgbNW).xyz; @@ -111,6 +111,6 @@ void main(void) { vec2 uv = varTexCoord.st; - gl_FragColor = fxaa(rendered, uv, texelSize0, + gl_FragColor = fxaa(rendered, uv, texelSize0, sampleNW, sampleNE, sampleSW, sampleSE, uv); } diff --git a/client/shaders/fxaa/opengl_vertex.glsl b/client/shaders/fxaa/opengl_vertex.glsl index 26913c28e..68bb36d1c 100644 --- a/client/shaders/fxaa/opengl_vertex.glsl +++ b/client/shaders/fxaa/opengl_vertex.glsl @@ -12,7 +12,7 @@ varying vec2 sampleSW; varying vec2 sampleSE; /* -Based on +Based on https://github.com/mattdesl/glsl-fxaa/ Portions Copyright (c) 2011 by Armin Ronacher. */ diff --git a/client/shaders/minimap_shader/opengl_vertex.glsl b/client/shaders/minimap_shader/opengl_vertex.glsl index b23d27181..1a9491805 100644 --- a/client/shaders/minimap_shader/opengl_vertex.glsl +++ b/client/shaders/minimap_shader/opengl_vertex.glsl @@ -7,9 +7,5 @@ void main(void) { varTexCoord = inTexCoord0.st; gl_Position = mWorldViewProj * inVertexPosition; -#ifdef GL_ES - varColor = inVertexColor.bgra; -#else varColor = inVertexColor; -#endif } diff --git a/client/shaders/nodes_shader/opengl_fragment.glsl b/client/shaders/nodes_shader/opengl_fragment.glsl index 46977b147..c560a8505 100644 --- a/client/shaders/nodes_shader/opengl_fragment.glsl +++ b/client/shaders/nodes_shader/opengl_fragment.glsl @@ -1,3 +1,7 @@ +#if (MATERIAL_TYPE == TILE_MATERIAL_WAVING_LIQUID_TRANSPARENT || MATERIAL_TYPE == TILE_MATERIAL_WAVING_LIQUID_OPAQUE || MATERIAL_TYPE == TILE_MATERIAL_WAVING_LIQUID_BASIC || MATERIAL_TYPE == TILE_MATERIAL_LIQUID_TRANSPARENT) +#define MATERIAL_WAVING_LIQUID 1 +#endif + uniform sampler2D baseTexture; uniform vec3 dayLight; @@ -7,6 +11,7 @@ uniform float fogShadingParameter; // The cameraOffset is the current center of the visible world. uniform highp vec3 cameraOffset; +uniform vec3 cameraPosition; uniform float animationTimer; #ifdef ENABLE_DYNAMIC_SHADOWS // shadow texture @@ -20,6 +25,7 @@ uniform float animationTimer; uniform vec4 CameraPos; uniform float xyPerspectiveBias0; uniform float xyPerspectiveBias1; + uniform vec3 shadow_tint; varying float adj_shadow_strength; varying float cosLight; @@ -47,6 +53,49 @@ varying highp vec3 eyeVec; varying float nightRatio; #ifdef ENABLE_DYNAMIC_SHADOWS +#if (defined(MATERIAL_WAVING_LIQUID) && defined(ENABLE_WATER_REFLECTIONS) && ENABLE_WAVING_WATER) +vec4 perm(vec4 x) +{ + return mod(((x * 34.0) + 1.0) * x, 289.0); +} + +// Corresponding gradient of snoise +vec3 gnoise(vec3 p){ + vec3 a = floor(p); + vec3 d = p - a; + vec3 dd = 6.0 * d * (1.0 - d); + d = d * d * (3.0 - 2.0 * d); + + vec4 b = a.xxyy + vec4(0.0, 1.0, 0.0, 1.0); + vec4 k1 = perm(b.xyxy); + vec4 k2 = perm(k1.xyxy + b.zzww); + + vec4 c = k2 + a.zzzz; + vec4 k3 = perm(c); + vec4 k4 = perm(c + 1.0); + + vec4 o1 = fract(k3 * (1.0 / 41.0)); + vec4 o2 = fract(k4 * (1.0 / 41.0)); + + vec4 o3 = o2 * d.z + o1 * (1.0 - d.z); + vec2 o4 = o3.yw * d.x + o3.xz * (1.0 - d.x); + + vec4 dz1 = (o2 - o1) * dd.z; + vec2 dz2 = dz1.yw * d.x + dz1.xz * (1.0 - d.x); + + vec2 dx = (o3.yw - o3.xz) * dd.x; + + return vec3( + dx.y * d.y + dx.x * (1. - d.y), + (o4.y - o4.x) * dd.y, + dz2.y * d.y + dz2.x * (1. - d.y) + ); +} + +vec2 wave_noise(vec3 p, float off) { + return (gnoise(p + vec3(0.0, 0.0, off)) * 0.4 + gnoise(2.0 * p + vec3(0.0, off, off)) * 0.2 + gnoise(3.0 * p + vec3(0.0, off, off)) * 0.225 + gnoise(4.0 * p + vec3(-off, off, 0.0)) * 0.2).xz; +} +#endif // assuming near is always 1.0 float getLinearDepth() @@ -66,6 +115,14 @@ float mtsmoothstep(in float edge0, in float edge1, in float x) return t * t * (3.0 - 2.0 * t); } +float shadowCutoff(float x) { + #if defined(ENABLE_TRANSLUCENT_FOLIAGE) && MATERIAL_TYPE == TILE_MATERIAL_WAVING_LEAVES + return mtsmoothstep(0.0, 0.002, x); + #else + return step(0.0, x); + #endif +} + #ifdef COLORED_SHADOWS // c_precision of 128 fits within 7 base-10 digits @@ -92,10 +149,10 @@ vec4 getHardShadowColor(sampler2D shadowsampler, vec2 smTexCoord, float realDist { vec4 texDepth = texture2D(shadowsampler, smTexCoord.xy).rgba; - float visibility = step(0.0, realDistance - texDepth.r); + float visibility = shadowCutoff(realDistance - texDepth.r); vec4 result = vec4(visibility, vec3(0.0,0.0,0.0));//unpackColor(texDepth.g)); if (visibility < 0.1) { - visibility = step(0.0, realDistance - texDepth.b); + visibility = shadowCutoff(realDistance - texDepth.b); result = vec4(visibility, unpackColor(texDepth.a)); } return result; @@ -106,7 +163,7 @@ vec4 getHardShadowColor(sampler2D shadowsampler, vec2 smTexCoord, float realDist float getHardShadow(sampler2D shadowsampler, vec2 smTexCoord, float realDistance) { float texDepth = texture2D(shadowsampler, smTexCoord.xy).r; - float visibility = step(0.0, realDistance - texDepth); + float visibility = shadowCutoff(realDistance - texDepth); return visibility; } @@ -378,6 +435,9 @@ void main(void) vec4 col = vec4(color.rgb * varColor.rgb, 1.0); #ifdef ENABLE_DYNAMIC_SHADOWS + // Fragment normal, can differ from vNormal which is derived from vertex normals. + vec3 fNormal = vNormal; + if (f_shadow_strength > 0.0) { float shadow_int = 0.0; vec3 shadow_color = vec3(0.0, 0.0, 0.0); @@ -414,12 +474,19 @@ void main(void) // Power ratio was measured on torches in MTG (brightness = 14). float adjusted_night_ratio = pow(max(0.0, nightRatio), 0.6); + float shadow_uncorrected = shadow_int; + // Apply self-shadowing when light falls at a narrow angle to the surface // Cosine of the cut-off angle. const float self_shadow_cutoff_cosine = 0.035; if (f_normal_length != 0 && cosLight < self_shadow_cutoff_cosine) { shadow_int = max(shadow_int, 1 - clamp(cosLight, 0.0, self_shadow_cutoff_cosine)/self_shadow_cutoff_cosine); shadow_color = mix(vec3(0.0), shadow_color, min(cosLight, self_shadow_cutoff_cosine)/self_shadow_cutoff_cosine); + +#if (MATERIAL_TYPE == TILE_MATERIAL_WAVING_LEAVES || MATERIAL_TYPE == TILE_MATERIAL_WAVING_PLANTS) + // Prevents foliage from becoming insanely bright outside the shadow map. + shadow_uncorrected = mix(shadow_int, shadow_uncorrected, clamp(distance_rate * 4.0 - 3.0, 0.0, 1.0)); +#endif } shadow_int *= f_adj_shadow_strength; @@ -428,8 +495,60 @@ void main(void) col.rgb = adjusted_night_ratio * col.rgb + // artificial light (1.0 - adjusted_night_ratio) * ( // natural light - col.rgb * (1.0 - shadow_int * (1.0 - shadow_color)) + // filtered texture color + col.rgb * (1.0 - shadow_int * (1.0 - shadow_color) * (1.0 - shadow_tint)) + // filtered texture color dayLight * shadow_color * shadow_int); // reflected filtered sunlight/moonlight + + + vec3 reflect_ray = -normalize(v_LightDirection - fNormal * dot(v_LightDirection, fNormal) * 2.0); + + vec3 viewVec = normalize(worldPosition + cameraOffset - cameraPosition); + + // Water reflections +#if (defined(MATERIAL_WAVING_LIQUID) && defined(ENABLE_WATER_REFLECTIONS) && ENABLE_WAVING_WATER) + vec3 wavePos = worldPosition * vec3(2.0, 0.0, 2.0); + float off = animationTimer * WATER_WAVE_SPEED * 10.0; + wavePos.x /= WATER_WAVE_LENGTH * 3.0; + wavePos.z /= WATER_WAVE_LENGTH * 2.0; + + // This is an analogous method to the bumpmap, except we get the gradient information directly from gnoise. + vec2 gradient = wave_noise(wavePos, off); + fNormal = normalize(normalize(fNormal) + vec3(gradient.x, 0., gradient.y) * WATER_WAVE_HEIGHT * abs(fNormal.y) * 0.25); + reflect_ray = -normalize(v_LightDirection - fNormal * dot(v_LightDirection, fNormal) * 2.0); + float fresnel_factor = dot(fNormal, viewVec); + + float brightness_factor = 1.0 - adjusted_night_ratio; + + // A little trig hack. We go from the dot product of viewVec and normal to the dot product of viewVec and tangent to apply a fresnel effect. + fresnel_factor = clamp(pow(1.0 - fresnel_factor * fresnel_factor, 8.0), 0.0, 1.0) * 0.8 + 0.2; + col.rgb *= 0.5; + vec3 reflection_color = mix(vec3(max(fogColor.r, max(fogColor.g, fogColor.b))), fogColor.rgb, f_shadow_strength); + + // Sky reflection + col.rgb += reflection_color * pow(fresnel_factor, 2.0) * 0.5 * brightness_factor; + vec3 water_reflect_color = 12.0 * dayLight * fresnel_factor * mtsmoothstep(0.85, 0.9, pow(clamp(dot(reflect_ray, viewVec), 0.0, 1.0), 32.0)) * max(1.0 - shadow_uncorrected, 0.0); + + // This line exists to prevent ridiculously bright reflection colors. + water_reflect_color /= clamp(max(water_reflect_color.r, max(water_reflect_color.g, water_reflect_color.b)) * 0.375, 1.0, 400.0); + col.rgb += water_reflect_color * f_adj_shadow_strength * brightness_factor; +#endif + +#if (defined(ENABLE_NODE_SPECULAR) && !defined(MATERIAL_WAVING_LIQUID)) + // Apply specular to blocks. + if (dot(v_LightDirection, vNormal) < 0.0) { + float intensity = 2.0 * (1.0 - (base.r * varColor.r)); + const float specular_exponent = 5.0; + const float fresnel_exponent = 4.0; + + col.rgb += + intensity * dayLight * (1.0 - nightRatio) * (1.0 - shadow_uncorrected) * f_adj_shadow_strength * + pow(max(dot(reflect_ray, viewVec), 0.0), fresnel_exponent) * pow(1.0 - abs(dot(viewVec, fNormal)), specular_exponent); + } +#endif + +#if (MATERIAL_TYPE == TILE_MATERIAL_WAVING_PLANTS || MATERIAL_TYPE == TILE_MATERIAL_WAVING_LEAVES) && defined(ENABLE_TRANSLUCENT_FOLIAGE) + // Simulate translucent foliage. + col.rgb += 4.0 * dayLight * base.rgb * normalize(base.rgb * varColor.rgb * varColor.rgb) * f_adj_shadow_strength * pow(max(-dot(v_LightDirection, viewVec), 0.0), 4.0) * max(1.0 - shadow_uncorrected, 0.0); +#endif } #endif @@ -444,7 +563,13 @@ void main(void) // Note: clarity = (1 - fogginess) float clarity = clamp(fogShadingParameter - fogShadingParameter * length(eyeVec) / fogDistance, 0.0, 1.0); - col = mix(fogColor, col, clarity); + float fogColorMax = max(max(fogColor.r, fogColor.g), fogColor.b); + // Prevent zero division. + if (fogColorMax < 0.0000001) fogColorMax = 1.0; + // For high clarity (light fog) we tint the fog color. + // For this to not make the fog color artificially dark we need to normalize using the + // fog color's brightest value. We then blend our base color with this to make the fog. + col = mix(fogColor * pow(fogColor / fogColorMax, vec4(2.0 * clarity)), col, clarity); col = vec4(col.rgb, base.a); gl_FragData[0] = col; diff --git a/client/shaders/nodes_shader/opengl_vertex.glsl b/client/shaders/nodes_shader/opengl_vertex.glsl index d96164d76..d5d6dd59e 100644 --- a/client/shaders/nodes_shader/opengl_vertex.glsl +++ b/client/shaders/nodes_shader/opengl_vertex.glsl @@ -199,15 +199,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 + @@ -256,7 +252,9 @@ void main(void) z_bias *= pFactor * pFactor / f_textureresolution / f_shadowfar; shadow_position = applyPerspectiveDistortion(m_ShadowViewProj * mWorld * (shadow_pos + vec4(normalOffsetScale * nNormal, 0.0))).xyz; +#if !defined(ENABLE_TRANSLUCENT_FOLIAGE) || MATERIAL_TYPE != TILE_MATERIAL_WAVING_LEAVES shadow_position.z -= z_bias; +#endif perspective_factor = pFactor; if (f_timeofday < 0.2) { diff --git a/client/shaders/object_shader/opengl_fragment.glsl b/client/shaders/object_shader/opengl_fragment.glsl index 2b8af3fa9..db72d7f7d 100644 --- a/client/shaders/object_shader/opengl_fragment.glsl +++ b/client/shaders/object_shader/opengl_fragment.glsl @@ -20,6 +20,7 @@ uniform float animationTimer; uniform vec4 CameraPos; uniform float xyPerspectiveBias0; uniform float xyPerspectiveBias1; + uniform vec3 shadow_tint; varying float adj_shadow_strength; varying float cosLight; @@ -432,7 +433,7 @@ void main(void) col.rgb = adjusted_night_ratio * col.rgb + // artificial light (1.0 - adjusted_night_ratio) * ( // natural light - col.rgb * (1.0 - shadow_int * (1.0 - shadow_color)) + // filtered texture color + col.rgb * (1.0 - shadow_int * (1.0 - shadow_color) * (1.0 - shadow_tint)) + // filtered texture color dayLight * shadow_color * shadow_int); // reflected filtered sunlight/moonlight } #endif @@ -448,7 +449,13 @@ void main(void) // Note: clarity = (1 - fogginess) float clarity = clamp(fogShadingParameter - fogShadingParameter * length(eyeVec) / fogDistance, 0.0, 1.0); - col = mix(fogColor, col, clarity); + float fogColorMax = max(max(fogColor.r, fogColor.g), fogColor.b); + // Prevent zero division. + if (fogColorMax < 0.0000001) fogColorMax = 1.0; + // For high clarity (light fog) we tint the fog color. + // For this to not make the fog color artificially dark we need to normalize using the + // fog color's brightest value. We then blend our base color with this to make the fog. + col = mix(fogColor * pow(fogColor / fogColorMax, vec4(2.0 * clarity)), col, clarity); col = vec4(col.rgb, base.a); gl_FragData[0] = col; diff --git a/client/shaders/object_shader/opengl_vertex.glsl b/client/shaders/object_shader/opengl_vertex.glsl index d5a434da5..4bb109f68 100644 --- a/client/shaders/object_shader/opengl_vertex.glsl +++ b/client/shaders/object_shader/opengl_vertex.glsl @@ -1,7 +1,7 @@ uniform mat4 mWorld; uniform vec3 dayLight; uniform float animationTimer; -uniform lowp vec4 emissiveColor; +uniform lowp vec4 materialColor; varying vec3 vNormal; varying vec3 vPosition; @@ -91,7 +91,7 @@ float directional_ambient(vec3 normal) void main(void) { - varTexCoord = (mTexture * inTexCoord0).st; + varTexCoord = (mTexture * vec4(inTexCoord0.xy, 1.0, 1.0)).st; gl_Position = mWorldViewProj * inVertexPosition; vPosition = gl_Position.xyz; @@ -109,13 +109,9 @@ void main(void) : directional_ambient(normalize(inVertexNormal)); #endif -#ifdef GL_ES - vec4 color = inVertexColor.bgra; -#else vec4 color = inVertexColor; -#endif - color *= emissiveColor; + color *= materialColor; // The alpha gives the ratio of sunlight in the incoming light. nightRatio = 1.0 - color.a; diff --git a/client/shaders/selection_shader/opengl_vertex.glsl b/client/shaders/selection_shader/opengl_vertex.glsl index 39dde3056..9ca87a9cf 100644 --- a/client/shaders/selection_shader/opengl_vertex.glsl +++ b/client/shaders/selection_shader/opengl_vertex.glsl @@ -6,9 +6,5 @@ void main(void) varTexCoord = inTexCoord0.st; gl_Position = mWorldViewProj * inVertexPosition; -#ifdef GL_ES - varColor = inVertexColor.bgra; -#else varColor = inVertexColor; -#endif } diff --git a/client/shaders/stars_shader/opengl_fragment.glsl b/client/shaders/stars_shader/opengl_fragment.glsl index 224032fa3..e991e4f94 100644 --- a/client/shaders/stars_shader/opengl_fragment.glsl +++ b/client/shaders/stars_shader/opengl_fragment.glsl @@ -1,6 +1,6 @@ -uniform lowp vec4 emissiveColor; +uniform lowp vec4 materialColor; void main(void) { - gl_FragColor = emissiveColor; + gl_FragColor = materialColor; } diff --git a/client/shaders/volumetric_light/opengl_fragment.glsl b/client/shaders/volumetric_light/opengl_fragment.glsl index 9ed5fa9ba..001f59465 100644 --- a/client/shaders/volumetric_light/opengl_fragment.glsl +++ b/client/shaders/volumetric_light/opengl_fragment.glsl @@ -46,7 +46,9 @@ float sampleVolumetricLight(vec2 uv, vec3 lightVec, float rawDepth) if (min(samplepos.x, samplepos.y) > 0. && max(samplepos.x, samplepos.y) < 1.) result += texture2D(depthmap, samplepos).r < 1. ? 0.0 : 1.0; } - return result / samples; + // We use the depth map to approximate the effect of depth on the light intensity. + // The exponent was chosen based on aesthetic preference. + return result / samples * pow(texture2D(depthmap, uv).r, 128.0); } vec3 getDirectLightScatteringAtGround(vec3 v_LightDirection) diff --git a/cmake/Modules/FindLua.cmake b/cmake/Modules/FindLua.cmake index be5d92d8c..a239046ac 100644 --- a/cmake/Modules/FindLua.cmake +++ b/cmake/Modules/FindLua.cmake @@ -11,7 +11,7 @@ if(ENABLE_LUAJIT) find_package(LuaJIT) if(LUAJIT_FOUND) set(USE_LUAJIT TRUE) - message (STATUS "Using LuaJIT provided by system.") + message (STATUS "Using LuaJIT") elseif(REQUIRE_LUAJIT) message(FATAL_ERROR "LuaJIT not found whereas REQUIRE_LUAJIT=\"TRUE\" is used.\n" "To continue, either install LuaJIT or do not use REQUIRE_LUAJIT=\"TRUE\".") diff --git a/doc/client_lua_api.md b/doc/client_lua_api.md index fac3f7b93..cd651f1b3 100644 --- a/doc/client_lua_api.md +++ b/doc/client_lua_api.md @@ -315,7 +315,7 @@ Call these functions only at load time! * `minetest.register_globalstep(function(dtime))` * Called every client environment step - * `dtime` is the time since last execution in seconds. + * `dtime` is the time since last execution in seconds. * `minetest.register_on_mods_loaded(function())` * Called just after mods have finished loading. * `minetest.register_on_shutdown(function())` @@ -338,8 +338,6 @@ Call these functions only at load time! is checked to see if the command exists, but after the input is parsed. * Return `true` to mark the command as handled, which means that the default handlers will be prevented. -* `minetest.register_on_death(function())` - * Called when the local player dies * `minetest.register_on_hp_modification(function(hp))` * Called when server modified player's HP * `minetest.register_on_damage_taken(function(hp))` @@ -487,8 +485,6 @@ Call these functions only at load time! * Returns `false` if the client is already disconnecting otherwise returns `true`. * `minetest.get_server_info()` * Returns [server info](#server-info). -* `minetest.send_respawn()` - * Sends a respawn request to the server. ### Storage API * `minetest.get_mod_storage()`: @@ -586,9 +582,9 @@ Call these functions only at load time! * `minetest.camera` * Reference to the camera object. See [`Camera`](#camera) class reference for methods. * `minetest.show_formspec(formname, formspec)` : returns true on success - * Shows a formspec to the player + * Shows a formspec to the player * `minetest.display_chat_message(message)` returns true on success - * Shows a chat message to the current player. + * Shows a chat message to the current player. Setting-related --------------- @@ -866,9 +862,9 @@ It can be created via `Raycast(pos1, pos2, objects, liquids)` or ----------------- ### Definitions * `minetest.get_node_def(nodename)` - * Returns [node definition](#node-definition) table of `nodename` + * Returns [node definition](#node-definition) table of `nodename` * `minetest.get_item_def(itemstring)` - * Returns item definition table of `itemstring` + * Returns item definition table of `itemstring` #### Node Definition @@ -971,10 +967,10 @@ It can be created via `Raycast(pos1, pos2, objects, liquids)` or ```lua { - address = "minetest.example.org", -- The domain name/IP address of a remote server or "" for a local server. - ip = "203.0.113.156", -- The IP address of the server. - port = 30000, -- The port the client is connected to. - protocol_version = 30 -- Will not be accurate at start up as the client might not be connected to the server yet, in that case it will be 0. + address = "minetest.example.org", -- The domain name/IP address of a remote server or "" for a local server. + ip = "203.0.113.156", -- The IP address of the server. + port = 30000, -- The port the client is connected to. + protocol_version = 30 -- Will not be accurate at start up as the client might not be connected to the server yet, in that case it will be 0. } ``` diff --git a/doc/compiling/README.md b/doc/compiling/README.md index a1ab1ebbd..bfe91950f 100644 --- a/doc/compiling/README.md +++ b/doc/compiling/README.md @@ -22,6 +22,7 @@ General options and their default values: MinSizeRel - Release build with -Os passed to compiler to make executable as small as possible PRECOMPILE_HEADERS=FALSE - Precompile some headers (experimental; requires CMake 3.16 or later) PRECOMPILED_HEADERS_PATH= - Path to a file listing all headers to precompile (default points to src/precompiled_headers.txt) + USE_SDL2=TRUE - Build with SDL2; Enables IrrlichtMt device SDL2 ENABLE_CURL=ON - Build with cURL; Enables use of online mod repo, public serverlist and remote media fetching via http ENABLE_CURSES=ON - Build with (n)curses; Enables a server side terminal (command line option: --terminal) ENABLE_GETTEXT=ON - Build with Gettext; Allows using translations @@ -39,10 +40,15 @@ General options and their default values: ENABLE_UPDATE_CHECKER=TRUE - Whether to enable update checks by default INSTALL_DEVTEST=FALSE - Whether the Development Test game should be installed alongside Minetest USE_GPROF=FALSE - Enable profiling using GProf + BUILD_WITH_TRACY=FALSE - Fetch and build with the Tracy profiler client + FETCH_TRACY_GIT_TAG=master - Git tag for fetching Tracy client. Match with your server (gui) version VERSION_EXTRA= - Text to append to version (e.g. VERSION_EXTRA=foobar -> Minetest 0.4.9-foobar) Library specific options: + SDL2_DLL - Only if building with SDL2 on Windows; path to libSDL2.dll + SDL2_INCLUDE_DIRS - Only if building with SDL2; directory where SDL.h is located + SDL2_LIBRARIES - Only if building with SDL2; path to libSDL2.a/libSDL2.so/libSDL2.lib CURL_DLL - Only if building with cURL on Windows; path to libcurl.dll CURL_INCLUDE_DIR - Only if building with cURL; directory where curl.h is located CURL_LIBRARY - Only if building with cURL; path to libcurl.a/libcurl.so/libcurl.lib diff --git a/doc/compiling/windows.md b/doc/compiling/windows.md index c63a7b319..eeaf2e4fd 100644 --- a/doc/compiling/windows.md +++ b/doc/compiling/windows.md @@ -14,7 +14,7 @@ It is highly recommended to use vcpkg as package manager. After you successfully built vcpkg you can easily install the required libraries: ```powershell -vcpkg install zlib zstd curl[winssl] openal-soft libvorbis libogg libjpeg-turbo sqlite3 freetype luajit gmp jsoncpp gettext sdl2 --triplet x64-windows +vcpkg install zlib zstd curl[winssl] openal-soft libvorbis libogg libjpeg-turbo sqlite3 freetype luajit gmp jsoncpp gettext[tools] sdl2 --triplet x64-windows ``` - `curl` is optional, but required to read the serverlist, `curl[winssl]` is required to use the content store. @@ -52,7 +52,7 @@ Use `--triplet` to specify the target triplet, e.g. `x64-windows` or `x86-window Run the following script in PowerShell: ```powershell -cmake . -G"Visual Studio 15 2017 Win64" -DCMAKE_TOOLCHAIN_FILE=D:/vcpkg/scripts/buildsystems/vcpkg.cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_GETTEXT=OFF -DENABLE_CURSES=OFF +cmake . -G"Visual Studio 16 2019" -DCMAKE_TOOLCHAIN_FILE=D:/vcpkg/scripts/buildsystems/vcpkg.cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_CURSES=OFF cmake --build . --config Release ``` Make sure that the right compiler is selected and the path to the vcpkg toolchain is correct. diff --git a/doc/developing/misc.md b/doc/developing/misc.md index 1d3d8c941..2ac843caf 100644 --- a/doc/developing/misc.md +++ b/doc/developing/misc.md @@ -1,6 +1,6 @@ # Miscellaneous -## Profiling Minetest on Linux +## Profiling Minetest on Linux with perf We will be using a tool called "perf", which you can get by installing `perf` or `linux-perf` or `linux-tools-common`. @@ -36,3 +36,54 @@ Give both files to the developer and also provide: * commit the source was built from and/or modified source code (if applicable) Hotspot will resolve symbols correctly when pointing the sysroot option at the collected libs. + + +## Profiling with Tracy + +[Tracy](https://github.com/wolfpld/tracy) is +> A real time, nanosecond resolution, remote telemetry, hybrid frame and sampling +> profiler for games and other applications. + +It allows one to annotate important functions and generate traces, where one can +see when each individual function call happened, and how long it took. + +Tracy can also record when frames, e.g. server step, start and end, and inspect +frames that took longer than usual. Minetest already contains annotations for +its frames. + +See also [Tracy's official documentation](https://github.com/wolfpld/tracy/releases/latest/download/tracy.pdf). + +### Installing + +Tracy consists of a client (Minetest) and a server (the gui). + +Install the server, e.g. using your package manager. + +### Building + +Build Minetest with `-DDBUILD_WITH_TRACY=1`, this will fetch Tracy for building +the Tracy client. And use `FETCH_TRACY_GIT_TAG` to get a version matching your +Tracy server, e.g. `-DFETCH_TRACY_GIT_TAG=v0.11.0` if it's `0.11.0`. + +To actually use Tracy, you also have to enable it with Tracy's build options: +``` +-DTRACY_ENABLE=1 -DTRACY_ONLY_LOCALHOST=1 +``` + +See Tracy's documentation for more build options. + +### Using in C++ + +Start the Tracy server and Minetest. You should see Minetest in the menu. + +To actually get useful traces, you have to annotate functions with `ZoneScoped` +macros and recompile. Please refer to Tracy's official documentation. + +### Using in Lua + +Tracy also supports Lua. +If built with Tracy, Minetest loads its API in the global `tracy` table. +See Tracy's official documentation for more information. + +Note: The whole Tracy Lua API is accessible to all mods. And we don't check if it +is or becomes insecure. Run untrusted mods at your own risk. diff --git a/doc/lua_api.md b/doc/lua_api.md index 389ea73f2..2c827d7ad 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -274,7 +274,7 @@ Accepted formats are: images: .png, .jpg, .tga, (deprecated:) .bmp sounds: .ogg vorbis - models: .x, .b3d, .obj + 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) @@ -291,6 +291,49 @@ in one of its parents, the parent's file is used. Although it is discouraged, a mod can overwrite a media file of any mod that it 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, .b3d and .gltf formats additionally support (a single) animation. + +#### glTF + +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: + +* 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 + * Backface culling is overridden + * Double-sided materials don't work +* Alternative means of supplying data + * Embedded images + * References to files via URIs + +Textures are supplied solely via the same means as for the other model file formats: +The `textures` object property, the `tiles` node definition field and +the list of textures used in the `model[]` formspec element. + +The order in which textures are to be supplied +is that in which they appear in the `textures` array in the glTF file. + +Do not rely on glTF features not being supported; they may be supported in the future. +The backwards compatibility guarantee does not extend to ignoring unsupported features. + +For example, if your model used an emissive material, +you should expect that a future version of Minetest may respect this, +and thus cause your model to render differently there. + Naming conventions ------------------ @@ -453,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. @@ -466,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 @@ -664,6 +713,8 @@ Apply a mask to the base image. The mask is applied using binary AND. +*See notes: `TEXMOD_UPSCALE`* + #### `[sheet:x:,` Retrieves a tile at position x, y (in tiles, 0-indexed) @@ -761,6 +812,8 @@ in GIMP. Overlay is the same as Hard light but with the role of the two textures swapped, see the `[hardlight` modifier description for more detail about these blend modes. +*See notes: `TEXMOD_UPSCALE`* + #### `[hardlight:` Applies a Hard light blend with the two textures, like the Hard light layer @@ -776,6 +829,8 @@ increase contrast without clipping. Hard light is the same as Overlay but with the roles of the two textures swapped, i.e. `A.png^[hardlight:B.png` is the same as `B.png^[overlay:A.png` +*See notes: `TEXMOD_UPSCALE`* + #### `[png:` Embed a base64 encoded PNG image in the texture string. @@ -794,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 ----------------- @@ -1357,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 @@ -1380,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 @@ -1433,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 @@ -1744,6 +1805,13 @@ Displays a horizontal bar made up of half-images with an optional background. * `item`: Position of item that is selected. * `direction`: Direction the list will be displayed in * `offset`: offset in pixels from position. +* `alignment`: The alignment of the inventory. Aligned at the top left corner if not specified. + +### `hotbar` + +* `direction`: Direction the list will be displayed in +* `offset`: offset in pixels from position. +* `alignment`: The alignment of the inventory. ### `waypoint` @@ -1802,6 +1870,11 @@ Displays a minimap on the HUD. * `size`: Size of the minimap to display. Minimap should be a square to avoid distortion. + * Negative values represent percentages of the screen. If either `x` or `y` + is specified as a percentage, the resulting pixel size will be used for + both `x` and `y`. Example: On a 1920x1080 screen, `{x = 0, y = -25}` will + result in a 270x270 minimap. + * Negative values are supported starting with protocol version 45. * `alignment`: The alignment of the minimap. * `offset`: offset in pixels from position. @@ -2625,6 +2698,9 @@ background elements are drawn before all other elements. **WARNING**: do _not_ use an element name starting with `key_`; those names are reserved to pass key press events to formspec! +**WARNING**: names and values of elements cannot contain binary data such as ASCII +control characters. For values, escape sequences used by the engine are an exception to this. + **WARNING**: Minetest allows you to add elements to every single formspec instance using `player:set_formspec_prepend()`, which may be the reason backgrounds are appearing when you don't expect them to, or why things are styled differently @@ -2677,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 -------- @@ -2760,7 +2838,7 @@ Elements * End of a container, following elements are no longer relative to this container. -### `scroll_container[,;,;;;]` +### `scroll_container[,;,;;;;]` * Start of a scroll_container block. All contained elements will ... * take the scroll_container coordinate as position origin, @@ -2769,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 @@ -2861,14 +2945,14 @@ Elements * Requires formspec version >= 6. * See `background9[]` documentation for more information. -### `model[,;,;;;;;;;;]` +### `model[,;,;;;;;;;;]` * Show a mesh model. * `name`: Element name that can be used for styling * `mesh`: The mesh model to use. * `textures`: The mesh textures to use according to the mesh materials. Texture names must be separated by commas. -* `rotation {X,Y}` (Optional): Initial rotation of the camera. +* `rotation` (Optional): Initial rotation of the camera, format `x,y`. The axes are euler angles in degrees. * `continuous` (Optional): Whether the rotation is continuous. Default `false`. * `mouse control` (Optional): Whether the model can be controlled with the mouse. Default `true`. @@ -3658,7 +3742,7 @@ Player Inventory lists * `hand`: list containing an override for the empty hand * Is not created automatically, use `InvRef:set_size` * Is only used to enhance the empty hand's tool capabilities - + Custom lists can be added and deleted with `InvRef:set_size(name, size)` like any other inventory. @@ -3847,15 +3931,23 @@ vectors are written like this: `(x, y, z)`: * If `v` has zero length, returns `(0, 0, 0)`. * `vector.floor(v)`: * Returns a vector, each dimension rounded down. +* `vector.ceil(v)`: + * Returns a vector, each dimension rounded up. * `vector.round(v)`: * Returns a vector, each dimension rounded to nearest integer. * At a multiple of 0.5, rounds away from zero. -* `vector.apply(v, func)`: +* `vector.sign(v, tolerance)`: + * Returns a vector where `math.sign` was called for each component. + * See [Helper functions] for details. +* `vector.abs(v)`: + * Returns a vector with absolute values for each component. +* `vector.apply(v, func, ...)`: * Returns a vector where the function `func` has been applied to each component. + * `...` are optional arguments passed to `func`. * `vector.combine(v, w, func)`: - * Returns a vector where the function `func` has combined both components of `v` and `w` - for each component + * Returns a vector where the function `func` has combined both components of `v` and `w` + for each component * `vector.equals(v1, v2)`: * Returns a boolean, `true` if the vectors are identical. * `vector.sort(v1, v2)`: @@ -3873,10 +3965,14 @@ vectors are written like this: `(x, y, z)`: by a `vector.*` function. * Returns `false` for anything else, including tables like `{x=3,y=1,z=4}`. * `vector.in_area(pos, min, max)`: - * Returns a boolean value indicating if `pos` is inside area formed by `min` and `max`. - * `min` and `max` are inclusive. - * If `min` is bigger than `max` on some axis, function always returns false. - * You can use `vector.sort` if you have two vectors and don't know which are the minimum and the maximum. + * Returns a boolean value indicating if `pos` is inside area formed by `min` and `max`. + * `min` and `max` are inclusive. + * If `min` is bigger than `max` on some axis, function always returns false. + * You can use `vector.sort` if you have two vectors and don't know which are the minimum and the maximum. +* `vector.random_in_area(min, max)`: + * Returns a random integer position in area formed by `min` and `max` + * `min` and `max` are inclusive. + * You can use `vector.sort` if you have two vectors and don't know which are the minimum and the maximum. For the following functions `x` can be either a vector or a number: @@ -4082,10 +4178,6 @@ Translations Texts can be translated client-side with the help of `minetest.translate` and translation files. -Consider using the script `mod_translation_updater.py` in the Minetest -[modtools](https://github.com/minetest/modtools) repository to generate and -update translation files automatically from the Lua sources. - Translating a string -------------------- @@ -4093,13 +4185,15 @@ Two functions are provided to translate strings: `minetest.translate` and `minetest.get_translator`. * `minetest.get_translator(textdomain)` is a simple wrapper around - `minetest.translate`, and `minetest.get_translator(textdomain)(str, ...)` is - equivalent to `minetest.translate(textdomain, str, ...)`. + `minetest.translate` and `minetest.translate_n`. + After `local S, NS = minetest.get_translator(textdomain)`, we have + `S(str, ...)` equivalent to `minetest.translate(textdomain, str, ...)`, and + `NS(str, str_plural, n, ...)` to `minetest.translate_n(textdomain, str, str_plural, n, ...)`. It is intended to be used in the following way, so that it avoids verbose repetitions of `minetest.translate`: ```lua - local S = minetest.get_translator(textdomain) + local S, NS = minetest.get_translator(textdomain) S(str, ...) ``` @@ -4116,29 +4210,102 @@ Two functions are provided to translate strings: `minetest.translate` and arguments the translated string expects. Arguments are literal strings -- they will not be translated. -For instance, suppose we want to greet players when they join. We can do the +* `minetest.translate_n(textdomain, str, str_plural, n, ...)` translates the + string `str` with the given `textdomain` for disambiguaion. The value of + `n`, which must be a nonnegative integer, is used to decide whether to use + the singular or the plural version of the string. Depending on the locale of + the client, the choice between singular and plural might be more complicated, + but the choice will be done automatically using the value of `n`. + + You can read https://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html + for more details on the differences of plurals between languages. + + Also note that plurals are only handled in .po or .mo files, and not in .tr files. + +For instance, suppose we want to greet players when they join and provide a +command that shows the amount of time since the player joined. We can do the following: ```lua -local S = minetest.get_translator("hello") +local S, NS = minetest.get_translator("hello") minetest.register_on_joinplayer(function(player) local name = player:get_player_name() minetest.chat_send_player(name, S("Hello @1, how are you today?", name)) end) +minetest.register_chatcommand("playtime", { + func = function(name) + local last_login = core.get_auth_handler().get_auth(name).last_login + local playtime = math.floor((last_login-os.time())/60) + return true, NS( + "You have been playing for @1 minute.", + "You have been playing for @1 minutes.", + minutes, tostring(minutes)) + end, +}) ``` When someone called "CoolGuy" joins the game with an old client or a client that does not have localization enabled, they will see `Hello CoolGuy, how are -you today?` +you today?`. If they use the `/playtime` command, they will see `You have been +playing for 1 minute` or (for example) `You have been playing for 4 minutes.` -However, if we have for instance a translation file named `hello.de.tr` +However, if we have for instance a translation file named `hello.de.po` containing the following: - # textdomain: hello - Hello @1, how are you today?=Hallo @1, wie geht es dir heute? +```po +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Hello @1, how are you today?" +msgstr "Hallo @1, wie geht es dir heute?" + +msgid "You have been playing for @1 minute." +msgid_plural "You have been playing for @1 minutes." +msgstr[0] "Du spielst seit @1 Minute." +msgstr[1] "Du spielst seit @1 Minuten." +``` and CoolGuy has set a German locale, they will see `Hallo CoolGuy, wie geht es -dir heute?` +dir heute?` when they join, and the `/playtime` command will show them `Du +spielst seit 1 Minute.` or (for example) `Du spielst seit 4 Minuten.` + +Creating and updating translation files +--------------------------------------- + +As an alternative to writing translation files by hand (as shown in the above +example), it is also possible to generate translation files based on the source +code. + +It is recommended to first generate a translation template. The translation +template includes translatable strings that translators can directly work on. +After creating the `locale` directory, a translation template for the above +example using the following command: + +```sh +xgettext -L lua -kS -kNS:1,2 -kminetest.translate:1c,2 -kminetest.translate_n:1c,2,3 \ + -d hello -o locale/hello.pot *.lua +``` + +The above command can also be used to update the translation template when new +translatable strings are added. + +The German translator can then create the translation file with + +```sh +msginit -l de -i locale/hello.pot -o locale/hello.de.po +``` + +and provide the translations by editing `locale/hello.de.po`. + +The translation file can be updated using + +```sh +msgmerge -U locale/hello.de.po locale/hello.pot +``` + +Refer to the [Gettext manual](https://www.gnu.org/software/gettext/manual/) for +further information on creating and updating translation files. Operations on translated strings -------------------------------- @@ -4152,8 +4319,8 @@ expected manner. However, string concatenation will still work as expected sentences by breaking them into parts; arguments should be used instead), and operations such as `minetest.colorize` which are also concatenation. -Translation file format ------------------------ +Old translation file format +--------------------------- A translation file has the suffix `.[lang].tr`, where `[lang]` is the language it corresponds to. It must be put into the `locale` subdirectory of the mod. @@ -4168,6 +4335,34 @@ The file should be a text file, with the following format: There must be no extraneous whitespace around the `=` or at the beginning or the end of the line. +Using the earlier example of greeting the player, the translation file would be + +``` +# textdomain: hello +Hello @1, how are you today?=Hallo @1, wie geht es dir heute? +``` + +For old translation files, consider using the script `mod_translation_updater.py` +in the Minetest [modtools](https://github.com/minetest/modtools) repository to +generate and update translation files automatically from the Lua sources. + +Gettext translation file format +------------------------------- + +Gettext files can also be used as translations. A translation file has the suffix +`.[lang].po` or `.[lang].mo`, depending on whether it is compiled or not, and must +also be placed in the `locale` subdirectory of the mod. The value of `textdomain` +is `msgctxt` in the gettext files. If `msgctxt` is not provided, the name of the +translation file is used instead. + +A typical entry in a `.po` file would look like: + +```po +msgctxt "textdomain" +msgid "Hello world!" +msgstr "Bonjour le monde!" +``` + Escapes ------- @@ -4914,8 +5109,7 @@ Methods the `VoxelManip`. * `calc_lighting([p1, p2], [propagate_shadow])`: Calculate lighting within the `VoxelManip`. - * To be used only by a `VoxelManip` object from - `minetest.get_mapgen_object`. + * To be used only with a `VoxelManip` object from `minetest.get_mapgen_object`. * (`p1`, `p2`) is the area in which lighting is set, defaults to the whole area if left out or nil. For almost all uses these should be left out or nil to use the default. @@ -4923,9 +5117,11 @@ Methods generated mapchunk above are propagated down into the mapchunk, defaults to `true` if left out. * `update_liquids()`: Update liquid flow -* `was_modified()`: Returns `true` or `false` if the data in the voxel - manipulator had been modified since the last read from map, due to a call to - `minetest.set_data()` on the loaded area elsewhere. +* `was_modified()`: Returns `true` if the data in the voxel manipulator has been modified + since it was last read from the map. This means you have to call `get_data` again. + This only applies to a `VoxelManip` object from `minetest.get_mapgen_object`, + where the engine will keep the map and the VM in sync automatically. + * Note: this doesn't do what you think it does and is subject to removal. Don't use it! * `get_emerged_area()`: Returns actual emerged minimum and maximum positions. `VoxelArea` @@ -5076,12 +5272,12 @@ Callbacks: used for updating the entity state. * `on_deactivate(self, removal)` * Called when the object is about to get removed or unloaded. - * `removal`: boolean indicating whether the object is about to get removed. - Calling `object:remove()` on an active object will call this with `removal=true`. - The mapblock the entity resides in being unloaded will call this with `removal=false`. - * Note that this won't be called if the object hasn't been activated in the first place. - In particular, `minetest.clear_objects({mode = "full"})` won't call this, - whereas `minetest.clear_objects({mode = "quick"})` might call this. + * `removal`: boolean indicating whether the object is about to get removed. + Calling `object:remove()` on an active object will call this with `removal=true`. + The mapblock the entity resides in being unloaded will call this with `removal=false`. + * Note that this won't be called if the object hasn't been activated in the first place. + In particular, `minetest.clear_objects({mode = "full"})` won't call this, + whereas `minetest.clear_objects({mode = "quick"})` might call this. * `on_step(self, dtime, moveresult)` * Called on every server tick, after movement and collision processing. * `dtime`: elapsed time since last call @@ -5456,6 +5652,12 @@ Utilities moveresult_new_pos = true, -- Allow removing definition fields in `minetest.override_item` (5.9.0) override_item_remove_fields = true, + -- The predefined hotbar is a Lua HUD element of type `hotbar` (5.10.0) + 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, } ``` @@ -5514,8 +5716,8 @@ Utilities }, -- Estimated maximum formspec size before Minetest will start shrinking the - -- formspec to fit. For a fullscreen formspec, use a size 10-20% larger than - -- this and `padding[-0.01,-0.01]`. + -- formspec to fit. For a fullscreen formspec, use this formspec size and + -- `padding[0,0]`. `bgcolor[;true]` is also recommended. max_formspec_size = { x = 20, y = 11.25 @@ -5589,6 +5791,13 @@ Utilities * `minetest.colorspec_to_bytes(colorspec)`: Converts a ColorSpec to a raw string of four bytes in an RGBA layout, returned as a string. * `colorspec`: The ColorSpec to convert +* `minetest.colorspec_to_table(colorspec)`: Converts a ColorSpec into RGBA table + form. If the ColorSpec is invalid, returns `nil`. You can use this to parse + ColorStrings. + * `colorspec`: The ColorSpec to convert +* `minetest.time_to_day_night_ratio(time_of_day)`: Returns a "day-night ratio" value + (as accepted by `ObjectRef:override_day_night_ratio`) that is equivalent to + the given "time of day" value (as returned by `minetest.get_timeofday`). * `minetest.encode_png(width, height, data, [compression])`: Encode a PNG image and return it in string form. * `width`: Width of the image @@ -5723,7 +5932,7 @@ Call these functions only at load time! * `minetest.register_globalstep(function(dtime))` * Called every server step, usually interval of 0.1s. - * `dtime` is the time since last execution in seconds. + * `dtime` is the time since last execution in seconds. * `minetest.register_on_mods_loaded(function())` * Called after mods have finished loading and before the media is cached or the aliases handled. @@ -5772,8 +5981,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 @@ -5794,6 +6008,7 @@ Call these functions only at load time! * `minetest.register_on_dieplayer(function(ObjectRef, reason))` * Called when a player dies * `reason`: a PlayerHPChangeReason table, see register_on_player_hpchange + * For customizing the death screen, see `minetest.show_death_screen`. * `minetest.register_on_respawnplayer(function(ObjectRef))` * Called when player is to be respawned * Called _before_ repositioning of player occurs @@ -6070,6 +6285,8 @@ Environment access * `minetest.swap_node(pos, node)` * Swap node at position with another. * This keeps the metadata intact and will not run con-/destructor callbacks. +* `minetest.bulk_swap_node({pos1, pos2, pos3, ...}, node)` + * Equivalent to `minetest.swap_node` but in bulk. * `minetest.remove_node(pos)`: Remove a node * Equivalent to `minetest.set_node(pos, {name="air"})`, but a bit faster. * `minetest.get_node(pos)` @@ -6151,7 +6368,7 @@ Environment access * **Warning**: The same warning as for `minetest.get_objects_inside_radius` applies. Use `minetest.objects_in_area` instead to iterate only valid objects. * `minetest.objects_in_area(min_pos, max_pos)` - * returns an iterator of valid objects + * returns an iterator of valid objects * `minetest.set_timeofday(val)`: set time of day * `val` is between `0` and `1`; `0` for midnight, `0.5` for midday * `minetest.get_timeofday()`: get time of day @@ -6463,7 +6680,8 @@ Formspec * `playername`: name of player to show formspec * `formname`: name passed to `on_player_receive_fields` callbacks. It should follow the `"modname:"` naming convention. - `formname` must not be empty. + * `formname` must not be empty, unless you want to reshow + the inventory formspec without updating it for future opens. * `formspec`: formspec to display * `minetest.close_formspec(playername, formname)` * `playername`: name of player to close formspec @@ -6477,6 +6695,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: @@ -6495,6 +6716,13 @@ Formspec * `"INV"`: something failed * `"CHG"`: has been changed * `"VAL"`: not changed +* `minetest.show_death_screen(player, reason)` + * Called when the death screen should be shown. + * `player` is an ObjectRef, `reason` is a PlayerHPChangeReason table or nil. + * By default, this shows a simple formspec with the option to respawn. + Respawning is done via `ObjectRef:respawn`. + * You can override this to show a custom death screen. + * For general death handling, use `minetest.register_on_dieplayer` instead. Item handling ------------- @@ -6727,17 +6955,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 @@ -6767,7 +6984,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: @@ -6845,6 +7063,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: @@ -6922,6 +7141,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 ---- @@ -6932,10 +7197,11 @@ Bans * Returns boolean indicating success * `minetest.unban_player_or_ip(ip_or_name)`: remove ban record matching IP address or name -* `minetest.kick_player(name, [reason])`: disconnect a player with an optional +* `minetest.kick_player(name[, reason[, reconnect]])`: disconnect a player with an optional reason. * Returns boolean indicating success (false if player nonexistent) -* `minetest.disconnect_player(name, [reason])`: disconnect a player with an + * If `reconnect` is true, allow the user to reconnect. +* `minetest.disconnect_player(name[, reason[, reconnect]])`: disconnect a player with an optional reason, this will not prefix with 'Kicked: ' like kick_player. If no reason is given, it will default to 'Disconnected.' * Returns boolean indicating success (false if player nonexistent) @@ -7101,7 +7367,7 @@ Misc. could be used as a player name (regardless of whether said player exists). * `minetest.hud_replace_builtin(name, hud_definition)` * Replaces definition of a builtin hud element - * `name`: `"breath"`, `"health"` or `"minimap"` + * `name`: `"breath"`, `"health"`, `"minimap"` or `"hotbar"` * `hud_definition`: definition to replace builtin definition * `minetest.parse_relative_number(arg, relative_to)`: returns number or nil * Helper function for chat commands. @@ -7320,6 +7586,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 -------------- @@ -7930,8 +8207,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` @@ -7961,13 +8237,13 @@ child will follow movement and rotation of that bone. object. * `set_detach()`: Detaches object. No-op if object was not attached. * `set_bone_position([bone, position, rotation])` - * Shorthand for `set_bone_override(bone, {position = position, rotation = rotation:apply(math.rad)})` using absolute values. - * **Note:** Rotation is in degrees, not radians. - * **Deprecated:** Use `set_bone_override` instead. + * Shorthand for `set_bone_override(bone, {position = position, rotation = rotation:apply(math.rad)})` using absolute values. + * **Note:** Rotation is in degrees, not radians. + * **Deprecated:** Use `set_bone_override` instead. * `get_bone_position(bone)`: returns the previously set position and rotation of the bone - * Shorthand for `get_bone_override(bone).position.vec, get_bone_override(bone).rotation.vec:apply(math.deg)`. - * **Note:** Returned rotation is in degrees, not radians. - * **Deprecated:** Use `get_bone_override` instead. + * Shorthand for `get_bone_override(bone).position.vec, get_bone_override(bone).rotation.vec:apply(math.deg)`. + * **Note:** Returned rotation is in degrees, not radians. + * **Deprecated:** Use `get_bone_override` instead. * `set_bone_override(bone, override)` * `bone`: string * `override`: `{ position = property, rotation = property, scale = property }` or `nil` @@ -7984,7 +8260,7 @@ child will follow movement and rotation of that bone. * Compatibility note: Clients prior to 5.9.0 only support absolute position and rotation. All values are treated as absolute and are set immediately (no interpolation). * `get_bone_override(bone)`: returns `override` in the above format - * **Note:** Unlike `get_bone_position`, the returned rotation is in radians, not degrees. + * **Note:** Unlike `get_bone_position`, the returned rotation is in radians, not degrees. * `get_bone_overrides()`: returns all bone overrides as table `{[bonename] = override, ...}` * `set_properties(object property table)` * `get_properties()`: returns a table of all object properties @@ -8081,8 +8357,8 @@ child will follow movement and rotation of that bone. * Fifth column: subject viewed from above * Sixth column: subject viewed from below * `get_luaentity()`: - * Returns the object's associated luaentity table, if there is one - * Otherwise returns `nil` (e.g. for players) + * Returns the object's associated luaentity table, if there is one + * Otherwise returns `nil` (e.g. for players) * `get_entity_name()`: * **Deprecated**: Will be removed in a future version, use `:get_luaentity().name` instead. @@ -8154,12 +8430,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. @@ -8441,12 +8723,15 @@ child will follow movement and rotation of that bone. if set to zero the clouds are rendered flat. * `speed`: 2D cloud speed + direction in nodes per second (default `{x=0, z=-2}`). + * `shadow`: shadow color, applied to the base of the cloud + (default `#cccccc`). * `get_clouds()`: returns a table with the current cloud parameters as in `set_clouds`. * `override_day_night_ratio(ratio or nil)` * `0`...`1`: Overrides day-night ratio, controlling sunlight to a specific amount. * Passing no arguments disables override, defaulting to sunlight based on day-night cycle + * See also `minetest.time_to_day_night_ratio`, * `get_day_night_ratio()`: returns the ratio or nil if it isn't overridden * `set_local_animation(idle, walk, dig, walk_while_dig, frame_speed)`: set animation for player model in third person view. @@ -8473,27 +8758,67 @@ child will follow movement and rotation of that bone. * Passing no arguments resets lighting to its default values. * `light_definition` is a table with the following optional fields: * `saturation` sets the saturation (vividness; default: `1.0`). - * values > 1 increase the saturation - * values in [0,1] decrease the saturation + * It is applied according to the function `result = b*(1-s) + c*s`, where: + * `c` is the original color + * `b` is the greyscale version of the color with the same luma + * `s` is the saturation set here + * The resulting color always has the same luma (perceived brightness) as the original. + * This means that: + * values > 1 oversaturate + * values < 1 down to 0 desaturate, 0 being entirely greyscale + * 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}`) * `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`. * `respawn()`: Respawns the player using the same mechanism as the death screen, including calling `on_respawnplayer` callbacks. +* `get_flags()`: returns a table of player flags (the following boolean fields): + * `breathing`: Whether breathing (regaining air) is enabled, default `true`. + * `drowning`: Whether drowning (losing air) is enabled, default `true`. + * `node_damage`: Whether the player takes damage from nodes, default `true`. +* `set_flags(flags)`: sets flags + * takes a table in the same format as returned by `get_flags` + * absent fields are left unchanged + `PcgRandom` ----------- @@ -8669,7 +8994,7 @@ In multiplayer mode, the error may be arbitrarily large. Interface for the operating system's crypto-secure PRNG. -It can be created via `SecureRandom()`. The constructor returns nil if a +It can be created via `SecureRandom()`. The constructor throws an error if a secure random device cannot be found on the system. ### Methods @@ -8940,7 +9265,7 @@ Player properties need to be saved manually. Entity definition ----------------- -Used by `minetest.register_entity`. +Used by `minetest.register_entity`. The entity definition table becomes a metatable of a newly created per-entity luaentity table, meaning its fields (e.g. `initial_properties`) will be shared between all instances of an entity. @@ -8996,6 +9321,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 @@ -9031,7 +9361,12 @@ Used by `minetest.register_lbm`. A loading block modifier (LBM) is used to define a function that is called for specific nodes (defined by `nodenames`) when a mapblock which contains such nodes -gets activated (not loaded!) +gets activated (not loaded!). + +Note: LBMs operate on a "snapshot" of node positions taken once before they are triggered. +That means if an LBM callback adds a node, it won't be taken into account. +However the engine guarantees that when the callback is called that all given position(s) +contain a matching node. ```lua { @@ -9055,7 +9390,13 @@ gets activated (not loaded!) action = function(pos, node, dtime_s) end, -- Function triggered for each qualifying node. -- `dtime_s` is the in-game time (in seconds) elapsed since the block - -- was last active + -- was last active. + + bulk_action = function(pos_list, dtime_s) end, + -- Function triggered with a list of all applicable node positions at once. + -- This can be provided as an alternative to `action` (not both). + -- Available since `minetest.features.bulk_lbms` (5.10.0) + -- `dtime_s`: as above } ``` @@ -9262,9 +9603,17 @@ Used by `minetest.register_node`, `minetest.register_craftitem`, and -- If specified as a table, the field to be used is selected according to -- the current `pointed_thing`. -- There are three possible TouchInteractionMode values: - -- * "user" (meaning depends on client-side settings) -- * "long_dig_short_place" (long tap = dig, short tap = place) -- * "short_dig_long_place" (short tap = dig, long tap = place) + -- * "user": + -- * For `pointed_object`: Equivalent to "short_dig_long_place" if the + -- client-side setting "touch_punch_gesture" is "short_tap" (the + -- default value) and the item is able to punch (i.e. has no on_use + -- callback defined). + -- Equivalent to "long_dig_short_place" otherwise. + -- * For `pointed_node` and `pointed_nothing`: + -- Equivalent to "long_dig_short_place". + -- * The behavior of "user" may change in the future. -- The default value is "user". sound = { @@ -9386,12 +9735,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 @@ -10640,8 +10995,9 @@ Used by `ObjectRef:hud_add`. Returned by `ObjectRef:hud_get`. ```lua { type = "image", - -- Type of element, can be "image", "text", "statbar", "inventory", - -- "waypoint", "image_waypoint", "compass" or "minimap" + -- Type of element, can be "compass", "hotbar" (46 ¹), "image", "image_waypoint", + -- "inventory", "minimap" (44 ¹), "statbar", "text" or "waypoint" + -- ¹: minimal protocol version for client-side support -- If undefined "text" will be used. hud_elem_type = "image", @@ -10987,7 +11343,7 @@ Types used are defined in the previous section. * vec3 range `acc`: the direction and speed with which the particle accelerates -* vec3 range `size`: scales the visual size of the particle texture. +* float range `size`: scales the visual size of the particle texture. if `node` is set, this can be set to 0 to spawn randomly-sized particles (just like actual node dig particles). @@ -11292,6 +11648,16 @@ Functions: bit.tobit, bit.tohex, bit.bnot, bit.band, bit.bor, bit.bxor, bit.lshi See http://bitop.luajit.org/ for advanced information. +Tracy Profiler +-------------- + +Minetest can be built with support for the Tracy profiler, which can also be +useful for profiling mods and is exposed to Lua as the global `tracy`. + +See doc/developing/misc.md for details. + +Note: This is a development feature and not covered by compatibility promises. + Error Handling -------------- diff --git a/doc/menu_lua_api.md b/doc/menu_lua_api.md index 9f0e23a11..c03c0501e 100644 --- a/doc/menu_lua_api.md +++ b/doc/menu_lua_api.md @@ -8,6 +8,15 @@ The main menu is defined as a formspec by Lua in `builtin/mainmenu/` Description of formspec language to show your menu is in `lua_api.md` +Images and 3D models +------ + +Directory delimiters change according to the OS (e.g. on Unix-like systems +is `/`, on Windows is `\`). When putting an image or a 3D model inside a formspec, +be sure to sanitize it first with `core.formspec_escape(img)`; otherwise, +any resource located in a subpath won't be displayed on OSs using `\` as delimiter. + + Callbacks --------- @@ -48,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. @@ -56,12 +68,20 @@ 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 Filesystem ---------- +To access specific subpaths, use `DIR_DELIM` as a directory delimiter instead +of manually putting one, as different OSs use different delimiters. E.g. +```lua +"my" .. DIR_DELIM .. "custom" .. DIR_DELIM .. "path" -- and not my/custom/path +``` + * `core.get_builtin_path()` * returns path to builtin root * `core.create_dir(absolute_path)` (possible in async calls) @@ -238,8 +258,8 @@ GUI }, -- Estimated maximum formspec size before Minetest will start shrinking the - -- formspec to fit. For a fullscreen formspec, use a size 10-20% larger than - -- this and `padding[-0.01,-0.01]`. + -- formspec to fit. For a fullscreen formspec, use this formspec size and + -- `padding[0,0]`. `bgcolor[;true]` is also recommended. max_formspec_size = { x = 20, y = 11.25 @@ -282,7 +302,7 @@ Package - content which is downloadable from the content db, may or may not be i ```lua { mods = "/home/user/.minetest/mods", - share = "/usr/share/minetest/mods", + share = "/usr/share/minetest/mods", -- only provided when RUN_IN_PLACE=0 -- Custom dirs can be specified by the MINETEST_MOD_DIR env variable ["/path/to/custom/dir"] = "/path/to/custom/dir", diff --git a/doc/world_format.md b/doc/world_format.md index b5a2a3cfa..93920f391 100644 --- a/doc/world_format.md +++ b/doc/world_format.md @@ -394,7 +394,7 @@ Timestamp and node ID mappings were introduced in map format version 29. * `u8` `name_id_mapping_version` * Should be zero for map format version 29. - + * `u16` `num_name_id_mappings` * foreach `num_name_id_mappings`: * `u16` `id` diff --git a/games/devtest/mods/benchmarks/init.lua b/games/devtest/mods/benchmarks/init.lua index 1f5001c69..e3a4409a5 100644 --- a/games/devtest/mods/benchmarks/init.lua +++ b/games/devtest/mods/benchmarks/init.lua @@ -154,3 +154,36 @@ minetest.register_chatcommand("bench_bulk_get_node", { return true, msg end, }) + +minetest.register_chatcommand("bench_bulk_swap_node", { + params = "", + description = "Benchmark: Bulk-swap 99×99×99 stone nodes", + func = function(name, param) + local player = minetest.get_player_by_name(name) + if not player then + return false, "No player." + end + local pos_list = get_positions_cube(player:get_pos()) + + minetest.chat_send_player(name, "Benchmarking minetest.bulk_swap_node. Warming up ...") + + -- warm up because first execution otherwise becomes + -- significantly slower + minetest.bulk_swap_node(pos_list, {name = "mapgen_stone"}) + + minetest.chat_send_player(name, "Warming up finished, now benchmarking ...") + + local start_time = minetest.get_us_time() + for i=1,#pos_list do + minetest.swap_node(pos_list[i], {name = "mapgen_stone"}) + end + local middle_time = minetest.get_us_time() + minetest.bulk_swap_node(pos_list, {name = "mapgen_stone"}) + local end_time = minetest.get_us_time() + local msg = string.format("Benchmark results: minetest.swap_node loop: %.2f ms; minetest.bulk_swap_node: %.2f ms", + ((middle_time - start_time)) / 1000, + ((end_time - middle_time)) / 1000 + ) + return true, msg + end, +}) diff --git a/games/devtest/mods/gltf/LICENSE.md b/games/devtest/mods/gltf/LICENSE.md new file mode 100644 index 000000000..6c3828a4a --- /dev/null +++ b/games/devtest/mods/gltf/LICENSE.md @@ -0,0 +1,14 @@ +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) + * Licensed under CC0, public domain "wherever public domain carries fewer rights or legal protections" +* Frog (`gltf_frog.gltf`, `gltf_frog.png`): + * By [Susybaka1234](https://sketchfab.com/3d-models/african-clawed-frog-v2-c81152c93948480c931c280d18957358) + * Licensed under CC-BY 4.0 +* Snow Man (`gltf_snow_man.gltf`, `gltf_snow_man.png`): + * By [jordan4ibanez](https://github.com/jordan4ibanez) + * Licensed under CC0 +* Minimal triangle, triangle without indices (`gltf_minimal_triangle.gltf`, `gltf_triangle_without_indices.gltf`) + * From [the glTF sample model collection](https://github.com/KhronosGroup/glTF-Sample-Models) + * Licensed under CC0 / public domain diff --git a/games/devtest/mods/gltf/init.lua b/games/devtest/mods/gltf/init.lua new file mode 100644 index 000000000..252fd017d --- /dev/null +++ b/games/devtest/mods/gltf/init.lua @@ -0,0 +1,95 @@ +local function register_entity(name, textures, backface_culling) + minetest.register_entity("gltf:" .. name, { + initial_properties = { + visual = "mesh", + mesh = "gltf_" .. name .. ".gltf", + textures = textures, + backface_culling = backface_culling, + }, + }) +end + +-- These do not have texture coordinates; they simple render as black surfaces. +register_entity("minimal_triangle", {}, false) +register_entity("triangle_with_vertex_stride", {}, false) +register_entity("triangle_without_indices", {}, false) +do + local cube_textures = {"gltf_cube.png"} + 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"}) + +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. +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", + tiles = {{name = "gltf_frog.png", backface_culling = false}}, + drawtype = "mesh", + mesh = "gltf_frog.gltf", +}) + +minetest.register_chatcommand("show_model", { + params = " [textures]", + description = "Show a model (defaults to gltf models, for example '/show_model frog').", + func = function(name, param) + local model, textures = param:match"^(.-)%s+(.+)$" + if not model then + model = "gltf_" .. param .. ".gltf" + textures = "gltf_" .. param .. ".png" + end + minetest.show_formspec(name, "gltf:model", table.concat{ + "formspec_version[7]", + "size[10,10]", + "model[0,0;10,10;model;", model, ";", textures, ";0,0;true;true;0,0;0]", + }) + end, +}) diff --git a/games/devtest/mods/gltf/invalid/empty.gltf b/games/devtest/mods/gltf/invalid/empty.gltf new file mode 100644 index 000000000..e69de29bb diff --git a/games/devtest/mods/gltf/invalid/invalid_bufferview_bounds.gltf b/games/devtest/mods/gltf/invalid/invalid_bufferview_bounds.gltf new file mode 100644 index 000000000..2182861c6 --- /dev/null +++ b/games/devtest/mods/gltf/invalid/invalid_bufferview_bounds.gltf @@ -0,0 +1 @@ +{"scene":0,"scenes":[{"nodes":[0]}],"nodes":[{"mesh":0}],"meshes":[{"primitives":[{"attributes":{"POSITION":0}}]}],"buffers":[{"uri":"data:application/octet-stream;base64,AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAA","byteLength":36}],"bufferViews":[{"buffer":0,"byteOffset":1,"byteLength":36,"target":34962}],"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":3,"type":"VEC3","max":[1,1,0],"min":[0,0,0]}],"asset":{"version":"2.0"}} diff --git a/games/devtest/mods/gltf/invalid/json_missing_brace.gltf b/games/devtest/mods/gltf/invalid/json_missing_brace.gltf new file mode 100644 index 000000000..98232c64f --- /dev/null +++ b/games/devtest/mods/gltf/invalid/json_missing_brace.gltf @@ -0,0 +1 @@ +{ diff --git a/games/devtest/mods/gltf/mod.conf b/games/devtest/mods/gltf/mod.conf new file mode 100644 index 000000000..3ec50d2ef --- /dev/null +++ b/games/devtest/mods/gltf/mod.conf @@ -0,0 +1,2 @@ +name = gltf +description = Hosts gltf test models, both for the C++ unit tests and for in-game viewing diff --git a/games/devtest/mods/gltf/models/gltf_blender_cube.glb b/games/devtest/mods/gltf/models/gltf_blender_cube.glb new file mode 100644 index 000000000..b1894fc4f Binary files /dev/null and b/games/devtest/mods/gltf/models/gltf_blender_cube.glb differ diff --git a/games/devtest/mods/gltf/models/gltf_blender_cube.gltf b/games/devtest/mods/gltf/models/gltf_blender_cube.gltf new file mode 100644 index 000000000..041b4a1fc --- /dev/null +++ b/games/devtest/mods/gltf/models/gltf_blender_cube.gltf @@ -0,0 +1 @@ +{"asset":{"generator":"Khronos glTF Blender I/O v1.7.33","version":"2.0"},"scene":0,"scenes":[{"name":"Scene","nodes":[0]}],"nodes":[{"mesh":0,"name":"Cube","scale":[10,10,10]}],"meshes":[{"name":"Cube.004","primitives":[{"attributes":{"POSITION":0,"NORMAL":1,"TEXCOORD_0":2},"indices":3}]}],"accessors":[{"bufferView":0,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":1,"componentType":5126,"count":24,"type":"VEC3"},{"bufferView":2,"componentType":5126,"count":24,"type":"VEC2"},{"bufferView":3,"componentType":5123,"count":36,"type":"SCALAR"}],"bufferViews":[{"buffer":0,"byteLength":288,"byteOffset":0},{"buffer":0,"byteLength":288,"byteOffset":288},{"buffer":0,"byteLength":192,"byteOffset":576},{"buffer":0,"byteLength":72,"byteOffset":768}],"buffers":[{"byteLength":840,"uri":"data:application/octet-stream;base64,AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAvwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAvwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAPwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAPwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAADAPgAAgD8AAAA+AACAPgAAwD4AAAAAAAAgPwAAgD8AACA/AAAAAAAAYD8AAIA+AADAPgAAQD8AAAA+AAAAPwAAwD4AAEA/AAAgPwAAQD8AACA/AABAPwAAYD8AAAA/AADAPgAAgD4AAMA+AACAPgAAwD4AAIA+AAAgPwAAgD4AACA/AACAPgAAID8AAIA+AADAPgAAAD8AAMA+AAAAPwAAwD4AAAA/AAAgPwAAAD8AACA/AAAAPwAAID8AAAA/AAADAAkAAAAJAAYACAAKABUACAAVABMAFAAXABEAFAARAA4ADQAPAAQADQAEAAIABwASAAwABwAMAAEAFgALAAUAFgAFABAA"}]} diff --git a/games/devtest/mods/gltf/models/gltf_blender_cube_matrix_transform.gltf b/games/devtest/mods/gltf/models/gltf_blender_cube_matrix_transform.gltf new file mode 100644 index 000000000..50235ceae --- /dev/null +++ b/games/devtest/mods/gltf/models/gltf_blender_cube_matrix_transform.gltf @@ -0,0 +1 @@ +{"asset":{"generator":"Khronos glTF Blender I/O v1.7.33","version":"2.0"},"scene":0,"scenes":[{"name":"Scene","nodes":[0]}],"nodes":[{"mesh":0,"name":"Cube","matrix":[1,0,0,0,0,2,0,0,0,0,3,0,4,5,6,1]}],"meshes":[{"name":"Cube.004","primitives":[{"attributes":{"POSITION":0,"NORMAL":1,"TEXCOORD_0":2},"indices":3}]}],"accessors":[{"bufferView":0,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":1,"componentType":5126,"count":24,"type":"VEC3"},{"bufferView":2,"componentType":5126,"count":24,"type":"VEC2"},{"bufferView":3,"componentType":5123,"count":36,"type":"SCALAR"}],"bufferViews":[{"buffer":0,"byteLength":288,"byteOffset":0},{"buffer":0,"byteLength":288,"byteOffset":288},{"buffer":0,"byteLength":192,"byteOffset":576},{"buffer":0,"byteLength":72,"byteOffset":768}],"buffers":[{"byteLength":840,"uri":"data:application/octet-stream;base64,AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAvwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAvwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAPwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAPwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAADAPgAAgD8AAAA+AACAPgAAwD4AAAAAAAAgPwAAgD8AACA/AAAAAAAAYD8AAIA+AADAPgAAQD8AAAA+AAAAPwAAwD4AAEA/AAAgPwAAQD8AACA/AABAPwAAYD8AAAA/AADAPgAAgD4AAMA+AACAPgAAwD4AAIA+AAAgPwAAgD4AACA/AACAPgAAID8AAIA+AADAPgAAAD8AAMA+AAAAPwAAwD4AAAA/AAAgPwAAAD8AACA/AAAAPwAAID8AAAA/AAADAAkAAAAJAAYACAAKABUACAAVABMAFAAXABEAFAARAA4ADQAPAAQADQAEAAIABwASAAwABwAMAAEAFgALAAUAFgAFABAA"}]} diff --git a/games/devtest/mods/gltf/models/gltf_blender_cube_scaled.gltf b/games/devtest/mods/gltf/models/gltf_blender_cube_scaled.gltf new file mode 100644 index 000000000..3b626b37e --- /dev/null +++ b/games/devtest/mods/gltf/models/gltf_blender_cube_scaled.gltf @@ -0,0 +1 @@ +{"asset":{"generator":"Khronos glTF Blender I/O v1.7.33","version":"2.0"},"scene":0,"scenes":[{"name":"Scene","nodes":[0]}],"nodes":[{"mesh":0,"name":"Cube","scale":[150,1,21.5]}],"meshes":[{"name":"Cube.004","primitives":[{"attributes":{"POSITION":0,"NORMAL":1,"TEXCOORD_0":2},"indices":3}]}],"accessors":[{"bufferView":0,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":1,"componentType":5126,"count":24,"type":"VEC3"},{"bufferView":2,"componentType":5126,"count":24,"type":"VEC2"},{"bufferView":3,"componentType":5123,"count":36,"type":"SCALAR"}],"bufferViews":[{"buffer":0,"byteLength":288,"byteOffset":0},{"buffer":0,"byteLength":288,"byteOffset":288},{"buffer":0,"byteLength":192,"byteOffset":576},{"buffer":0,"byteLength":72,"byteOffset":768}],"buffers":[{"byteLength":840,"uri":"data:application/octet-stream;base64,AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAvwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAvwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAPwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAPwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAADAPgAAgD8AAAA+AACAPgAAwD4AAAAAAAAgPwAAgD8AACA/AAAAAAAAYD8AAIA+AADAPgAAQD8AAAA+AAAAPwAAwD4AAEA/AAAgPwAAQD8AACA/AABAPwAAYD8AAAA/AADAPgAAgD4AAMA+AACAPgAAwD4AAIA+AAAgPwAAgD4AACA/AACAPgAAID8AAIA+AADAPgAAAD8AAMA+AAAAPwAAwD4AAAA/AAAgPwAAAD8AACA/AAAAPwAAID8AAAA/AAADAAkAAAAJAAYACAAKABUACAAVABMAFAAXABEAFAARAA4ADQAPAAQADQAEAAIABwASAAwABwAMAAEAFgALAAUAFgAFABAA"}]} diff --git a/games/devtest/mods/gltf/models/gltf_frog.gltf b/games/devtest/mods/gltf/models/gltf_frog.gltf new file mode 100644 index 000000000..201604fd3 --- /dev/null +++ b/games/devtest/mods/gltf/models/gltf_frog.gltf @@ -0,0 +1 @@ +{"asset":{"version":"2.0","generator":"Blockbench 4.9.4 glTF exporter"},"scenes":[{"nodes":[20],"name":"blockbench_export"}],"scene":0,"nodes":[{"name":"cube","mesh":0},{"name":"cube","mesh":1},{"name":"cube","mesh":2},{"name":"body","children":[0,1,2]},{"translation":[0,0,-0.0625],"name":"cube","mesh":3},{"translation":[0.03125,0,-0.3125],"name":"cube","mesh":4},{"rotation":[0,-0.19509032201612825,0,0.9807852804032304],"translation":[0.01812248876854733,-0.0625,-0.25194388507103505],"name":"cube","mesh":5},{"translation":[0.0625,0,0.3125],"name":"leftleg","children":[4,5,6]},{"translation":[0.0625,0,-0.3125],"name":"cube","mesh":6},{"translation":[-0.03125,0,-0.3125],"name":"cube","mesh":7},{"rotation":[0,0.19509032201612825,0,0.9807852804032304],"translation":[-0.01812248876854733,-0.0625,-0.25194388507103505],"name":"cube","mesh":8},{"translation":[-0.0625,0,0.3125],"name":"rightleg","children":[8,9,10]},{"translation":[-0.125,-0.0625,0.125],"name":"cube","mesh":9},{"rotation":[0,0.5372996083468239,0,0.8433914458128857],"translation":[0.10431178959951112,-0.0625,0.2349474087973531],"name":"cube","mesh":10},{"rotation":[0,0.5372996083468239,0,0.8433914458128857],"translation":[0.10431178959951112,-0.0625,0.2349474087973531],"name":"cube","mesh":11},{"translation":[0.125,0.0625,-0.125],"name":"leftarm","children":[12,13,14]},{"translation":[0.125,-0.0625,0.125],"name":"cube","mesh":12},{"rotation":[0,-0.5372996083468239,0,0.8433914458128857],"translation":[-0.10431178959951112,-0.0625,0.2349474087973531],"name":"cube","mesh":13},{"rotation":[0,-0.5372996083468239,0,0.8433914458128857],"translation":[-0.10431178959951112,-0.0625,0.2349474087973531],"name":"cube","mesh":14},{"translation":[-0.125,0.0625,-0.125],"name":"rightarm","children":[16,17,18]},{"children":[3,7,11,15,19]}],"bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":288,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":576,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":768,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":840,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":1128,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":1416,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":1608,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":1680,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":1968,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":2256,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":2448,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":2520,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":2808,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":3096,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":3288,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":3360,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":3648,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":3936,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":4128,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":4200,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":4488,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":4776,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":4968,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":5040,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":5328,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":5616,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":5808,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":5880,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":6168,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":6456,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":6648,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":6720,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":7008,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":7296,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":7488,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":7560,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":7848,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":8136,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":8328,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":8400,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":8688,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":8976,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":9168,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":9240,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":9528,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":9816,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":10008,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":10080,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":10368,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":10656,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":10848,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":10920,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":11208,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":11496,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":11688,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":11760,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":12048,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":12336,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":12528,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":12600,"byteLength":12},{"buffer":0,"byteOffset":12612,"byteLength":48},{"buffer":0,"byteOffset":12660,"byteLength":12},{"buffer":0,"byteOffset":12672,"byteLength":48},{"buffer":0,"byteOffset":12720,"byteLength":12},{"buffer":0,"byteOffset":12732,"byteLength":48},{"buffer":0,"byteOffset":12780,"byteLength":12},{"buffer":0,"byteOffset":12792,"byteLength":48},{"buffer":0,"byteOffset":12840,"byteLength":12},{"buffer":0,"byteOffset":12852,"byteLength":48},{"buffer":0,"byteOffset":12900,"byteLength":12},{"buffer":0,"byteOffset":12912,"byteLength":48},{"buffer":0,"byteOffset":12960,"byteLength":12},{"buffer":0,"byteOffset":12972,"byteLength":48},{"buffer":0,"byteOffset":13020,"byteLength":12},{"buffer":0,"byteOffset":13032,"byteLength":48},{"buffer":0,"byteOffset":13080,"byteLength":12},{"buffer":0,"byteOffset":13092,"byteLength":48},{"buffer":0,"byteOffset":13140,"byteLength":4},{"buffer":0,"byteOffset":13144,"byteLength":16},{"buffer":0,"byteOffset":13160,"byteLength":4},{"buffer":0,"byteOffset":13164,"byteLength":16}],"buffers":[{"byteLength":13180,"uri":"data:application/octet-stream;base64,AAAgPgAAAD4AAIA+AAAgPgAAAD4AAIC9AAAgPgAAAAAAAIA+AAAgPgAAAAAAAIC9AAAgvgAAAD4AAIC9AAAgvgAAAD4AAIA+AAAgvgAAAAAAAIC9AAAgvgAAAAAAAIA+AAAgvgAAAD4AAIC9AAAgPgAAAD4AAIC9AAAgvgAAAD4AAIA+AAAgPgAAAD4AAIA+AAAgvgAAAAAAAIA+AAAgPgAAAAAAAIA+AAAgvgAAAAAAAIC9AAAgPgAAAAAAAIC9AAAgvgAAAD4AAIA+AAAgPgAAAD4AAIA+AAAgvgAAAAAAAIA+AAAgPgAAAAAAAIA+AAAgPgAAAD4AAIC9AAAgvgAAAD4AAIC9AAAgPgAAAAAAAIC9AAAgvgAAAAAAAIC9AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAOgCAID4AgB8+AIAgPgAAADoAgF8+AIAfPgCAXz4AQKA+AIAgPgDA7z4AgCA+AECgPgCAXz4AwO8+AIBfPgDAnz4AgB8+AIAgPgCAHz4AwJ8+AAAAOgCAID4AAAA6AMDvPgAAADoAQKA+AAAAOgDA7z4AgB8+AECgPgCAHz4AQPA+AIAgPgDgHz8AgCA+AEDwPgCAXz4A4B8/AIBfPgCAID4AgCA+AMCfPgCAID4AgCA+AIBfPgDAnz4AgF8+AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAAAAPgAAAD4AAIC9AAAAPgAAAD4AAKC+AAAAPgAAAAAAAIC9AAAAPgAAAAAAAKC+AAAAvgAAAD4AAKC+AAAAvgAAAD4AAIC9AAAAvgAAAAAAAKC+AAAAvgAAAAAAAIC9AAAAvgAAAD4AAKC+AAAAPgAAAD4AAKC+AAAAvgAAAD4AAIC9AAAAPgAAAD4AAIC9AAAAvgAAAAAAAIC9AAAAPgAAAAAAAIC9AAAAvgAAAAAAAKC+AAAAPgAAAAAAAKC+AAAAvgAAAD4AAIC9AAAAPgAAAD4AAIC9AAAAvgAAAAAAAIC9AAAAPgAAAAAAAIC9AAAAPgAAAD4AAKC+AAAAvgAAAD4AAKC+AAAAPgAAAAAAAKC+AAAAvgAAAAAAAKC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAOgBAsD4AAP89AECwPgAAADoAwM8+AAD/PQDAzz4AQIA+AECwPgDAvz4AQLA+AECAPgDAzz4AwL8+AMDPPgCAfz4AwK8+AIAAPgDArz4AgH8+AIBgPgCAAD4AgGA+AMC/PgCAYD4AQIA+AIBgPgDAvz4AwK8+AECAPgDArz4AQMA+AECwPgDA/z4AQLA+AEDAPgDAzz4AwP8+AMDPPgCAAD4AQLA+AIB/PgBAsD4AgAA+AMDPPgCAfz4AwM8+AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAAAAPQAAAD4AAKA+AAAAPQAAAD4AAIA+AAAAPQAAAAAAAKA+AAAAPQAAAAAAAIA+AAAAvQAAAD4AAIA+AAAAvQAAAD4AAKA+AAAAvQAAAAAAAIA+AAAAvQAAAAAAAKA+AAAAvQAAAD4AAIA+AAAAPQAAAD4AAIA+AAAAvQAAAD4AAKA+AAAAPQAAAD4AAKA+AAAAvQAAAAAAAKA+AAAAPQAAAAAAAKA+AAAAvQAAAAAAAIA+AAAAPQAAAAAAAIA+AAAAvQAAAD4AAKA+AAAAPQAAAD4AAKA+AAAAvQAAAAAAAKA+AAAAPQAAAAAAAKA+AAAAPQAAAD4AAIA+AAAAvQAAAD4AAIA+AAAAPQAAAAAAAIA+AAAAvQAAAAAAAIA+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AEDwPgAAAj0AwP8+AAACPQBA8D4AAL89AMD/PgAAvz0AIAg/AAACPQDgDz8AAAI9ACAIPwAAvz0A4A8/AAC/PQDgBz8AAPw8ACAAPwAA/DwA4Ac/AAAAOgAgAD8AAAA6AOAPPwAAADoAIAg/AAAAOgDgDz8AAPw8ACAIPwAA/DwAIBA/AAACPQDgFz8AAAI9ACAQPwAAvz0A4Bc/AAC/PQAgAD8AAAI9AOAHPwAAAj0AIAA/AAC/PQDgBz8AAL89AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAABgPgAAAD4AAEA+AABgPgAAAD4AAAAAAABgPgAAAAAAAEA+AABgPgAAAAAAAAAAAAAAvQAAAD4AAAAAAAAAvQAAAD4AAEA+AAAAvQAAAAAAAAAAAAAAvQAAAAAAAEA+AAAAvQAAAD4AAAAAAABgPgAAAD4AAAAAAAAAvQAAAD4AAEA+AABgPgAAAD4AAEA+AAAAvQAAAAAAAEA+AABgPgAAAAAAAEA+AAAAvQAAAAAAAAAAAABgPgAAAAAAAAAAAAAAvQAAAD4AAEA+AABgPgAAAD4AAEA+AAAAvQAAAAAAAEA+AABgPgAAAAAAAEA+AABgPgAAAD4AAAAAAAAAvQAAAD4AAAAAAABgPgAAAAAAAAAAAAAAvQAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AEDQPgBA0D4AwP8+AEDQPgBA0D4AwO8+AMD/PgDA7z4AICA/AEDQPgDgNz8AQNA+ACAgPwDA7z4A4Dc/AMDvPgDgHz8AwM8+ACAAPwDAzz4A4B8/AECgPgAgAD8AQKA+AOA/PwBAoD4AICA/AECgPgDgPz8AwM8+ACAgPwDAzz4AIDg/AEDQPgDgVz8AQNA+ACA4PwDA7z4A4Fc/AMDvPgAgAD8AQNA+AOAfPwBA0D4AIAA/AMDvPgDgHz8AwO8+AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAABgPgAAwD0AABA/AABgPgAAwD0AAKA+AABgPgAAAD0AABA/AABgPgAAAD0AAKA+AADAPQAAwD0AAKA+AADAPQAAwD0AABA/AADAPQAAAD0AAKA+AADAPQAAAD0AABA/AADAPQAAwD0AAKA+AABgPgAAwD0AAKA+AADAPQAAwD0AABA/AABgPgAAwD0AABA/AADAPQAAAD0AABA/AABgPgAAAD0AABA/AADAPQAAAD0AAKA+AABgPgAAAD0AAKA+AADAPQAAwD0AABA/AABgPgAAwD0AABA/AADAPQAAAD0AABA/AABgPgAAAD0AABA/AABgPgAAwD0AAKA+AADAPQAAwD0AAKA+AABgPgAAAD0AAKA+AADAPQAAAD0AAKA+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AECgPgAgGD8AwN8+ACAYPwBAoD4A4B8/AMDfPgDgHz8AIAA/ACAYPwDgHz8AIBg/ACAAPwDgHz8A4B8/AOAfPwDA/z4A4Bc/AEDgPgDgFz8AwP8+AEDwPgBA4D4AQPA+AOAPPwBA8D4AIAA/AEDwPgDgDz8A4Bc/ACAAPwDgFz8AICA/ACAYPwDgLz8AIBg/ACAgPwDgHz8A4C8/AOAfPwBA4D4AIBg/AMD/PgAgGD8AQOA+AOAfPwDA/z4A4B8/AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAMQjQPgAAQD4AAMA+MQjQPgAAQD4AAIA+MQjQPgAAgD0AAMA+MQjQPgAAgD0AAIA+AADQPgAAQD4AAIA+AADQPgAAQD4AAMA+AADQPgAAgD0AAIA+AADQPgAAgD0AAMA+AADQPgAAQD4AAIA+MQjQPgAAQD4AAIA+AADQPgAAQD4AAMA+MQjQPgAAQD4AAMA+AADQPgAAgD0AAMA+MQjQPgAAgD0AAMA+AADQPgAAgD0AAIA+MQjQPgAAgD0AAIA+AADQPgAAQD4AAMA+MQjQPgAAQD4AAMA+AADQPgAAgD0AAMA+MQjQPgAAgD0AAMA+MQjQPgAAQD4AAIA+AADQPgAAQD4AAIA+MQjQPgAAgD0AAIA+AADQPgAAgD0AAIA+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAOgCAYD4AAH49AIBgPgAAADoAwI8+AAB+PQDAjz4AAIE9AIBgPgAA/z0AgGA+AACBPQDAjz4AAP89AMCPPgAAgT0AgF8+AAB+PQCAXz4AAIE9AIAgPgAAfj0AgCA+AACBPQCAID4AAH49AIAgPgAAgT0AgF8+AAB+PQCAXz4AgAA+AIBgPgAA/z0AgGA+AIAAPgDAjz4AAP89AMCPPgAAgT0AgGA+AAB+PQCAYD4AAIE9AMCPPgAAfj0AwI8+AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAAAAvQAAAD4AAOA+AAAAvQAAAD4AAIA+AAAAvQAAAAAAAOA+AAAAvQAAAAAAAIA+AACQvgAAAD4AAIA+AACQvgAAAD4AAOA+AACQvgAAAAAAAIA+AACQvgAAAAAAAOA+AACQvgAAAD4AAIA+AAAAvQAAAD4AAIA+AACQvgAAAD4AAOA+AAAAvQAAAD4AAOA+AACQvgAAAAAAAOA+AAAAvQAAAAAAAOA+AACQvgAAAAAAAIA+AAAAvQAAAAAAAIA+AACQvgAAAD4AAOA+AAAAvQAAAD4AAOA+AACQvgAAAAAAAOA+AAAAvQAAAAAAAOA+AAAAvQAAAD4AAIA+AACQvgAAAD4AAIA+AAAAvQAAAAAAAIA+AACQvgAAAAAAAIA+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAOgAgAD8AAL89ACAAPwAAADoA4A8/AAC/PQDgDz8AgGA+ACAAPwDAnz4AIAA/AIBgPgDgDz8AwJ8+AOAPPwCAXz4AwP8+AADBPQDA/z4AgF8+AEDQPgAAwT0AQNA+AMCvPgBA0D4AgGA+AEDQPgDArz4AwP8+AIBgPgDA/z4AQKA+ACAAPwDA3z4AIAA/AECgPgDgDz8AwN8+AOAPPwAAwT0AIAA/AIBfPgAgAD8AAME9AOAPPwCAXz4A4A8/AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAADAvQAAwD0AABA/AADAvQAAwD0AAKA+AADAvQAAAD0AABA/AADAvQAAAD0AAKA+AABgvgAAwD0AAKA+AABgvgAAwD0AABA/AABgvgAAAD0AAKA+AABgvgAAAD0AABA/AABgvgAAwD0AAKA+AADAvQAAwD0AAKA+AABgvgAAwD0AABA/AADAvQAAwD0AABA/AABgvgAAAD0AABA/AADAvQAAAD0AABA/AABgvgAAAD0AAKA+AADAvQAAAD0AAKA+AABgvgAAwD0AABA/AADAvQAAwD0AABA/AABgvgAAAD0AABA/AADAvQAAAD0AABA/AADAvQAAwD0AAKA+AABgvgAAwD0AAKA+AADAvQAAAD0AAKA+AABgvgAAAD0AAKA+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AEDwPgCAAD4A4Bc/AIAAPgBA8D4AgB8+AOAXPwCAHz4AICg/AIAAPgDgRz8AgAA+ACAoPwCAHz4A4Ec/AIAfPgDgJz8AAP89ACAYPwAA/z0A4Cc/AAAAOgAgGD8AAAA6AOA3PwAAADoAICg/AAAAOgDgNz8AAP89ACAoPwAA/z0AIEg/AIAAPgDgVz8AgAA+ACBIPwCAHz4A4Fc/AIAfPgAgGD8AgAA+AOAnPwCAAD4AIBg/AIAfPgDgJz8AgB8+AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAz/fPvgAAQD4AAMA+z/fPvgAAQD4AAIA+z/fPvgAAgD0AAMA+z/fPvgAAgD0AAIA+AADQvgAAQD4AAIA+AADQvgAAQD4AAMA+AADQvgAAgD0AAIA+AADQvgAAgD0AAMA+AADQvgAAQD4AAIA+z/fPvgAAQD4AAIA+AADQvgAAQD4AAMA+z/fPvgAAQD4AAMA+AADQvgAAgD0AAMA+z/fPvgAAgD0AAMA+AADQvgAAgD0AAIA+z/fPvgAAgD0AAIA+AADQvgAAQD4AAMA+z/fPvgAAQD4AAMA+AADQvgAAgD0AAMA+z/fPvgAAgD0AAMA+z/fPvgAAQD4AAIA+AADQvgAAQD4AAIA+z/fPvgAAgD0AAIA+AADQvgAAgD0AAIA+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAOgAAgT0AAH49AACBPQAAADoAAP89AAB+PQAA/z0AAIE9AACBPQAA/z0AAIE9AACBPQAA/z0AAP89AAD/PQAAgT0AAH49AAB+PQAAfj0AAIE9AAAAOgAAfj0AAAA6AACBPQAAADoAAH49AAAAOgAAgT0AAH49AAB+PQAAfj0AgAA+AACBPQAA/z0AAIE9AIAAPgAA/z0AAP89AAD/PQAAgT0AAIE9AAB+PQAAgT0AAIE9AAD/PQAAfj0AAP89AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAACAPgAAwD0AAMC9AACAPgAAwD0AACC+AACAPgAAAD0AAMC9AACAPgAAAD0AACC+AAAAPgAAwD0AACC+AAAAPgAAwD0AAMC9AAAAPgAAAD0AACC+AAAAPgAAAD0AAMC9AAAAPgAAwD0AACC+AACAPgAAwD0AACC+AAAAPgAAwD0AAMC9AACAPgAAwD0AAMC9AAAAPgAAAD0AAMC9AACAPgAAAD0AAMC9AAAAPgAAAD0AACC+AACAPgAAAD0AACC+AAAAPgAAwD0AAMC9AACAPgAAwD0AAMC9AAAAPgAAAD0AAMC9AACAPgAAAD0AAMC9AACAPgAAwD0AACC+AAAAPgAAwD0AACC+AACAPgAAAD0AACC+AAAAPgAAAD0AACC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/ACAIPwBAkD4A4A8/AECQPgAgCD8AwJ8+AOAPPwDAnz4AICA/AECQPgDgJz8AQJA+ACAgPwDAnz4A4Cc/AMCfPgDgHz8AwI8+ACAQPwDAjz4A4B8/AECAPgAgED8AQIA+AOAvPwBAgD4AICA/AECAPgDgLz8AwI8+ACAgPwDAjz4AICg/AECQPgDgNz8AQJA+ACAoPwDAnz4A4Dc/AMCfPgAgED8AQJA+AOAfPwBAkD4AIBA/AMCfPgDgHz8AwJ8+AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAACQPgAAwD0AAMC9AACQPgAAwD0AACC+AACQPgAAAD0AAMC9AACQPgAAAD0AACC+AABgPgAAwD0AACC+AABgPgAAwD0AAMC9AABgPgAAAD0AACC+AABgPgAAAD0AAMC9AABgPgAAwD0AACC+AACQPgAAwD0AACC+AABgPgAAwD0AAMC9AACQPgAAwD0AAMC9AABgPgAAAD0AAMC9AACQPgAAAD0AAMC9AABgPgAAAD0AACC+AACQPgAAAD0AACC+AABgPgAAwD0AAMC9AACQPgAAwD0AAMC9AABgPgAAAD0AAMC9AACQPgAAAD0AAMC9AACQPgAAwD0AACC+AABgPgAAwD0AACC+AACQPgAAAD0AACC+AABgPgAAAD0AACC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAOgBAoD4AAPw8AECgPgAAADoAwK8+AAD8PADArz4AAIE9AECgPgAAvz0AQKA+AACBPQDArz4AAL89AMCvPgAAfj0AwJ8+AAACPQDAnz4AAH49AECQPgAAAj0AQJA+AAC/PQBAkD4AAIE9AECQPgAAvz0AwJ8+AACBPQDAnz4AAME9AECgPgAA/z0AQKA+AADBPQDArz4AAP89AMCvPgAAAj0AQKA+AAB+PQBAoD4AAAI9AMCvPgAAfj0AwK8+AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAACwPsUggD0AAMC9AACwPsUggD0AACC+AACwPgAAgD0AAMC9AACwPgAAgD0AACC+AACQPsUggD0AACC+AACQPsUggD0AAMC9AACQPgAAgD0AACC+AACQPgAAgD0AAMC9AACQPsUggD0AACC+AACwPsUggD0AACC+AACQPsUggD0AAMC9AACwPsUggD0AAMC9AACQPgAAgD0AAMC9AACwPgAAgD0AAMC9AACQPgAAgD0AACC+AACwPgAAgD0AACC+AACQPsUggD0AAMC9AACwPsUggD0AAMC9AACQPgAAgD0AAMC9AACwPgAAgD0AAMC9AACwPsUggD0AACC+AACQPsUggD0AACC+AACwPgAAgD0AACC+AACQPgAAgD0AACC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAOgCAID4AAPw8AIAgPgAAADoAgB8+AAD8PACAHz4AAIE9AIAgPgAAvz0AgCA+AACBPQCAHz4AAL89AIAfPgAAfj0AgB8+AAACPQCAHz4AAH49AIAAPgAAAj0AgAA+AAC/PQCAAD4AAIE9AIAAPgAAvz0AgB8+AACBPQCAHz4AAME9AIAgPgAA/z0AgCA+AADBPQCAHz4AAP89AIAfPgAAAj0AgCA+AAB+PQCAID4AAAI9AIAfPgAAfj0AgB8+AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAAAAvgAAwD0AAMC9AAAAvgAAwD0AACC+AAAAvgAAAD0AAMC9AAAAvgAAAD0AACC+AACAvgAAwD0AACC+AACAvgAAwD0AAMC9AACAvgAAAD0AACC+AACAvgAAAD0AAMC9AACAvgAAwD0AACC+AAAAvgAAwD0AACC+AACAvgAAwD0AAMC9AAAAvgAAwD0AAMC9AACAvgAAAD0AAMC9AAAAvgAAAD0AAMC9AACAvgAAAD0AACC+AAAAvgAAAD0AACC+AACAvgAAwD0AAMC9AAAAvgAAwD0AAMC9AACAvgAAAD0AAMC9AAAAvgAAAD0AAMC9AAAAvgAAwD0AACC+AACAvgAAwD0AACC+AAAAvgAAAD0AACC+AACAvgAAAD0AACC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AEDAPgBAgD4AwM8+AECAPgBAwD4AwI8+AMDPPgDAjz4AQPA+AECAPgDA/z4AQIA+AEDwPgDAjz4AwP8+AMCPPgDA7z4AgH8+AEDQPgCAfz4AwO8+AIBgPgBA0D4AgGA+AOAHPwCAYD4AQPA+AIBgPgDgBz8AgH8+AEDwPgCAfz4AIAA/AECAPgDgDz8AQIA+ACAAPwDAjz4A4A8/AMCPPgBA0D4AQIA+AMDvPgBAgD4AQNA+AMCPPgDA7z4AwI8+AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAABgvgAAwD0AAMC9AABgvgAAwD0AACC+AABgvgAAAD0AAMC9AABgvgAAAD0AACC+AACQvgAAwD0AACC+AACQvgAAwD0AAMC9AACQvgAAAD0AACC+AACQvgAAAD0AAMC9AACQvgAAwD0AACC+AABgvgAAwD0AACC+AACQvgAAwD0AAMC9AABgvgAAwD0AAMC9AACQvgAAAD0AAMC9AABgvgAAAD0AAMC9AACQvgAAAD0AACC+AABgvgAAAD0AACC+AACQvgAAwD0AAMC9AABgvgAAwD0AAMC9AACQvgAAAD0AAMC9AABgvgAAAD0AAMC9AABgvgAAwD0AACC+AACQvgAAwD0AACC+AABgvgAAAD0AACC+AACQvgAAAD0AACC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAOgAAAj0AAPw8AAACPQAAADoAAH49AAD8PAAAfj0AAIE9AAACPQAAvz0AAAI9AACBPQAAfj0AAL89AAB+PQAAfj0AAPw8AAACPQAA/DwAAH49AAAAOgAAAj0AAAA6AAC/PQAAADoAAIE9AAAAOgAAvz0AAPw8AACBPQAA/DwAAME9AAACPQAA/z0AAAI9AADBPQAAfj0AAP89AAB+PQAAAj0AAAI9AAB+PQAAAj0AAAI9AAB+PQAAfj0AAH49AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAACQvsUggD0AAMC9AACQvsUggD0AACC+AACQvgAAgD0AAMC9AACQvgAAgD0AACC+AACwvsUggD0AACC+AACwvsUggD0AAMC9AACwvgAAgD0AACC+AACwvgAAgD0AAMC9AACwvsUggD0AACC+AACQvsUggD0AACC+AACwvsUggD0AAMC9AACQvsUggD0AAMC9AACwvgAAgD0AAMC9AACQvgAAgD0AAMC9AACwvgAAgD0AACC+AACQvgAAgD0AACC+AACwvsUggD0AAMC9AACQvsUggD0AAMC9AACwvgAAgD0AAMC9AACQvgAAgD0AAMC9AACQvsUggD0AACC+AACwvsUggD0AACC+AACQvgAAgD0AACC+AACwvgAAgD0AACC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AACBPQAAAj0AAL89AAACPQAAgT0AAPw8AAC/PQAA/DwAgAA+AAACPQCAHz4AAAI9AIAAPgAA/DwAgB8+AAD8PAAA/z0AAPw8AADBPQAA/DwAAP89AAAAOgAAwT0AAAA6AIAfPgAAADoAgAA+AAAAOgCAHz4AAPw8AIAAPgAA/DwAgCA+AAACPQCAPz4AAAI9AIAgPgAA/DwAgD8+AAD8PAAAwT0AAAI9AAD/PQAAAj0AAME9AAD8PAAA/z0AAPw8AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAAAAAAAAwD4AAEA/AAAAAAAAAAAAAAAAAACAPxPyhT0AAAAAAAAAAK9zfz8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAwD4AAEA/AAAAAAAAAAAAAAAAAACAPwAAAADug4Q+AAAAAOpGdz8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAwD4AAEA/AAAAAAAAAAAAAAAAAACAPwAAAADug4S+AAAAAOpGdz8AAAAAIbWyvAAAAABn8H8/AAAAAAAAwD4AAEA/AAAAAAAAAAAAAAAAAACAPwAAAACoqAU+AAAAAFXPfT8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAwD4AAEA/AAAAAAAAAAAAAAAAAACAPwAAAACoqAW+AAAAAFXPfT8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAD+rqko/AAAAAAAAAAAAAAAAAACAPwAAAAAhtbI8AAAAAGfwfz8AAAAAx71QPAAAAACu+n8/AAAAAAAAAD+rqko/AAAAAAAAAAAAAAAAAACAPwAAAAAhtbK8AAAAAGfwfz8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAQD8AAMA/AAAAAAAAAAAAAAAAAACAPwAAAAC2frK9AAAAAJ4Gfz8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAQD8AAMA/AAAAAAAAAAAAAAAAAACAPwAAAAC2frI9AAAAAJ4Gfz8AAAAAAAAAAAAAAAAAAIA/AAAAAKioBb4AAAAAAAAAAFXPfT8AAIA+qKgFvgAAAAAAAAAAVc99Pw=="}],"accessors":[{"bufferView":0,"componentType":5126,"count":24,"max":[0.15625,0.125,0.25],"min":[-0.15625,0,-0.0625],"type":"VEC3"},{"bufferView":1,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":2,"componentType":5126,"count":24,"max":[0.62451171875,0.21826171875],"min":[0.00048828125,0.00048828125],"type":"VEC2"},{"bufferView":3,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":4,"componentType":5126,"count":24,"max":[0.125,0.125,-0.0625],"min":[-0.125,0,-0.3125],"type":"VEC3"},{"bufferView":5,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":6,"componentType":5126,"count":24,"max":[0.49951171875,0.40576171875],"min":[0.00048828125,0.21923828125],"type":"VEC2"},{"bufferView":7,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":8,"componentType":5126,"count":24,"max":[0.03125,0.125,0.3125],"min":[-0.03125,0,0.25],"type":"VEC3"},{"bufferView":9,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":10,"componentType":5126,"count":24,"max":[0.59326171875,0.09326171875],"min":[0.46923828125,0.00048828125],"type":"VEC2"},{"bufferView":11,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":12,"componentType":5126,"count":24,"max":[0.21875,0.125,0.1875],"min":[-0.03125,0,0],"type":"VEC3"},{"bufferView":13,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":14,"componentType":5126,"count":24,"max":[0.84326171875,0.46826171875],"min":[0.40673828125,0.31298828125],"type":"VEC2"},{"bufferView":15,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":16,"componentType":5126,"count":24,"max":[0.21875,0.09375,0.5625],"min":[0.09375,0.03125,0.3125],"type":"VEC3"},{"bufferView":17,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":18,"componentType":5126,"count":24,"max":[0.68701171875,0.62451171875],"min":[0.31298828125,0.46923828125],"type":"VEC2"},{"bufferView":19,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":20,"componentType":5126,"count":24,"max":[0.406312495470047,0.1875,0.375],"min":[0.40625,0.0625,0.25],"type":"VEC3"},{"bufferView":21,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":22,"componentType":5126,"count":24,"max":[0.12548828125,0.28076171875],"min":[0.00048828125,0.15673828125],"type":"VEC2"},{"bufferView":23,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":24,"componentType":5126,"count":24,"max":[-0.03125,0.125,0.4375],"min":[-0.28125,0,0.25],"type":"VEC3"},{"bufferView":25,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":26,"componentType":5126,"count":24,"max":[0.43701171875,0.56201171875],"min":[0.00048828125,0.40673828125],"type":"VEC2"},{"bufferView":27,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":28,"componentType":5126,"count":24,"max":[-0.09375,0.09375,0.5625],"min":[-0.21875,0.03125,0.3125],"type":"VEC3"},{"bufferView":29,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":30,"componentType":5126,"count":24,"max":[0.84326171875,0.15576171875],"min":[0.46923828125,0.00048828125],"type":"VEC2"},{"bufferView":31,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":32,"componentType":5126,"count":24,"max":[-0.406187504529953,0.1875,0.375],"min":[-0.40625,0.0625,0.25],"type":"VEC3"},{"bufferView":33,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":34,"componentType":5126,"count":24,"max":[0.12548828125,0.12451171875],"min":[0.00048828125,0.00048828125],"type":"VEC2"},{"bufferView":35,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":36,"componentType":5126,"count":24,"max":[0.25,0.09375,-0.09375],"min":[0.125,0.03125,-0.15625],"type":"VEC3"},{"bufferView":37,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":38,"componentType":5126,"count":24,"max":[0.71826171875,0.31201171875],"min":[0.53173828125,0.25048828125],"type":"VEC2"},{"bufferView":39,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":40,"componentType":5126,"count":24,"max":[0.28125,0.09375,-0.09375],"min":[0.21875,0.03125,-0.15625],"type":"VEC3"},{"bufferView":41,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":42,"componentType":5126,"count":24,"max":[0.12451171875,0.34326171875],"min":[0.00048828125,0.28173828125],"type":"VEC2"},{"bufferView":43,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":44,"componentType":5126,"count":24,"max":[0.34375,0.0625625029206276,-0.09375],"min":[0.28125,0.0625,-0.15625],"type":"VEC3"},{"bufferView":45,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":46,"componentType":5126,"count":24,"max":[0.12451171875,0.15673828125],"min":[0.00048828125,0.12548828125],"type":"VEC2"},{"bufferView":47,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":48,"componentType":5126,"count":24,"max":[-0.125,0.09375,-0.09375],"min":[-0.25,0.03125,-0.15625],"type":"VEC3"},{"bufferView":49,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":50,"componentType":5126,"count":24,"max":[0.56201171875,0.28076171875],"min":[0.37548828125,0.21923828125],"type":"VEC2"},{"bufferView":51,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":52,"componentType":5126,"count":24,"max":[-0.21875,0.09375,-0.09375],"min":[-0.28125,0.03125,-0.15625],"type":"VEC3"},{"bufferView":53,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":54,"componentType":5126,"count":24,"max":[0.12451171875,0.06201171875],"min":[0.00048828125,0.00048828125],"type":"VEC2"},{"bufferView":55,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":56,"componentType":5126,"count":24,"max":[-0.28125,0.0625625029206276,-0.09375],"min":[-0.34375,0.0625,-0.15625],"type":"VEC3"},{"bufferView":57,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":58,"componentType":5126,"count":24,"max":[0.18701171875,0.03173828125],"min":[0.06298828125,0.00048828125],"type":"VEC2"},{"bufferView":59,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":60,"componentType":5126,"count":3,"max":[0.75],"min":[0],"type":"SCALAR"},{"bufferView":61,"componentType":5126,"count":3,"max":[0.06540312618017197,0,0,1],"min":[0,0,0,0.9978589415550232],"type":"VEC4"},{"bufferView":62,"componentType":5126,"count":3,"max":[0.75],"min":[0],"type":"SCALAR"},{"bufferView":63,"componentType":5126,"count":3,"max":[0,0.258819043636322,0,1],"min":[0,0,0,0.9659258127212524],"type":"VEC4"},{"bufferView":64,"componentType":5126,"count":3,"max":[0.75],"min":[0],"type":"SCALAR"},{"bufferView":65,"componentType":5126,"count":3,"max":[0,0,0,1],"min":[0,-0.258819043636322,0,0.9659258127212524],"type":"VEC4"},{"bufferView":66,"componentType":5126,"count":3,"max":[0.75],"min":[0],"type":"SCALAR"},{"bufferView":67,"componentType":5126,"count":3,"max":[0,0.13052618503570557,0,1],"min":[0,0,0,0.9914448857307434],"type":"VEC4"},{"bufferView":68,"componentType":5126,"count":3,"max":[0.75],"min":[0],"type":"SCALAR"},{"bufferView":69,"componentType":5126,"count":3,"max":[0,0,0,1],"min":[0,-0.13052618503570557,0,0.9914448857307434],"type":"VEC4"},{"bufferView":70,"componentType":5126,"count":3,"max":[0.7916666865348816],"min":[0],"type":"SCALAR"},{"bufferView":71,"componentType":5126,"count":3,"max":[0,0.02181488461792469,0,1],"min":[0,0,0,0.9997619986534119],"type":"VEC4"},{"bufferView":72,"componentType":5126,"count":3,"max":[0.7916666865348816],"min":[0],"type":"SCALAR"},{"bufferView":73,"componentType":5126,"count":3,"max":[0,0,0,1],"min":[0,-0.02181488461792469,0,0.9997619986534119],"type":"VEC4"},{"bufferView":74,"componentType":5126,"count":3,"max":[1.5],"min":[0],"type":"SCALAR"},{"bufferView":75,"componentType":5126,"count":3,"max":[0,0,0,1],"min":[0,-0.08715574443340302,0,0.9961947202682495],"type":"VEC4"},{"bufferView":76,"componentType":5126,"count":3,"max":[1.5],"min":[0],"type":"SCALAR"},{"bufferView":77,"componentType":5126,"count":3,"max":[0,0.08715574443340302,0,1],"min":[0,0,0,0.9961947202682495],"type":"VEC4"},{"bufferView":78,"componentType":5126,"count":1,"max":[0],"min":[0],"type":"SCALAR"},{"bufferView":79,"componentType":5126,"count":1,"max":[-0.13052618503570557,0,0,0.9914448857307434],"min":[-0.13052618503570557,0,0,0.9914448857307434],"type":"VEC4"},{"bufferView":80,"componentType":5126,"count":1,"max":[0.25],"min":[0.25],"type":"SCALAR"},{"bufferView":81,"componentType":5126,"count":1,"max":[-0.13052618503570557,0,0,0.9914448857307434],"min":[-0.13052618503570557,0,0,0.9914448857307434],"type":"VEC4"}],"materials":[{"pbrMetallicRoughness":{"metallicFactor":0,"roughnessFactor":1,"baseColorTexture":{"index":0}},"alphaMode":"MASK","alphaCutoff":0.05,"doubleSided":true}],"textures":[{"sampler":0}],"samplers":[{"magFilter":9728,"minFilter":9728,"wrapS":33071,"wrapT":33071}],"meshes":[{"primitives":[{"mode":4,"attributes":{"POSITION":0,"NORMAL":1,"TEXCOORD_0":2},"indices":3,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":4,"NORMAL":5,"TEXCOORD_0":6},"indices":7,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":8,"NORMAL":9,"TEXCOORD_0":10},"indices":11,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":12,"NORMAL":13,"TEXCOORD_0":14},"indices":15,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":16,"NORMAL":17,"TEXCOORD_0":18},"indices":19,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":20,"NORMAL":21,"TEXCOORD_0":22},"indices":23,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":24,"NORMAL":25,"TEXCOORD_0":26},"indices":27,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":28,"NORMAL":29,"TEXCOORD_0":30},"indices":31,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":32,"NORMAL":33,"TEXCOORD_0":34},"indices":35,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":36,"NORMAL":37,"TEXCOORD_0":38},"indices":39,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":40,"NORMAL":41,"TEXCOORD_0":42},"indices":43,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":44,"NORMAL":45,"TEXCOORD_0":46},"indices":47,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":48,"NORMAL":49,"TEXCOORD_0":50},"indices":51,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":52,"NORMAL":53,"TEXCOORD_0":54},"indices":55,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":56,"NORMAL":57,"TEXCOORD_0":58},"indices":59,"material":0}]}],"animations":[{"name":"animation.model.walk","samplers":[{"input":60,"output":61,"interpolation":"LINEAR"},{"input":62,"output":63,"interpolation":"LINEAR"},{"input":64,"output":65,"interpolation":"LINEAR"},{"input":66,"output":67,"interpolation":"LINEAR"},{"input":68,"output":69,"interpolation":"LINEAR"}],"channels":[{"sampler":0,"target":{"node":3,"path":"rotation"}},{"sampler":1,"target":{"node":7,"path":"rotation"}},{"sampler":2,"target":{"node":11,"path":"rotation"}},{"sampler":3,"target":{"node":15,"path":"rotation"}},{"sampler":4,"target":{"node":19,"path":"rotation"}}]},{"name":"animation.model.idle","samplers":[{"input":70,"output":71,"interpolation":"LINEAR"},{"input":72,"output":73,"interpolation":"LINEAR"},{"input":74,"output":75,"interpolation":"LINEAR"},{"input":76,"output":77,"interpolation":"LINEAR"}],"channels":[{"sampler":0,"target":{"node":7,"path":"rotation"}},{"sampler":1,"target":{"node":11,"path":"rotation"}},{"sampler":2,"target":{"node":15,"path":"rotation"}},{"sampler":3,"target":{"node":19,"path":"rotation"}}]},{"name":"animation.model.back","samplers":[{"input":78,"output":79,"interpolation":"LINEAR"},{"input":80,"output":81,"interpolation":"LINEAR"}],"channels":[{"sampler":0,"target":{"node":15,"path":"rotation"}},{"sampler":1,"target":{"node":19,"path":"rotation"}}]}]} diff --git a/games/devtest/mods/gltf/models/gltf_minimal_triangle.gltf b/games/devtest/mods/gltf/models/gltf_minimal_triangle.gltf new file mode 100644 index 000000000..9a624f085 --- /dev/null +++ b/games/devtest/mods/gltf/models/gltf_minimal_triangle.gltf @@ -0,0 +1 @@ +{"scene":0,"scenes":[{"nodes":[0]}],"nodes":[{"mesh":0}],"meshes":[{"primitives":[{"attributes":{"POSITION":1},"indices":0}]}],"buffers":[{"uri":"data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=","byteLength":44}],"bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":6,"target":34963},{"buffer":0,"byteOffset":8,"byteLength":36,"target":34962}],"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5123,"count":3,"type":"SCALAR","max":[2],"min":[0]},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":3,"type":"VEC3","max":[1,1,0],"min":[0,0,0]}],"asset":{"version":"2.0"}} diff --git a/games/devtest/mods/gltf/models/gltf_simple_skin.gltf b/games/devtest/mods/gltf/models/gltf_simple_skin.gltf new file mode 100644 index 000000000..3d6c24a6c --- /dev/null +++ b/games/devtest/mods/gltf/models/gltf_simple_skin.gltf @@ -0,0 +1 @@ +{"scene":0,"scenes":[{"nodes":[0,1]}],"nodes":[{"skin":0,"mesh":0},{"children":[2]},{"translation":[0.0,1.0,0.0],"rotation":[0.0,0.0,0.0,1.0]}],"meshes":[{"primitives":[{"attributes":{"POSITION":1,"JOINTS_0":2,"WEIGHTS_0":3},"indices":0}]}],"skins":[{"inverseBindMatrices":4,"joints":[1,2]}],"animations":[{"channels":[{"sampler":0,"target":{"node":2,"path":"rotation"}}],"samplers":[{"input":5,"interpolation":"LINEAR","output":6}]}],"buffers":[{"uri":"data:application/gltf-buffer;base64,AAABAAMAAAADAAIAAgADAAUAAgAFAAQABAAFAAcABAAHAAYABgAHAAkABgAJAAgAAAAAvwAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAvwAAAD8AAAAAAAAAPwAAAD8AAAAAAAAAvwAAgD8AAAAAAAAAPwAAgD8AAAAAAAAAvwAAwD8AAAAAAAAAPwAAwD8AAAAAAAAAvwAAAEAAAAAAAAAAPwAAAEAAAAAA","byteLength":168},{"uri":"data:application/gltf-buffer;base64,AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAABAPwAAgD4AAAAAAAAAAAAAQD8AAIA+AAAAAAAAAAAAAAA/AAAAPwAAAAAAAAAAAAAAPwAAAD8AAAAAAAAAAAAAgD4AAEA/AAAAAAAAAAAAAIA+AABAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAA=","byteLength":320},{"uri":"data:application/gltf-buffer;base64,AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAgD8=","byteLength":128},{"uri":"data:application/gltf-buffer;base64,AAAAAAAAAD8AAIA/AADAPwAAAEAAACBAAABAQAAAYEAAAIBAAACQQAAAoEAAALBAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAkxjEPkSLbD8AAAAAAAAAAPT9ND/0/TQ/AAAAAAAAAAD0/TQ/9P00PwAAAAAAAAAAkxjEPkSLbD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAkxjEvkSLbD8AAAAAAAAAAPT9NL/0/TQ/AAAAAAAAAAD0/TS/9P00PwAAAAAAAAAAkxjEvkSLbD8AAAAAAAAAAAAAAAAAAIA/","byteLength":240}],"bufferViews":[{"buffer":0,"byteLength":48,"target":34963},{"buffer":0,"byteOffset":48,"byteLength":120,"target":34962},{"buffer":1,"byteLength":320,"byteStride":16},{"buffer":2,"byteLength":128},{"buffer":3,"byteLength":240}],"accessors":[{"bufferView":0,"componentType":5123,"count":24,"type":"SCALAR"},{"bufferView":1,"componentType":5126,"count":10,"type":"VEC3","max":[0.5,2.0,0.0],"min":[-0.5,0.0,0.0]},{"bufferView":2,"componentType":5123,"count":10,"type":"VEC4"},{"bufferView":2,"byteOffset":160,"componentType":5126,"count":10,"type":"VEC4"},{"bufferView":3,"componentType":5126,"count":2,"type":"MAT4"},{"bufferView":4,"componentType":5126,"count":12,"type":"SCALAR","max":[5.5],"min":[0.0]},{"bufferView":4,"byteOffset":48,"componentType":5126,"count":12,"type":"VEC4","max":[0.0,0.0,0.707,1.0],"min":[0.0,0.0,-0.707,0.707]}],"asset":{"version":"2.0"}} diff --git a/games/devtest/mods/gltf/models/gltf_simple_sparse_accessor.gltf b/games/devtest/mods/gltf/models/gltf_simple_sparse_accessor.gltf new file mode 100644 index 000000000..979896825 --- /dev/null +++ b/games/devtest/mods/gltf/models/gltf_simple_sparse_accessor.gltf @@ -0,0 +1 @@ +{"scene":0,"scenes":[{"nodes":[0]}],"nodes":[{"mesh":0}],"meshes":[{"primitives":[{"attributes":{"POSITION":1},"indices":0}]}],"buffers":[{"uri":"data:application/gltf-buffer;base64,AAAIAAcAAAABAAgAAQAJAAgAAQACAAkAAgAKAAkAAgADAAoAAwALAAoAAwAEAAsABAAMAAsABAAFAAwABQANAAwABQAGAA0AAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAQAAAAAAAAAAAAABAQAAAAAAAAAAAAACAQAAAAAAAAAAAAACgQAAAAAAAAAAAAADAQAAAAAAAAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAAAAQAAAgD8AAAAAAABAQAAAgD8AAAAAAACAQAAAgD8AAAAAAACgQAAAgD8AAAAAAADAQAAAgD8AAAAACAAKAAwAAAAAAIA/AAAAQAAAAAAAAEBAAABAQAAAAAAAAKBAAACAQAAAAAA=","byteLength":284}],"bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":72,"byteLength":168},{"buffer":0,"byteOffset":240,"byteLength":6},{"buffer":0,"byteOffset":248,"byteLength":36}],"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5123,"count":36,"type":"SCALAR","max":[13],"min":[0]},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":14,"type":"VEC3","max":[6,4,0],"min":[0,0,0],"sparse":{"count":3,"indices":{"bufferView":2,"byteOffset":0,"componentType":5123},"values":{"bufferView":3,"byteOffset":0}}}],"asset":{"version":"2.0"}} diff --git a/games/devtest/mods/gltf/models/gltf_snow_man.gltf b/games/devtest/mods/gltf/models/gltf_snow_man.gltf new file mode 100644 index 000000000..cd8c347d2 --- /dev/null +++ b/games/devtest/mods/gltf/models/gltf_snow_man.gltf @@ -0,0 +1 @@ +{"asset":{"version":"2.0","generator":"Blockbench 4.6.0 glTF exporter"},"scenes":[{"nodes":[3],"name":"blockbench_export"}],"scene":0,"nodes":[{"name":"cube","mesh":0},{"name":"cube","mesh":1},{"name":"cube","mesh":2},{"children":[0,1,2]}],"bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":288,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":576,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":768,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":840,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":1128,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":1416,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":1608,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":1680,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":1968,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":2256,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":2448,"byteLength":72,"target":34963}],"buffers":[{"byteLength":2520,"uri":"data:application/octet-stream;base64,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"}],"accessors":[{"bufferView":0,"componentType":5126,"count":24,"max":[3,24,3],"min":[-3,18,-3],"type":"VEC3"},{"bufferView":1,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":2,"componentType":5126,"count":24,"max":[0.8333333134651184,1],"min":[0,0.25],"type":"VEC2"},{"bufferView":3,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":4,"componentType":5126,"count":24,"max":[5,10,5],"min":[-5,0,-5],"type":"VEC3"},{"bufferView":5,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":6,"componentType":5126,"count":24,"max":[0.625,1],"min":[0,0.375],"type":"VEC2"},{"bufferView":7,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":8,"componentType":5126,"count":24,"max":[4,18,4],"min":[-4,10,-4],"type":"VEC3"},{"bufferView":9,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":10,"componentType":5126,"count":24,"max":[0.7083333134651184,0.7916666865348816],"min":[0.2083333283662796,0.25],"type":"VEC2"},{"bufferView":11,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"}],"materials":[{"pbrMetallicRoughness":{"metallicFactor":0,"roughnessFactor":1,"baseColorTexture":{"index":0,"texCoord":0}},"alphaMode":"MASK","alphaCutoff":0.05,"doubleSided":true}],"textures":[{}],"meshes":[{"primitives":[{"mode":4,"attributes":{"POSITION":0,"NORMAL":1,"TEXCOORD_0":2},"indices":3,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":4,"NORMAL":5,"TEXCOORD_0":6},"indices":7,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":8,"NORMAL":9,"TEXCOORD_0":10},"indices":11,"material":0}]}]} diff --git a/games/devtest/mods/gltf/models/gltf_spider.gltf b/games/devtest/mods/gltf/models/gltf_spider.gltf new file mode 100644 index 000000000..6698b6bb4 --- /dev/null +++ b/games/devtest/mods/gltf/models/gltf_spider.gltf @@ -0,0 +1 @@ +{"asset":{"generator":"Khronos glTF Blender I/O v1.7.33","version":"2.0"},"scene":0,"scenes":[{"name":"Scene","nodes":[0]}],"nodes":[{"mesh":0,"name":"Spider"}],"materials":[{"doubleSided":true,"name":"Material.001","pbrMetallicRoughness":{}}],"meshes":[{"name":"Cube","primitives":[{"attributes":{"POSITION":0,"NORMAL":1,"TEXCOORD_0":2},"indices":3,"material":0}]}],"accessors":[{"bufferView":0,"componentType":5126,"count":1000,"max":[2.742279291152954,1.4045029878616333,2.0192716121673584],"min":[-2.742279291152954,-0.6434623599052429,-3.534085512161255],"type":"VEC3"},{"bufferView":1,"componentType":5126,"count":1000,"type":"VEC3"},{"bufferView":2,"componentType":5126,"count":1000,"type":"VEC2"},{"bufferView":3,"componentType":5123,"count":1500,"type":"SCALAR"}],"bufferViews":[{"buffer":0,"byteLength":12000,"byteOffset":0},{"buffer":0,"byteLength":12000,"byteOffset":12000},{"buffer":0,"byteLength":8000,"byteOffset":24000},{"buffer":0,"byteLength":3000,"byteOffset":32000}],"buffers":[{"byteLength":35000,"uri":"data:application/octet-stream;base64,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dQCGAHUAgACIAIsAkQCIAJEAjgCQAJIAnQCQAJ0AmwCcAJ8AmQCcAJkAlgCVAJcAjACVAIwAigCPAJoAlACPAJQAiQCeAJMAjQCeAI0AmACiAKgAqwCiAKsApQCnALQAtwCnALcAqgCyAKwArwCyAK8AtQCtAKAAowCtAKMAsACmAKEArgCmAK4AswC2ALEApAC2AKQAqQC6AMAAwwC6AMMAvQC/AMwAzgC/AM4AwQDKAMQAxwDKAMcAzQDGALkAuwDGALsAyAC+ALgAxQC+AMUAywDPAMkAvADPALwAwgDSANgA2wDSANsA1QDXAOQA5gDXAOYA2QDiANwA3wDiAN8A5QDeANEA0wDeANMA4ADWANAA3QDWAN0A4wDnAOEA1ADnANQA2gDpAOsA8QDpAPEA7wDwAPIA/QDwAP0A+wD8AP4A+AD8APgA9gD1APcA7AD1AOwA6gDuAPoA9ADuAPQA6AD/APMA7QD/AO0A+QAAAQMBCQEAAQkBBgEHAQoBFQEHARUBEgEUARcBEQEUAREBDgENARABBQENAQUBAgEIARMBDAEIAQwBAQEWAQsBBAEWAQQBDwEpAR0BIwEpASMBLwEoASYBGQEoARkBGwEtASsBJQEtASUBJwEhAR8BLAEhASwBLgEYASQBKgEYASoBHgEcARoBIAEcASABIgExATMBOgExAToBOAE3ATkBRQE3AUUBQwFEAUYBPwFEAT8BPQE+AUABNAE+ATQBMgE2AUIBPAE2ATwBMAFHATsBNQFHATUBQQFJAUsBUQFJAVEBTwFQAVIBXQFQAV0BWwFcAV4BWAFcAVgBVgFVAVcBTAFVAUwBSgFOAVoBVAFOAVQBSAFfAVMBTQFfAU0BWQFgAWMBaQFgAWkBZgFoAWoBdQFoAXUBcwF0AXcBcQF0AXEBbgFtAW8BZAFtAWQBYgFnAXIBbAFnAWwBYQF2AWsBZQF2AWUBcAGJAX0BgwGJAYMBjwGIAYYBeQGIAXkBewGNAYsBhQGNAYUBhwGBAX8BjAGBAYwBjgF4AYQBigF4AYoBfgF8AXoBgAF8AYABggGRAZMBmQGRAZkBlwGYAZoBpQGYAaUBowGkAaYBoAGkAaABngGdAZ8BlAGdAZQBkgGWAaIBnAGWAZwBkAGnAZsBlQGnAZUBoQGpAasBsQGpAbEBrwGwAbIBvQGwAb0BuwG8Ab4BuAG8AbgBtgG1AbcBrAG1AawBqgGuAboBtAGuAbQBqAG/AbMBrQG/Aa0BuQHAAcMByQHAAckBxgHIAcoB1QHIAdUB0wHUAdcB0QHUAdEBzgHNAc8BxAHNAcQBwgHHAdIBzAHHAcwBwQHWAcsBxQHWAcUB0AHpAd0B4wHpAeMB7wHoAeYB2QHoAdkB2wHtAesB5QHtAeUB5wHhAd8B7AHhAewB7gHYAeQB6gHYAeoB3gHcAdoB4AHcAeAB4gHxAfMB+QHxAfkB9wH4AfoBBQL4AQUCAwIEAgYCAAIEAgAC/gH9Af8B9AH9AfQB8gH2AQIC/AH2AfwB8AEHAvsB9QEHAvUBAQIJAgsCEQIJAhECDwIQAhICHQIQAh0CGwIcAh4CGAIcAhgCFgIVAhcCDAIVAgwCCgIOAhoCFAIOAhQCCAIfAhMCDQIfAg0CGQIgAiMCKQIgAikCJgIoAisCNgIoAjYCMwI0AjcCMQI0AjECLgIsAi8CJAIsAiQCIQInAjICLQInAi0CIgI1AioCJQI1AiUCMAJJAj0CQwJJAkMCTwJIAkYCOQJIAjkCOwJNAksCRQJNAkUCRwJBAj8CTAJBAkwCTgI4AkQCSgI4AkoCPgI8AjoCQAI8AkACQgJSAlQCWQJSAlkCVwJYAloCZgJYAmYCZAJjAmUCYAJjAmACXgJdAl8CUwJdAlMCUQJWAmICXAJWAlwCUAJnAlsCVQJnAlUCYQJpAm8CcwJpAnMCbQJuAnsCfwJuAn8CcgJ6AnQCeAJ6AngCfgJ1AmgCbAJ1AmwCeQJwAmoCdgJwAnYCfAJ9AncCawJ9AmsCcQKCAogCiwKCAosChQKHApQClwKHApcCigKSAowCjwKSAo8ClQKNAoACgwKNAoMCkAKGAoECjgKGAo4CkwKWApEChAKWAoQCiQKnAq0CoQKnAqECmwKoAp0CmQKoApkCpAKvAqkCpQKvAqUCqwKjAq4CqgKjAqoCnwKaAqACrAKaAqwCpgKcAqICngKcAp4CmAKxArYCugKxAroCtQK3AsMCxwK3AscCuwLCAr0CwQLCAsECxgK8ArACtAK8ArQCwAK4ArICvgK4Ar4CxALFAr8CswLFArMCuQLJAs8C0wLJAtMCzQLOAtsC3wLOAt8C0gLaAtQC2ALaAtgC3gLVAsgCzALVAswC2QLQAsoC1gLQAtYC3ALdAtcCywLdAssC0QLiAugC6wLiAusC5QLmAvMC9wLmAvcC6gLyAuwC7wLyAu8C9QLtAuAC5ALtAuQC8QLnAuEC7gLnAu4C9AL2AvAC4wL2AuMC6QIHAw0DAQMHAwED+wIIA/0C+QIIA/kCBAMPAwkDBQMPAwUDCwMDAw4DCgMDAwoD/wL6AgADDAP6AgwDBgP8AgID/gL8Av4C+AIRAxcDGwMRAxsDFQMWAyMDJwMWAycDGgMiAxwDIAMiAyADJgMdAxADFAMdAxQDIQMYAxIDHgMYAx4DJAMlAx8DEwMlAxMDGQMpAy8DMwMpAzMDLQMuAzsDPwMuAz8DMgM6AzQDOAM6AzgDPgM1AygDLAM1AywDOQMwAyoDNgMwAzYDPAM9AzcDKwM9AysDMQNCA0gDSwNCA0sDRQNGA1MDVwNGA1cDSgNSA0wDTwNSA08DVQNNA0ADRANNA0QDUQNHA0EDTgNHA04DVANWA1ADQwNWA0MDSQNnA20DYQNnA2EDWwNoA10DWQNoA1kDZANvA2kDZQNvA2UDawNjA24DagNjA2oDXwNaA2ADbANaA2wDZgNcA2IDXgNcA14DWANxA3cDewNxA3sDdQN2A4MDhwN2A4cDegOCA3wDgAOCA4ADhgN9A3ADdAN9A3QDgQN4A3IDfgN4A34DhAOFA38DcwOFA3MDeQOJA48DkwOJA5MDjQOOA5sDnwOOA58DkgOaA5QDmAOaA5gDngOVA4gDjAOVA4wDmQOQA4oDlgOQA5YDnAOdA5cDiwOdA4sDkQOiA6gDqwOiA6sDpQOmA7MDtgOmA7YDqQOyA6wDrwOyA68DtQOuA6EDpAOuA6QDsQOnA6ADrQOnA60DtAO3A7ADowO3A6MDqgPHA80DwQPHA8EDuwPIA70DuQPIA7kDxAPPA8kDxQPPA8UDywPDA84DygPDA8oDvwO6A8ADzAO6A8wDxgO8A8IDvgO8A74DuAPQA9cD2wPQA9sD1APWA+ID5gPWA+YD2gPjA9wD4APjA+AD5wPdA9ED1QPdA9UD4QPYA9ID3gPYA94D5APlA98D0wPlA9MD2QM="}]} diff --git a/games/devtest/mods/gltf/models/gltf_spider_animated.gltf b/games/devtest/mods/gltf/models/gltf_spider_animated.gltf new file mode 100644 index 000000000..79221b0c7 --- /dev/null +++ b/games/devtest/mods/gltf/models/gltf_spider_animated.gltf @@ -0,0 +1 @@ +{"asset":{"generator":"Khronos glTF Blender I/O v1.7.33","version":"2.0"},"scene":0,"scenes":[{"name":"Scene","nodes":[58]}],"nodes":[{"name":"Pincer.L","rotation":[0.03853772580623627,0.09671717882156372,0.5138389468193054,0.8515457510948181],"translation":[-2.2351741790771484e-08,0.2836739718914032,-2.2351741790771484e-08]},{"children":[0],"name":"JawBase.L","rotation":[-0.23922589421272278,-9.208349638356594e-08,-0.38811206817626953,0.8900224566459656],"scale":[1,1,0.9999999403953552],"translation":[8.097286041675034e-08,0.7702280879020691,-1.169656727029178e-07]},{"name":"Pincer.R","rotation":[0.038537755608558655,-0.09671713411808014,-0.5138388872146606,0.8515458106994629],"scale":[0.9999997615814209,0.9999999403953552,1],"translation":[2.9802322387695312e-08,0.2836737036705017,-2.9802322387695312e-08]},{"children":[2],"name":"JawBase.R","rotation":[-0.2392251342535019,1.9714243535418063e-06,0.3881126046180725,0.8900225758552551],"translation":[1.3833086898173974e-09,0.7702280282974243,-6.620245329713725e-08]},{"children":[1,3],"name":"Head","rotation":[0.4052415192127228,-3.4197712478652165e-13,-8.695541282577324e-07,0.9142096638679504],"translation":[-2.0781445141115906e-16,0.6883190274238586,-1.4901161193847656e-08]},{"children":[4],"name":"NeckBase","rotation":[-0.778048574924469,7.488795716881214e-08,1.7622618315726868e-06,0.6282041072845459],"translation":[-3.399441372928941e-14,0.3915250301361084,-2.3283064365386963e-09]},{"name":"Body.002","rotation":[0.17414046823978424,0,-4.151832513343834e-07,0.9847208261489868],"translation":[5.897654597759178e-14,1.0079095363616943,-1.2134763416327132e-08]},{"children":[6],"name":"Body.001","rotation":[0.6673352122306824,-7.632472941426077e-14,-1.5910509318928234e-06,0.7447575330734253],"scale":[1,0.9999999403953552,0.9999999403953552],"translation":[-3.962037268363458e-15,0.3915250301361084,-3.1428597502269895e-09]},{"name":"Leg4Fore.L","rotation":[-0.021953541785478592,0.030033688992261887,-0.4378480017185211,0.8982790112495422],"scale":[1,0.9999998211860657,1.0000001192092896],"translation":[1.9202238377147296e-07,0.8228543996810913,-1.749940707895803e-07]},{"children":[8],"name":"Leg4Lower.L","rotation":[-0.11090508848428726,0.11991499364376068,-0.48737218976020813,0.8577813506126404],"scale":[0.9999995231628418,0.9999997615814209,1.000000238418579],"translation":[1.9631791303709178e-07,0.8208085298538208,-4.769351491518137e-08]},{"children":[9],"name":"Leg4Mid.L","rotation":[-0.21032677590847015,0.09273893386125565,-0.42330121994018555,0.8763437271118164],"scale":[1,0.9999996423721313,0.999999463558197],"translation":[-1.720833893159579e-07,1.5146127939224243,1.4611718768264836e-07]},{"children":[10],"name":"Leg4Upper.L","rotation":[0.581340491771698,-0.03387186676263809,0.4926694631576538,0.6466628909111023],"scale":[1.0000003576278687,1.000000238418579,1.0000003576278687],"translation":[-2.6137989550534257e-09,0.8996680974960327,-2.8558396536482178e-08]},{"children":[11],"name":"Leg4Base.L","rotation":[0.4988132119178772,0.67340087890625,0.0026363276410847902,0.5456278324127197],"scale":[0.9999998807907104,0.9999998807907104,0.9999999403953552],"translation":[6.932457941033476e-10,0.3915250301361084,-7.783789612858527e-09]},{"name":"Leg3Fore.L","rotation":[-0.040254246443510056,0.0051941643469035625,-0.3734953701496124,0.9267436861991882],"scale":[1.0000001192092896,1,1.0000003576278687],"translation":[-7.186849302343035e-07,0.761729896068573,1.4940267689667053e-08]},{"children":[13],"name":"Leg3Lower.L","rotation":[-0.02293548174202442,0.03108014352619648,-0.5376279950141907,0.8422969579696655],"scale":[1,0.9999998807907104,1],"translation":[2.6879865799855907e-07,0.890315592288971,-2.3254589365251377e-08]},{"children":[14],"name":"Leg3Mid.L","rotation":[-0.10393687337636948,0.026799378916621208,-0.47722530364990234,0.8722012042999268],"scale":[1,1.0000001192092896,1],"translation":[-4.5783519908582093e-07,1.5058820247650146,6.428529530921878e-08]},{"children":[15],"name":"Leg3Upper.L","rotation":[0.22388437390327454,0.00046301534166559577,0.7424523234367371,0.6313795447349548],"scale":[0.9999996423721313,0.9999999403953552,0.9999995231628418],"translation":[8.173065424443848e-08,0.8363674879074097,-3.891337030381692e-09]},{"children":[16],"name":"Leg3Base.L","rotation":[0.48101410269737244,0.8565962910652161,-0.006458853371441364,0.18661867082118988],"scale":[0.9999999403953552,0.9999998807907104,0.9999999403953552],"translation":[1.1383551878907383e-08,0.3915250301361084,2.5693926986036786e-09]},{"name":"Leg2Fore.L","rotation":[0.04987334460020065,-0.01207074522972107,-0.4028705060482025,0.9138174653053284],"scale":[0.999999463558197,1.0000004768371582,1.0000001192092896],"translation":[2.9161077463868423e-07,0.7777483463287354,1.7455030842938868e-07]},{"children":[18],"name":"Leg2Lower.L","rotation":[0.08352424204349518,-0.04269447177648544,-0.518085777759552,0.8501694202423096],"scale":[1.0000001192092896,1.0000004768371582,1.0000004768371582],"translation":[-1.5067358560827415e-07,0.9397417306900024,-4.163759115272114e-08]},{"children":[19],"name":"Leg2Mid.L","rotation":[0.14706559479236603,-0.028868243098258972,-0.47296836972236633,0.868239164352417],"scale":[1.0000004768371582,0.9999999403953552,0.9999999403953552],"translation":[-2.2217867012841452e-07,1.5058820247650146,-6.989571943449846e-08]},{"children":[20],"name":"Leg2Upper.L","rotation":[-0.42424774169921875,-0.0005238422891125083,0.6472405791282654,0.6333192586898804],"scale":[1.000000238418579,1.0000003576278687,1.0000005960464478],"translation":[9.311328597050306e-08,0.881853461265564,-1.8038990745594674e-08]},{"children":[21],"name":"Leg2Base.L","rotation":[-0.4972459375858307,-0.7882749438285828,0.006057440303266048,0.362398236989975],"scale":[0.9999997615814209,0.9999998807907104,0.9999999403953552],"translation":[7.375939858889069e-10,0.3915250301361084,4.028271050060539e-09]},{"name":"Leg1Fore.L","rotation":[-0.01934647001326084,-0.04218549281358719,-0.4403696358203888,0.8966162800788879],"scale":[1,1,0.9999997615814209],"translation":[2.0805721590022586e-07,0.815664529800415,4.0515438115562574e-08]},{"children":[23],"name":"Leg1Lower.L","rotation":[0.15678077936172485,-0.1661715805530548,-0.47995010018348694,0.8470270037651062],"scale":[0.999999463558197,1,0.9999999403953552],"translation":[3.670676562705921e-08,0.8788074851036072,8.29251618483795e-08]},{"children":[24],"name":"Leg1Mid.L","rotation":[0.26206591725349426,-0.11672191321849823,-0.4046621024608612,0.8683006763458252],"scale":[1.0000001192092896,0.9999999403953552,0.9999995231628418],"translation":[3.601947184961318e-08,1.5125981569290161,-1.6144279868512967e-07]},{"children":[25],"name":"Leg1Upper.L","rotation":[-0.62815922498703,0.04343283176422119,0.39305803179740906,0.6701006889343262],"translation":[-1.0171092412747385e-07,1.043814778327942,1.114601104745816e-07]},{"children":[26],"name":"Leg1Base.L","rotation":[-0.536352813243866,-0.596045732498169,-0.006935927551239729,0.5975006818771362],"scale":[0.9999998211860657,0.9999998211860657,1],"translation":[7.451212979958655e-09,0.3915250301361084,-5.977072614626877e-09]},{"name":"Leg4Fore.R","rotation":[-0.0219536405056715,-0.030033595860004425,0.43784812092781067,0.8982789516448975],"scale":[1.000000238418579,0.9999998807907104,1.0000001192092896],"translation":[4.575199454848189e-07,0.82285475730896,1.3987688873839943e-07]},{"children":[28],"name":"Leg4Lower.R","rotation":[-0.11090517044067383,-0.11991491913795471,0.48737218976020813,0.8577813506126404],"scale":[1.0000001192092896,0.9999999403953552,1.0000001192092896],"translation":[5.0247152216797986e-08,0.8208085894584656,1.2523592829438712e-07]},{"children":[29],"name":"Leg4Mid.R","rotation":[-0.21032673120498657,-0.09273889660835266,0.42330119013786316,0.876343846321106],"scale":[0.9999998211860657,0.9999995231628418,1.0000001192092896],"translation":[-1.2884336797469587e-07,1.514613151550293,6.563716681284859e-08]},{"children":[30],"name":"Leg4Upper.R","rotation":[0.5813404321670532,0.03387187048792839,-0.4926694333553314,0.6466629505157471],"scale":[1,1.000000238418579,0.9999997019767761],"translation":[-3.940737158814045e-08,0.8996680974960327,1.9567494291550247e-09]},{"children":[31],"name":"Leg4Base.R","rotation":[0.4988132119178772,-0.6733996272087097,-0.0026374668814241886,0.5456294417381287],"scale":[1,1.0000001192092896,1],"translation":[-1.1682686817948706e-08,0.3915250301361084,-1.3812247345867945e-08]},{"name":"Leg3Fore.R","rotation":[-0.04025428742170334,-0.005194155499339104,0.3734953999519348,0.9267436861991882],"scale":[0.9999998211860657,1.0000001192092896,1.0000001192092896],"translation":[-7.285660217348777e-07,0.7617300748825073,-4.0205627271916455e-08]},{"children":[33],"name":"Leg3Lower.R","rotation":[-0.02293553575873375,-0.03108006715774536,0.5376282930374146,0.8422967791557312],"scale":[1.0000001192092896,0.9999996423721313,0.9999999403953552],"translation":[7.143101754536474e-08,0.8903149366378784,6.888667769544554e-08]},{"children":[34],"name":"Leg3Mid.R","rotation":[-0.10393673926591873,-0.026799339801073074,0.47722548246383667,0.872201144695282],"scale":[1.0000003576278687,0.9999998807907104,0.9999998807907104],"translation":[1.4287303429227904e-07,1.5058823823928833,9.578651827268914e-08]},{"children":[35],"name":"Leg3Upper.R","rotation":[0.2238844484090805,-0.00046323961578309536,-0.7424524426460266,0.6313793659210205],"scale":[1.0000001192092896,1.0000005960464478,0.9999997615814209],"translation":[-2.9145089897042453e-08,0.8363675475120544,-1.3412945421009681e-08]},{"children":[36],"name":"Leg3Base.R","rotation":[0.48101410269737244,-0.8565958738327026,0.006457682233303785,0.18662074208259583],"scale":[0.9999999403953552,1.0000001192092896,1],"translation":[1.187698939197901e-09,0.3915250301361084,1.396204218906405e-08]},{"name":"Leg2Fore.R","rotation":[0.04987342655658722,0.012070796452462673,0.40287071466445923,0.9138173460960388],"scale":[0.9999997615814209,0.9999998807907104,0.9999997019767761],"translation":[4.900767294202524e-07,0.7777489423751831,1.3496240569565998e-07]},{"children":[38],"name":"Leg2Lower.R","rotation":[0.08352430164813995,0.04269447922706604,0.518085777759552,0.8501694202423096],"scale":[1.000000238418579,1.0000003576278687,0.9999999403953552],"translation":[1.2208448652017978e-07,0.9397414326667786,-3.409446946989192e-08]},{"children":[39],"name":"Leg2Mid.R","rotation":[0.1470656394958496,0.028868237510323524,0.4729681611061096,0.8682392835617065],"scale":[1.0000001192092896,1.0000003576278687,1.0000001192092896],"translation":[4.8437236443987786e-08,1.5058820247650146,-2.5024842642551448e-08]},{"children":[40],"name":"Leg2Upper.R","rotation":[-0.4242475926876068,0.0005238187150098383,-0.6472404599189758,0.6333194971084595],"scale":[0.9999997019767761,1,0.9999998211860657],"translation":[3.550610472302651e-09,0.8818532824516296,4.425183419698442e-08]},{"children":[41],"name":"Leg2Base.R","rotation":[-0.4972459375858307,0.7882757782936096,-0.006056289654225111,0.36239632964134216],"scale":[0.9999998211860657,1,0.9999999403953552],"translation":[-7.2600920830723226e-09,0.3915250301361084,-5.773719280455225e-09]},{"name":"Leg1Fore.R","rotation":[-0.015208502300083637,0.04422945901751518,0.4362727701663971,0.8985980749130249],"scale":[1.000000238418579,0.9999995827674866,0.9999997615814209],"translation":[-6.20622927272052e-07,0.8156638741493225,-1.6136721114889951e-07]},{"children":[43],"name":"Leg1Lower.R","rotation":[0.15885458886623383,0.17276015877723694,0.4745163321495056,0.848382830619812],"scale":[1.000000238418579,1.0000001192092896,1.0000004768371582],"translation":[-2.3015780925561558e-07,0.8788077235221863,2.258973452740065e-08]},{"children":[44],"name":"Leg1Mid.R","rotation":[0.2600231170654297,0.12465617805719376,0.4028773903846741,0.8686419725418091],"scale":[1,0.9999997019767761,0.9999999403953552],"translation":[-2.3629894485566183e-08,1.512597680091858,-5.442473494099431e-08]},{"children":[45],"name":"Leg1Upper.R","rotation":[-0.6237055063247681,-0.03962605446577072,-0.3963613212108612,0.6725466251373291],"scale":[1,1,0.9999995827674866],"translation":[4.151442212219081e-08,1.0438144207000732,6.221015524943141e-08]},{"children":[46],"name":"Leg1Base.R","rotation":[-0.5363527536392212,0.5960471630096436,0.0069372160360217094,0.5974993705749512],"scale":[1,1.0000001192092896,1.0000001192092896],"translation":[7.877114072130098e-09,0.3915250301361084,-5.523408841412447e-09]},{"children":[5,7,12,17,22,27,32,37,42,47],"name":"Body","rotation":[-0.9999927282333374,-4.546671483751652e-09,1.1920842553081457e-06,0.003814017167314887],"translation":[-2.1589291564903364e-17,0.5146726369857788,0.22900062799453735]},{"name":"Leg4IK.L","rotation":[-2.6692541510442425e-08,-2.6692541510442425e-08,-0.7071068286895752,0.7071068286895752],"translation":[2.2291481494903564,-0.5599625110626221,-0.7613579630851746]},{"name":"Leg3IK.L","rotation":[-2.6692541510442425e-08,-2.6692541510442425e-08,-0.7071068286895752,0.7071068286895752],"translation":[2.3687760829925537,-0.5599625110626221,-0.033313095569610596]},{"name":"Leg2IK.L","rotation":[-2.6692541510442425e-08,-2.6692541510442425e-08,-0.7071068286895752,0.7071068286895752],"translation":[2.3687760829925537,-0.5599625110626221,0.6964529752731323]},{"name":"Leg1IK.L","rotation":[-2.6692541510442425e-08,-2.6692541510442425e-08,-0.7071068286895752,0.7071068286895752],"translation":[2.2556710243225098,-0.5599625110626221,1.4977319240570068]},{"name":"Leg4IK.R","rotation":[-2.6692541510442425e-08,2.6692541510442425e-08,0.7071068286895752,0.7071068286895752],"translation":[-2.2291481494903564,-0.5599625110626221,-0.7613579630851746]},{"name":"Leg3IK.R","rotation":[-2.6692541510442425e-08,2.6692541510442425e-08,0.7071068286895752,0.7071068286895752],"translation":[-2.3687760829925537,-0.5599625110626221,-0.033313095569610596]},{"name":"Leg2IK.R","rotation":[-2.6692541510442425e-08,2.6692541510442425e-08,0.7071068286895752,0.7071068286895752],"translation":[-2.3687760829925537,-0.5599625110626221,0.6964529752731323]},{"name":"Leg1IK.R","rotation":[-2.6692541510442425e-08,2.6692541510442425e-08,0.7071068286895752,0.7071068286895752],"translation":[-2.2556710243225098,-0.5599625110626221,1.5977319478988647]},{"mesh":0,"name":"Spider","skin":0},{"children":[57,48,49,50,51,52,53,54,55,56],"name":"Armature"}],"animations":[{"channels":[{"sampler":0,"target":{"node":48,"path":"translation"}},{"sampler":1,"target":{"node":48,"path":"rotation"}},{"sampler":2,"target":{"node":48,"path":"scale"}},{"sampler":3,"target":{"node":4,"path":"translation"}},{"sampler":4,"target":{"node":4,"path":"rotation"}},{"sampler":5,"target":{"node":4,"path":"scale"}},{"sampler":6,"target":{"node":0,"path":"translation"}},{"sampler":7,"target":{"node":0,"path":"rotation"}},{"sampler":8,"target":{"node":0,"path":"scale"}},{"sampler":9,"target":{"node":2,"path":"translation"}},{"sampler":10,"target":{"node":2,"path":"rotation"}},{"sampler":11,"target":{"node":2,"path":"scale"}},{"sampler":12,"target":{"node":6,"path":"translation"}},{"sampler":13,"target":{"node":6,"path":"rotation"}},{"sampler":14,"target":{"node":6,"path":"scale"}},{"sampler":15,"target":{"node":11,"path":"rotation"}},{"sampler":16,"target":{"node":10,"path":"rotation"}},{"sampler":17,"target":{"node":9,"path":"rotation"}},{"sampler":18,"target":{"node":8,"path":"rotation"}},{"sampler":19,"target":{"node":16,"path":"rotation"}},{"sampler":20,"target":{"node":15,"path":"rotation"}},{"sampler":21,"target":{"node":14,"path":"rotation"}},{"sampler":22,"target":{"node":13,"path":"rotation"}},{"sampler":23,"target":{"node":21,"path":"rotation"}},{"sampler":24,"target":{"node":20,"path":"rotation"}},{"sampler":25,"target":{"node":19,"path":"rotation"}},{"sampler":26,"target":{"node":18,"path":"rotation"}},{"sampler":27,"target":{"node":26,"path":"rotation"}},{"sampler":28,"target":{"node":25,"path":"rotation"}},{"sampler":29,"target":{"node":24,"path":"rotation"}},{"sampler":30,"target":{"node":23,"path":"translation"}},{"sampler":31,"target":{"node":23,"path":"rotation"}},{"sampler":32,"target":{"node":23,"path":"scale"}},{"sampler":33,"target":{"node":31,"path":"rotation"}},{"sampler":34,"target":{"node":30,"path":"rotation"}},{"sampler":35,"target":{"node":29,"path":"rotation"}},{"sampler":36,"target":{"node":28,"path":"rotation"}},{"sampler":37,"target":{"node":36,"path":"rotation"}},{"sampler":38,"target":{"node":35,"path":"rotation"}},{"sampler":39,"target":{"node":34,"path":"rotation"}},{"sampler":40,"target":{"node":33,"path":"rotation"}},{"sampler":41,"target":{"node":41,"path":"rotation"}},{"sampler":42,"target":{"node":40,"path":"rotation"}},{"sampler":43,"target":{"node":39,"path":"rotation"}},{"sampler":44,"target":{"node":38,"path":"rotation"}},{"sampler":45,"target":{"node":46,"path":"rotation"}},{"sampler":46,"target":{"node":45,"path":"rotation"}},{"sampler":47,"target":{"node":44,"path":"rotation"}},{"sampler":48,"target":{"node":43,"path":"rotation"}},{"sampler":49,"target":{"node":49,"path":"translation"}},{"sampler":50,"target":{"node":49,"path":"rotation"}},{"sampler":51,"target":{"node":49,"path":"scale"}},{"sampler":52,"target":{"node":50,"path":"translation"}},{"sampler":53,"target":{"node":50,"path":"rotation"}},{"sampler":54,"target":{"node":50,"path":"scale"}},{"sampler":55,"target":{"node":51,"path":"translation"}},{"sampler":56,"target":{"node":51,"path":"rotation"}},{"sampler":57,"target":{"node":51,"path":"scale"}},{"sampler":58,"target":{"node":52,"path":"translation"}},{"sampler":59,"target":{"node":52,"path":"rotation"}},{"sampler":60,"target":{"node":52,"path":"scale"}},{"sampler":61,"target":{"node":53,"path":"translation"}},{"sampler":62,"target":{"node":53,"path":"rotation"}},{"sampler":63,"target":{"node":53,"path":"scale"}},{"sampler":64,"target":{"node":54,"path":"translation"}},{"sampler":65,"target":{"node":54,"path":"rotation"}},{"sampler":66,"target":{"node":54,"path":"scale"}},{"sampler":67,"target":{"node":55,"path":"translation"}},{"sampler":68,"target":{"node":55,"path":"rotation"}},{"sampler":69,"target":{"node":55,"path":"scale"}},{"sampler":70,"target":{"node":56,"path":"translation"}},{"sampler":71,"target":{"node":56,"path":"rotation"}},{"sampler":72,"target":{"node":56,"path":"scale"}}],"name":"ArmatureAction","samplers":[{"input":7,"interpolation":"LINEAR","output":8},{"input":7,"interpolation":"LINEAR","output":9},{"input":10,"interpolation":"LINEAR","output":11},{"input":10,"interpolation":"LINEAR","output":12},{"input":7,"interpolation":"LINEAR","output":13},{"input":10,"interpolation":"LINEAR","output":14},{"input":10,"interpolation":"LINEAR","output":15},{"input":7,"interpolation":"LINEAR","output":16},{"input":10,"interpolation":"LINEAR","output":17},{"input":10,"interpolation":"LINEAR","output":18},{"input":7,"interpolation":"LINEAR","output":19},{"input":10,"interpolation":"LINEAR","output":20},{"input":10,"interpolation":"LINEAR","output":21},{"input":7,"interpolation":"LINEAR","output":22},{"input":10,"interpolation":"LINEAR","output":23},{"input":7,"interpolation":"LINEAR","output":24},{"input":7,"interpolation":"LINEAR","output":25},{"input":7,"interpolation":"LINEAR","output":26},{"input":7,"interpolation":"LINEAR","output":27},{"input":7,"interpolation":"LINEAR","output":28},{"input":7,"interpolation":"LINEAR","output":29},{"input":7,"interpolation":"LINEAR","output":30},{"input":7,"interpolation":"LINEAR","output":31},{"input":7,"interpolation":"LINEAR","output":32},{"input":7,"interpolation":"LINEAR","output":33},{"input":7,"interpolation":"LINEAR","output":34},{"input":7,"interpolation":"LINEAR","output":35},{"input":7,"interpolation":"LINEAR","output":36},{"input":7,"interpolation":"LINEAR","output":37},{"input":7,"interpolation":"LINEAR","output":38},{"input":10,"interpolation":"LINEAR","output":39},{"input":7,"interpolation":"LINEAR","output":40},{"input":10,"interpolation":"LINEAR","output":41},{"input":7,"interpolation":"LINEAR","output":42},{"input":7,"interpolation":"LINEAR","output":43},{"input":7,"interpolation":"LINEAR","output":44},{"input":7,"interpolation":"LINEAR","output":45},{"input":7,"interpolation":"LINEAR","output":46},{"input":7,"interpolation":"LINEAR","output":47},{"input":7,"interpolation":"LINEAR","output":48},{"input":7,"interpolation":"LINEAR","output":49},{"input":7,"interpolation":"LINEAR","output":50},{"input":7,"interpolation":"LINEAR","output":51},{"input":7,"interpolation":"LINEAR","output":52},{"input":7,"interpolation":"LINEAR","output":53},{"input":7,"interpolation":"LINEAR","output":54},{"input":7,"interpolation":"LINEAR","output":55},{"input":7,"interpolation":"LINEAR","output":56},{"input":7,"interpolation":"LINEAR","output":57},{"input":7,"interpolation":"LINEAR","output":58},{"input":10,"interpolation":"LINEAR","output":59},{"input":7,"interpolation":"LINEAR","output":60},{"input":7,"interpolation":"LINEAR","output":61},{"input":10,"interpolation":"LINEAR","output":62},{"input":7,"interpolation":"LINEAR","output":63},{"input":7,"interpolation":"LINEAR","output":64},{"input":10,"interpolation":"LINEAR","output":65},{"input":7,"interpolation":"LINEAR","output":66},{"input":7,"interpolation":"LINEAR","output":67},{"input":10,"interpolation":"LINEAR","output":68},{"input":7,"interpolation":"LINEAR","output":69},{"input":7,"interpolation":"LINEAR","output":70},{"input":10,"interpolation":"LINEAR","output":71},{"input":7,"interpolation":"LINEAR","output":72},{"input":7,"interpolation":"LINEAR","output":73},{"input":10,"interpolation":"LINEAR","output":74},{"input":7,"interpolation":"LINEAR","output":75},{"input":7,"interpolation":"LINEAR","output":76},{"input":10,"interpolation":"LINEAR","output":77},{"input":7,"interpolation":"LINEAR","output":78},{"input":7,"interpolation":"LINEAR","output":79},{"input":10,"interpolation":"LINEAR","output":80},{"input":7,"interpolation":"LINEAR","output":81}]}],"materials":[{"doubleSided":true,"name":"Material.001","pbrMetallicRoughness":{}}],"meshes":[{"name":"Cube","primitives":[{"attributes":{"POSITION":0,"NORMAL":1,"TEXCOORD_0":2,"JOINTS_0":3,"WEIGHTS_0":4},"indices":5,"material":0}]}],"skins":[{"inverseBindMatrices":6,"joints":[48,5,4,1,0,3,2,7,6,12,11,10,9,8,17,16,15,14,13,22,21,20,19,18,27,26,25,24,23,32,31,30,29,28,37,36,35,34,33,42,41,40,39,38,47,46,45,44,43,49,50,51,52,53,54,55,56],"name":"Armature"}],"accessors":[{"bufferView":0,"componentType":5126,"count":1000,"max":[2.742279291152954,1.4045029878616333,2.0192716121673584],"min":[-2.742279291152954,-0.6434623599052429,-3.534085512161255],"type":"VEC3"},{"bufferView":1,"componentType":5126,"count":1000,"type":"VEC3"},{"bufferView":2,"componentType":5126,"count":1000,"type":"VEC2"},{"bufferView":3,"componentType":5121,"count":1000,"type":"VEC4"},{"bufferView":4,"componentType":5126,"count":1000,"type":"VEC4"},{"bufferView":5,"componentType":5123,"count":1500,"type":"SCALAR"},{"bufferView":6,"componentType":5126,"count":57,"type":"MAT4"},{"bufferView":7,"componentType":5126,"count":120,"max":[5],"min":[0.041666666666666664],"type":"SCALAR"},{"bufferView":8,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":9,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":10,"componentType":5126,"count":2,"max":[5],"min":[0.041666666666666664],"type":"SCALAR"},{"bufferView":11,"componentType":5126,"count":2,"type":"VEC3"},{"bufferView":12,"componentType":5126,"count":2,"type":"VEC3"},{"bufferView":13,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":14,"componentType":5126,"count":2,"type":"VEC3"},{"bufferView":15,"componentType":5126,"count":2,"type":"VEC3"},{"bufferView":16,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":17,"componentType":5126,"count":2,"type":"VEC3"},{"bufferView":18,"componentType":5126,"count":2,"type":"VEC3"},{"bufferView":19,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":20,"componentType":5126,"count":2,"type":"VEC3"},{"bufferView":21,"componentType":5126,"count":2,"type":"VEC3"},{"bufferView":22,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":23,"componentType":5126,"count":2,"type":"VEC3"},{"bufferView":24,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":25,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":26,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":27,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":28,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":29,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":30,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":31,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":32,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":33,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":34,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":35,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":36,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":37,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":38,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":39,"componentType":5126,"count":2,"type":"VEC3"},{"bufferView":40,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":41,"componentType":5126,"count":2,"type":"VEC3"},{"bufferView":42,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":43,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":44,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":45,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":46,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":47,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":48,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":49,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":50,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":51,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":52,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":53,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":54,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":55,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":56,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":57,"componentType":5126,"count":120,"type":"VEC4"},{"bufferView":58,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":59,"componentType":5126,"count":2,"type":"VEC4"},{"bufferView":60,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":61,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":62,"componentType":5126,"count":2,"type":"VEC4"},{"bufferView":63,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":64,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":65,"componentType":5126,"count":2,"type":"VEC4"},{"bufferView":66,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":67,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":68,"componentType":5126,"count":2,"type":"VEC4"},{"bufferView":69,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":70,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":71,"componentType":5126,"count":2,"type":"VEC4"},{"bufferView":72,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":73,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":74,"componentType":5126,"count":2,"type":"VEC4"},{"bufferView":75,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":76,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":77,"componentType":5126,"count":2,"type":"VEC4"},{"bufferView":78,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":79,"componentType":5126,"count":120,"type":"VEC3"},{"bufferView":80,"componentType":5126,"count":2,"type":"VEC4"},{"bufferView":81,"componentType":5126,"count":120,"type":"VEC3"}],"bufferViews":[{"buffer":0,"byteLength":12000,"byteOffset":0},{"buffer":0,"byteLength":12000,"byteOffset":12000},{"buffer":0,"byteLength":8000,"byteOffset":24000},{"buffer":0,"byteLength":4000,"byteOffset":32000},{"buffer":0,"byteLength":16000,"byteOffset":36000},{"buffer":0,"byteLength":3000,"byteOffset":52000},{"buffer":0,"byteLength":3648,"byteOffset":55000},{"buffer":0,"byteLength":480,"byteOffset":58648},{"buffer":0,"byteLength":1440,"byteOffset":59128},{"buffer":0,"byteLength":1920,"byteOffset":60568},{"buffer":0,"byteLength":8,"byteOffset":62488},{"buffer":0,"byteLength":24,"byteOffset":62496},{"buffer":0,"byteLength":24,"byteOffset":62520},{"buffer":0,"byteLength":1920,"byteOffset":62544},{"buffer":0,"byteLength":24,"byteOffset":64464},{"buffer":0,"byteLength":24,"byteOffset":64488},{"buffer":0,"byteLength":1920,"byteOffset":64512},{"buffer":0,"byteLength":24,"byteOffset":66432},{"buffer":0,"byteLength":24,"byteOffset":66456},{"buffer":0,"byteLength":1920,"byteOffset":66480},{"buffer":0,"byteLength":24,"byteOffset":68400},{"buffer":0,"byteLength":24,"byteOffset":68424},{"buffer":0,"byteLength":1920,"byteOffset":68448},{"buffer":0,"byteLength":24,"byteOffset":70368},{"buffer":0,"byteLength":1920,"byteOffset":70392},{"buffer":0,"byteLength":1920,"byteOffset":72312},{"buffer":0,"byteLength":1920,"byteOffset":74232},{"buffer":0,"byteLength":1920,"byteOffset":76152},{"buffer":0,"byteLength":1920,"byteOffset":78072},{"buffer":0,"byteLength":1920,"byteOffset":79992},{"buffer":0,"byteLength":1920,"byteOffset":81912},{"buffer":0,"byteLength":1920,"byteOffset":83832},{"buffer":0,"byteLength":1920,"byteOffset":85752},{"buffer":0,"byteLength":1920,"byteOffset":87672},{"buffer":0,"byteLength":1920,"byteOffset":89592},{"buffer":0,"byteLength":1920,"byteOffset":91512},{"buffer":0,"byteLength":1920,"byteOffset":93432},{"buffer":0,"byteLength":1920,"byteOffset":95352},{"buffer":0,"byteLength":1920,"byteOffset":97272},{"buffer":0,"byteLength":24,"byteOffset":99192},{"buffer":0,"byteLength":1920,"byteOffset":99216},{"buffer":0,"byteLength":24,"byteOffset":101136},{"buffer":0,"byteLength":1920,"byteOffset":101160},{"buffer":0,"byteLength":1920,"byteOffset":103080},{"buffer":0,"byteLength":1920,"byteOffset":105000},{"buffer":0,"byteLength":1920,"byteOffset":106920},{"buffer":0,"byteLength":1920,"byteOffset":108840},{"buffer":0,"byteLength":1920,"byteOffset":110760},{"buffer":0,"byteLength":1920,"byteOffset":112680},{"buffer":0,"byteLength":1920,"byteOffset":114600},{"buffer":0,"byteLength":1920,"byteOffset":116520},{"buffer":0,"byteLength":1920,"byteOffset":118440},{"buffer":0,"byteLength":1920,"byteOffset":120360},{"buffer":0,"byteLength":1920,"byteOffset":122280},{"buffer":0,"byteLength":1920,"byteOffset":124200},{"buffer":0,"byteLength":1920,"byteOffset":126120},{"buffer":0,"byteLength":1920,"byteOffset":128040},{"buffer":0,"byteLength":1920,"byteOffset":129960},{"buffer":0,"byteLength":1440,"byteOffset":131880},{"buffer":0,"byteLength":32,"byteOffset":133320},{"buffer":0,"byteLength":1440,"byteOffset":133352},{"buffer":0,"byteLength":1440,"byteOffset":134792},{"buffer":0,"byteLength":32,"byteOffset":136232},{"buffer":0,"byteLength":1440,"byteOffset":136264},{"buffer":0,"byteLength":1440,"byteOffset":137704},{"buffer":0,"byteLength":32,"byteOffset":139144},{"buffer":0,"byteLength":1440,"byteOffset":139176},{"buffer":0,"byteLength":1440,"byteOffset":140616},{"buffer":0,"byteLength":32,"byteOffset":142056},{"buffer":0,"byteLength":1440,"byteOffset":142088},{"buffer":0,"byteLength":1440,"byteOffset":143528},{"buffer":0,"byteLength":32,"byteOffset":144968},{"buffer":0,"byteLength":1440,"byteOffset":145000},{"buffer":0,"byteLength":1440,"byteOffset":146440},{"buffer":0,"byteLength":32,"byteOffset":147880},{"buffer":0,"byteLength":1440,"byteOffset":147912},{"buffer":0,"byteLength":1440,"byteOffset":149352},{"buffer":0,"byteLength":32,"byteOffset":150792},{"buffer":0,"byteLength":1440,"byteOffset":150824},{"buffer":0,"byteLength":1440,"byteOffset":152264},{"buffer":0,"byteLength":32,"byteOffset":153704},{"buffer":0,"byteLength":1440,"byteOffset":153736}],"buffers":[{"byteLength":155176,"uri":"data:application/octet-stream;base64,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BgAAAAYAAAAGAAAABgAAAAYAAAAGAAAABgAAAAYAAAAGAAAABgAAAAYAAAAGAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAALQAAAC0AAAAtAAAALQAAAC0AAAAtAAAALQAAAC0AAAAtAAAALQAAAC0AAAAtAAAALQAAAC0AAAAtAAAALQAAAC0AAAAtAAAALQAAAC0AAAAtAAAALQAAAC0AAAAtAAAALgAAAC4AAAAuAAAALgAAAC4AAAAuAAAALgAAAC4AAAAuAAAALgAAAC4AAAAuAAAALgAAAC4AAAAuAAAALgAAAC4AAAAuAAAALgAAAC4AAAAuAAAALgAAAC4AAAAuAAAALwAAAC8AAAAvAAAALwAAAC8AAAAvAAAALwAAAC8AAAAvAAAALwAAAC8AAAAvAAAALwAAAC8AAAAvAAAALwAAAC8AAAAvAAAALwAAAC8AAAAvAAAALwAAAC8AAAAvAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKQAAACkAAAApAAAAKQAAACkAAAApAAAAKQAAACkAAAApAAAAKQAAACkAAAApAAAAKQAAACkAAAApAAAAKQAAACkAAAApAAAAKQAAACkAAAApAAAAKQAAACkAAAApAAAAKgAAACoAAAAqAAAAKgAAACoAAAAqAAAAKgAAACoAAAAqAAAAKgAAACoAAAAqAAAAKgAAACoAAAAqAAAAKgAAACoAAAAqAAAAKgAAACoAAAAqAAAAKgAAACoAAAAqAAAAKwAAACsAAAArAAAAKwAAACsAAAArAAAAKwAAACsAAAArAAAAKwAAACsAAAArAAAAKwAAACsAAAArAAAAKwAAACsAAAArAAAAKwAAACsAAAArAAAAKwAAACsAAAArAAAAIwAAACMAAAAjAAAAIwAAACMAAAAjAAAAIwAAACMAAAAjAAAAIwAAACMAAAAjAAAAIwAAACMAAAAjAAAAIwAAACMAAAAjAAAAIwAAACMAAAAjAAAAIwAAACMAAAAjAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJQAAACUAAAAlAAAAJQAAACUAAAAlAAAAJQAAACUAAAAlAAAAJQAAACUAAAAlAAAAJQAAACUAAAAlAAAAJQAAACUAAAAlAAAAJQAAACUAAAAlAAAAJQAAACUAAAAlAAAAJgAAACYAAAAmAAAAJgAAACYAAAAmAAAAJgAAACYAAAAmAAAAJgAAACYAAAAmAAAAJgAAACYAAAAmAAAAJgAAACYAAAAmAAAAJgAAACYAAAAmAAAAJgAAACYAAAAmAAAAHgAAAB4AAAAeAAAAHgAAAB4AAAAeAAAAHgAAAB4AAAAeAAAAHgAAAB4AAAAeAAAAHgAAAB4AAAAeAAAAHgAAAB4AAAAeAAAAHgAAAB4AAAAeAAAAHgAAAB4AAAAeAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIQAAACEAAAAhAAAAIQAAACEAAAAhAAAAIQAAACEAAAAhAAAAIQAAACEAAAAhAAAAIQAAACEAAAAhAAAAIQAAACEAAAAhAAAAIQAAACEAAAAhAAAAIQAAACEAAAAhAAAAGQAAABkAAAAZAAAAGQAAABkAAAAZAAAAGQAAABkAAAAZAAAAGQAAABkAAAAZAAAAGQAAABkAAAAZAAAAGQAAABkAAAAZAAAAGQAAABkAAAAZAAAAGQAAABkAAAAZAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGwAAABsAAAAbAAAAGwAAABsAAAAbAAAAGwAAABsAAAAbAAAAGwAAABsAAAAbAAAAGwAAABsAAAAbAAAAGwAAABsAAAAbAAAAGwAAABsAAAAbAAAAGwAAABsAAAAbAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAFAAAABQAAAAUAAAAFAAAABQAAAAUAAAAFAAAABQAAAAUAAAAFAAAABQAAAAUAAAAFAAAABQAAAAUAAAAFAAAABQAAAAUAAAAFAAAABQAAAAUAAAAFAAAABQAAAAUAAAAFQAAABUAAAAVAAAAFQAAABUAAAAVAAAAFQAAABUAAAAVAAAAFQAAABUAAAAVAAAAFQAAABUAAAAVAAAAFQAAABUAAAAVAAAAFQAAABUAAAAVAAAAFQAAABUAAAAVAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEgAAABIAAAASAAAAEgAAABIAAAASAAAAEgAAABIAAAASAAAAEgAAABIAAAASAAAAEgAAABIAAAASAAAAEgAAABIAAAASAAAAEgAAABIAAAASAAAAEgAAABIAAAASAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACwAAAAsAAAALAAAACwAAAAsAAAALAAAACwAAAAsAAAALAAAACwAAAAsAAAALAAAACwAAAAsAAAALAAAACwAAAAsAAAALAAAACwAAAAsAAAALAAAACwAAAAsAAAALAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADQAAAA0AAAANAAAADQAAAA0AAAANAAAADQAAAA0AAAANAAAADQAAAA0AAAANAAAADQAAAA0AAAANAAAADQAAAA0AAAANAAAADQAAAA0AAAANAAAADQAAAA0AAAANAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAEADgAUAAEAFAAHAAoABgATAAoAEwAXABUAEgAMABUADAAPABAAAwAJABAACQAWAAUAAgAIAAUACAALABEADQAAABEAAAAEAEwAUgAsAEwALAAfACIAHgArACIAKwAvAEgAUAAkAEgAJAAnAEsAVQAhAEsAIQAuAFYATgAgAFYAIAAjACkAJQAYACkAGAAcADcAQgA8ADcAPAAxAD0APwA0AD0ANAAyAEYAOwA1AEYANQBAADAAMwA5ADAAOQA2AEQARwBBAEQAQQA+ADgAOgBFADgARQBDAB0AGgBPAB0ATwBXACgAGwBUACgAVABKAC0AKgBRAC0AUQBJABkAJgBTABkAUwBNAFgAWwBhAFgAYQBeAF8AYgBtAF8AbQBqAGwAbwBpAGwAaQBmAGUAaABdAGUAXQBaAGAAawBkAGAAZABZAG4AYwBcAG4AXABnAHAAcwB5AHAAeQB2AHgAegCFAHgAhQCDAIQAhwCBAIQAgQB+AH0AfwB0AH0AdAByAHcAggB8AHcAfABxAIYAewB1AIYAdQCAAIgAiwCRAIgAkQCOAJAAkgCdAJAAnQCbAJwAnwCZAJwAmQCWAJUAlwCMAJUAjACKAI8AmgCUAI8AlACJAJ4AkwCNAJ4AjQCYAKIAqACrAKIAqwClAKcAtAC3AKcAtwCqALIArACvALIArwC1AK0AoACjAK0AowCwAKYAoQCuAKYArgCzALYAsQCkALYApACpALoAwADDALoAwwC9AL8AzADOAL8AzgDBAMoAxADHAMoAxwDNAMYAuQC7AMYAuwDIAL4AuADFAL4AxQDLAM8AyQC8AM8AvADCANIA2ADbANIA2wDVANcA5ADmANcA5gDZAOIA3ADfAOIA3wDlAN4A0QDTAN4A0wDgANYA0ADdANYA3QDjAOcA4QDUAOcA1ADaAOkA6wDxAOkA8QDvAPAA8gD9APAA/QD7APwA/gD4APwA+AD2APUA9wDsAPUA7ADqAO4A+gD0AO4A9ADoAP8A8wDtAP8A7QD5AAABAwEJAQABCQEGAQcBCgEVAQcBFQESARQBFwERARQBEQEOAQ0BEAEFAQ0BBQECAQgBEwEMAQgBDAEBARYBCwEEARYBBAEPASkBHQEjASkBIwEvASgBJgEZASgBGQEbAS0BKwElAS0BJQEnASEBHwEsASEBLAEuARgBJAEqARgBKgEeARwBGgEgARwBIAEiATEBMwE6ATEBOgE4ATcBOQFFATcBRQFDAUQBRgE/AUQBPwE9AT4BQAE0AT4BNAEyATYBQgE8ATYBPAEwAUcBOwE1AUcBNQFBAUkBSwFRAUkBUQFPAVABUgFdAVABXQFbAVwBXgFYAVwBWAFWAVUBVwFMAVUBTAFKAU4BWgFUAU4BVAFIAV8BUwFNAV8BTQFZAWABYwFpAWABaQFmAWgBagF1AWgBdQFzAXQBdwFxAXQBcQFuAW0BbwFkAW0BZAFiAWcBcgFsAWcBbAFhAXYBawFlAXYBZQFwAYkBfQGDAYkBgwGPAYgBhgF5AYgBeQF7AY0BiwGFAY0BhQGHAYEBfwGMAYEBjAGOAXgBhAGKAXgBigF+AXwBegGAAXwBgAGCAZEBkwGZAZEBmQGXAZgBmgGlAZgBpQGjAaQBpgGgAaQBoAGeAZ0BnwGUAZ0BlAGSAZYBogGcAZYBnAGQAacBmwGVAacBlQGhAakBqwGxAakBsQGvAbABsgG9AbABvQG7AbwBvgG4AbwBuAG2AbUBtwGsAbUBrAGqAa4BugG0Aa4BtAGoAb8BswGtAb8BrQG5AcABwwHJAcAByQHGAcgBygHVAcgB1QHTAdQB1wHRAdQB0QHOAc0BzwHEAc0BxAHCAccB0gHMAccBzAHBAdYBywHFAdYBxQHQAekB3QHjAekB4wHvAegB5gHZAegB2QHbAe0B6wHlAe0B5QHnAeEB3wHsAeEB7AHuAdgB5AHqAdgB6gHeAdwB2gHgAdwB4AHiAfEB8wH5AfEB+QH3AfgB+gEFAvgBBQIDAgQCBgIAAgQCAAL+Af0B/wH0Af0B9AHyAfYBAgL8AfYB/AHwAQcC+wH1AQcC9QEBAgkCCwIRAgkCEQIPAhACEgIdAhACHQIbAhwCHgIYAhwCGAIWAhUCFwIMAhUCDAIKAg4CGgIUAg4CFAIIAh8CEwINAh8CDQIZAiACIwIpAiACKQImAigCKwI2AigCNgIzAjQCNwIxAjQCMQIuAiwCLwIkAiwCJAIhAicCMgItAicCLQIiAjUCKgIlAjUCJQIwAkkCPQJDAkkCQwJPAkgCRgI5AkgCOQI7Ak0CSwJFAk0CRQJHAkECPwJMAkECTAJOAjgCRAJKAjgCSgI+AjwCOgJAAjwCQAJCAlICVAJZAlICWQJXAlgCWgJmAlgCZgJkAmMCZQJgAmMCYAJeAl0CXwJTAl0CUwJRAlYCYgJcAlYCXAJQAmcCWwJVAmcCVQJhAmkCbwJzAmkCcwJtAm4CewJ/Am4CfwJyAnoCdAJ4AnoCeAJ+AnUCaAJsAnUCbAJ5AnACagJ2AnACdgJ8An0CdwJrAn0CawJxAoICiAKLAoICiwKFAocClAKXAocClwKKApICjAKPApICjwKVAo0CgAKDAo0CgwKQAoYCgQKOAoYCjgKTApYCkQKEApYChAKJAqcCrQKhAqcCoQKbAqgCnQKZAqgCmQKkAq8CqQKlAq8CpQKrAqMCrgKqAqMCqgKfApoCoAKsApoCrAKmApwCogKeApwCngKYArECtgK6ArECugK1ArcCwwLHArcCxwK7AsICvQLBAsICwQLGArwCsAK0ArwCtALAArgCsgK+ArgCvgLEAsUCvwKzAsUCswK5AskCzwLTAskC0wLNAs4C2wLfAs4C3wLSAtoC1ALYAtoC2ALeAtUCyALMAtUCzALZAtACygLWAtAC1gLcAt0C1wLLAt0CywLRAuIC6ALrAuIC6wLlAuYC8wL3AuYC9wLqAvIC7ALvAvIC7wL1Au0C4ALkAu0C5ALxAucC4QLuAucC7gL0AvYC8ALjAvYC4wLpAgcDDQMBAwcDAQP7AggD/QL5AggD+QIEAw8DCQMFAw8DBQMLAwMDDgMKAwMDCgP/AvoCAAMMA/oCDAMGA/wCAgP+AvwC/gL4AhEDFwMbAxEDGwMVAxYDIwMnAxYDJwMaAyIDHAMgAyIDIAMmAx0DEAMUAx0DFAMhAxgDEgMeAxgDHgMkAyUDHwMTAyUDEwMZAykDLwMzAykDMwMtAy4DOwM/Ay4DPwMyAzoDNAM4AzoDOAM+AzUDKAMsAzUDLAM5AzADKgM2AzADNgM8Az0DNwMrAz0DKwMxA0IDSANLA0IDSwNFA0YDUwNXA0YDVwNKA1IDTANPA1IDTwNVA00DQANEA00DRANRA0cDQQNOA0cDTgNUA1YDUANDA1YDQwNJA2cDbQNhA2cDYQNbA2gDXQNZA2gDWQNkA28DaQNlA28DZQNrA2MDbgNqA2MDagNfA1oDYANsA1oDbANmA1wDYgNeA1wDXgNYA3EDdwN7A3EDewN1A3YDgwOHA3YDhwN6A4IDfAOAA4IDgAOGA30DcAN0A30DdAOBA3gDcgN+A3gDfgOEA4UDfwNzA4UDcwN5A4kDjwOTA4kDkwONA44DmwOfA44DnwOSA5oDlAOYA5oDmAOeA5UDiAOMA5UDjAOZA5ADigOWA5ADlgOcA50DlwOLA50DiwORA6IDqAOrA6IDqwOlA6YDswO2A6YDtgOpA7IDrAOvA7IDrwO1A64DoQOkA64DpAOxA6cDoAOtA6cDrQO0A7cDsAOjA7cDowOqA8cDzQPBA8cDwQO7A8gDvQO5A8gDuQPEA88DyQPFA88DxQPLA8MDzgPKA8MD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TmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+cw9Rrv9jabWlCWo/0XvPPt5waq/JcWm1pQlqP9F7zz54BNKqTmtptaUJaj/Re88+jT7qr0Z4abWlCWo/0XvPPhnLKDCae2m1pwlqP9F7zz7t5U+vDU5ptaUJaj/Se88+goncLxOCabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4MKI+vempptaUJaj/Re88+WtPpL1ZeabWlCWo/0XvPPgDKKDCYe2m1pQlqP9F7zz5ssc+vzDBptaUJaj/Re88+NwGPsPxnabWlCWo/0XvPPuaqzi7ueWm1pQlqP9F7zz5TyIEve2hptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/0XvPPhmEwKpOa2m1pQlqP9F7zz4ZhMCqTmtptaUJaj/Re88+GYTAqk5rabWlCWo/T3bMPtJ+tDupP1i6frJqPy4mxD7/IKg88kZJu6xobD/Tnrc+fTsvPRDA0bu5tm4/KgOoPrEvjz3tYSu8ZyVxP+Welj7imss9ZLFzvJtQcz8O6YQ+5MsDPmy+nbwK+HQ/xOdoPtgBHz5xT768WQd2P5CqTT7bDzU+qLTYvE+Sdj+AKzs+Ms5DPt1Z6ry7xXY/MFw0PtovST6gyvC8DdB2PxrKOT5Qoww+YeWUvI49eT9qDkY+lkQfO92u7zuDKHs/bAZVPlBwCL54YQY9Q+x3P7h7aT65NEa+0K00PTwFdD/RA4M+3nc/vu4bND3ZinI/3NGTPpc4K75bNDE92xNxP+Nhpj5gmwu+XiQsPdBVbz8GzLk+B/3FvaQiJT2lCW0/3hLNPmqXUL33gxw9TgBqPxw93z6DrA27J8YSPYwzZj8fde8+Yuo9PbqNCD29zWE/OCH9Pqtouz1TMP08myVdP1L1Az9hlQU+21HrPBuwWD892Ac/PqskPvT03Dx07lQ/wzUKP4nXOD69b9M8Wl5SPxMFCz8ADkA+6P7PPLJuUT8TBQs/AA5APuj+zzyyblE/EwULPwAOQD7o/s88sm5RPxMFCz8ADkA+6P7PPLJuUT8TBQs/AA5APuj+zzyyblE/EwULPwAOQD7o/s88sm5RPxMFCz8ADkA+6P7PPLJuUT8TBQs/AA5APuj+zzyyblE/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AADAsrc9kT4AAMCysBG0MrI9kT4uqXGzvNkdPacTxj3zigM/5/5ZP6mBIj1r1sQ9zwcDP+dOWj8Aty89mU/BPYaOAT/7MVs/6FpEPbLEuz06av4+35JcPwpPXz21eLQ9/R/4PldaXj9Vb389L6+rPRpn8D4CcGA/wsaRPb+uoT3vauc+J7tiP4I3pT3QwpY9bVvdPosjZT8KZ7k9fTyLPYNu0j5ykmc/YK7NPRjlfj2M4MY+gvNpP0xl4T0lgWc9B/S6Pqg1bD+B5fM9OQtRPY3wrj63S24/80YCPlZEPD01IaM+xCxwP4ViCT4E7Sk9xdKXPhHUcT9H/Q4+G8IaPdRRjT6iQHM/hNMSPrJ6Dz1X6YM+YXR0P0qlFD6Cxwg9GcN3PgBzdT/VNBQ+f1MHPSP/aj6VQHY/vIcOPqOgET3otWI+X+x2P8gdAj639io9ta1fPuF1dz8D0eE9AyxPPazEYT4Bw3c/ETThPf0yaT274MA+FQNrP71n1j2AuoA9izEHP6MhVz95Q6g9U6mYPTjlBz/5FVc/+rx8PUBBrj1zbwg/le9WPxqAOD0+vr89aMcIP4PBVj9khwo9w3jLPU7oCD/yo1Y/QtvyPE3Qzz1GzQg/5a5WP8QZ8jxa4889woQIP/bcVj+1hvI8hs3PPY4eCD/6HVc/N6j1PIxczz2plAc/znVXP3cE/Tz7Xc499eAGPyzoVz/CEAU9HZ/MPSn9BT+EeFg/gkIPPdzsyT3F4gQ/7SlZP7zZHT2nE8Y984oDP+f+WT++bzQ9oAfAPZPgAD8amVs/gbRTPRZ3tz3ZfPg+9kFeP53+dz3XSq09P5DsPuV3YT/dy449g32iPWNZ3z73w2Q/imigPZ0emD2eUtI+/sJnP24Orz36TI894QvHPkIpaj+nDLk9jC2JPUEevz6Mvms/Z7+8PWvjhj20ILw+3lFsP9YkuD2IsIk94Xe/PgGuaz/B7qs9aA2RPcw9yD737Gk/+IGaPZ1kmz1fi9Q+f0hnPwdZhj1YHqc97XfiPoYGZD8tGWQ98auyPawo8D5YiGA/X45APeiUvD3e4Ps+c01dP55kJz3ge8M9ewMCP37rWj+82R09qBPGPfOKAz/n/lk/vNkdPagTxj3zigM/5/5ZP7zZHT2oE8Y984oDP+f+WT+82R09qBPGPfOKAz/n/lk/vNkdPacTxj3zigM/5/5ZPylYID2CZsU93SQDPwM9Wj+kZCc93HvDPXsDAj9+61o/AVkyPZR9wD1RPwA/hvdbP2KOQD3llLw92+D7PnRNXT9tXFE9cOu3PTJe9j6B2V4/MBlkPe+rsj2rKPA+WYhgP4wZeD3SAq09aXTpPqZHYj8HWYY9VB6nPel34j6FBmQ/IpyQPXAuoT37a9s+2bVlP/6Bmj2cZJs9XovUPn9IZz9LuKM9QfOVPUcSzj5Rs2g/xO6rPWcNkT3KPcg++OxpPyzXsj0t5ow9P0vDPoztaj/WJLg9g7CJPeF3vz4Brms/xYu7PfGehz1gAL0+YidsP2e/vD1r44Y9tCC8Pt5RbD/Fi7s98p6HPWAAvT5iJ2w/1CS4PYiwiT3hd78+Aa5rPynXsj0u5ow9P0vDPoztaj/C7qs9aA2RPco9yD747Gk/R7ijPUHzlT1HEs4+UbNoP/yBmj2eZJs9X4vUPn9IZz8cnJA9dC6hPfxr2z7ZtWU/BFmGPVcepz3td+I+hgZkP4IZeD3SAq09aXTpPqZHYj8pGWQ98quyPawo8D5YiGA/ZFxRPXLrtz0zXvY+gdleP16OQD3plLw93uD7PnNNXT/+WDI9l33APVE/AD+F91s/nWQnPd17wz17AwI/futaPyRYID2EZsU93SQDPwM9Wj+82R09qBPGPfOKAz/n/lk/vtkdPagTxj3zigM/5/5ZP7vZHT2oE8Y984oDP+f+WT++2R09qBPGPfOKAz/n/lk/u9kdPaYTxj3zigM/5/5ZP7zZHT2nE8Y984oDP+f+WT+72R09qBPGPfOKAz/n/lk/vtkdPaYTxj3zigM/5/5ZP73ZHT2oE8Y984oDP+f+WT++2R09oxPGPfOKAz/n/lk/vdkdPacTxj3zigM/5/5ZP7vZHT2rE8Y984oDP+f+WT+72R09qBPGPfOKAz/n/lk/u9kdPagTxj3zigM/5/5ZP73ZHT2oE8Y984oDP+f+WT+82R09qBPGPfOKAz/n/lk/vNkdPagTxj3zigM/5/5ZP7zZHT2oE8Y984oDP+f+WT+82R09qBPGPfOKAz/n/lk/vdkdPagTxj3zigM/5/5ZP73ZHT2rE8Y984oDP+f+WT+82R09phPGPfOKAz/n/lk/wNkdPagTxj3zigM/5/5ZP7vZHT2oE8Y984oDP+f+WT+92R09phPGPfOKAz/n/lk/wtkdPagTxj3zigM/5/5ZP7zZHT2nE8Y984oDP+f+WT+82R09pxPGPfOKAz/n/lk/vNkdPacTxj3zigM/5/5ZP7zZHT2nE8Y984oDP+f+WT+82R09pxPGPfOKAz/n/lk/vNkdPacTxj3zigM/5/5ZP7zZHT2nE8Y984oDP+f+WT+82R09pxPGPfOKAz/n/lk/AACAPwAAgD8AAIA/AQCAPwAAgD8AAIA/AAAAM649kT4AAACzghvkM7I9kT75EZeyw9kdPaITxr3zigO/6P5ZP4LdIT0dAsW94hkDv+dDWj+ZPy09bfjBvfbUAb/sB1s/LgU/PSw0vb0RoP++BTlcP4czVj028ba9ojv6vtbDXT8yzHE9wGuvvQKi8744lF8/LGWIPQLipr1M+uu+4JVhP8EQmT1flZ29tW7jvga1Yz83X6o9AcuTvYQt2r4L32U/gMS7PdvLib1JadC+GQNoP+azzD1/yH+941jGvsMSaj8Aotw9u8Vsvb42vL5UAmw/rwbrPYQvW73wP7K+IMltP7Be9z00pku9B7Oovmdhbz9AlgA+k8g+vevOn74TyHA/JvwDPk0yNb3W0Ze+JfxxP4unBT5bey+9hPiQvvT9cj91YAU+CjguvdN9i743znM/Uvn/PdY2OL2K34e+ond0PxZ85z1mMlG9s3GGvqH3dD+EEMU9iSN1vZ0rh74fNXU/4FDCPb0fh72gL9e+k2ZmP5Egtj1OCpK9OpQRv6WLUD+6Hog9QsSpvcMpEr+YZ1A/j0Y8PQNIv73jjhK/xjJQP8EU7zy3y9C9vrQSv5gHUD9v2ZE81p7cvd+QEr+gBVA/57BcPDUa4b24GBK/XkxQP6poYDzm1+C9HE0Rv3XbUD8Hs3Q8N4XfvZQuEL/npVE/Ak2NPNoO3b0Urg6/vrNSP8iWqTzJX9m9s7sMv/ELVD8av888U2HUvYdGCr/4s1U/OSUAPSD7zb2XPAe/U69XP8PZHT2iE8a984oDv+j+WT97PUA9sse8vdfD/b7xwlw/wPpkPd6fsr1difK+y+NfP5HvhD1DLqi94ETmvpcSYz/fWpY9yROevQPo2b79C2Y/lKqlPWL9lL0Ndc6+YppoP8PUsT1Pn429p/jEvi+Vaj8r2rk93K+IvcKCvr6e3Gs/br+8PWfjhr2yILy+3lFsP9kkuD2DsIm94He/vgGuaz/F7qs9ZA2Rvco9yL747Gk/A4KaPZlkm71fi9S+f0hnPwpZhj1WHqe97XfivoYGZD8yGWQ98quyva0o8L5ZiGA/Z45APeeUvL3c4Pu+dE1dP6VkJz3Ze8O9ewMCv37rWj/D2R09qBPGvfOKA7/n/lk/w9kdPagTxr3zigO/5/5ZP8PZHT2oE8a984oDv+f+WT/D2R09qBPGvfOKA7/n/lk/w9kdPaITxr3zigO/6P5ZPy9YID19ZsW92yQDvwQ9Wj+sZCc91HvDvXoDAr9/61o/D1kyPY19wL1RPwC/hvdbP2yOQD3dlLy92OD7vnVNXT94XFE9Z+u3vS9e9r6C2V4/PBlkPearsr2oKPC+WohgP5oZeD3IAq29ZXTpvqdHYj8NWYY9TB6nveV34r6GBmQ/JZyQPWkuob32a9u+2rVlPwSCmj2WZJu9XYvUvoBIZz9QuKM9PfOVvUYSzr5Rs2g/x+6rPWENkb3HPci++OxpPy/Xsj0p5oy9PkvDvoztaj/bJLg9gbCJveB3v74Brms/xou7PfOeh71fAL2+YidsP26/vD1n44a9siC8vt5RbD/Ji7s9756HvWAAvb5iJ2w/2iS4PYOwib3gd7++Aa5rPy7Xsj0q5oy9P0vDvoztaj/G7qs9Yw2Rvco9yL747Gk/TbijPUDzlb1IEs6+UbNoP/6Bmj2ZZJu9X4vUvn9IZz8jnJA9cy6hvfxr277ZtWU/ClmGPVUep73td+K+hgZkP44ZeD3RAq29a3TpvqVHYj8yGWQ98auyva0o8L5YiGA/a1xRPXDrt70zXva+gNleP2KOQD3nlLy93OD7vnRNXT8FWTI9lH3AvVI/AL+F91s/oGQnPdp7w717AwK/futaPytYID2DZsW93SQDvwM9Wj/D2R09qBPGvfOKA7/n/lk/x9kdPacTxr3zigO/5/5ZP8XZHT2oE8a984oDv+f+WT/E2R09pxPGvfOKA7/n/lk/xNkdPacTxr3zigO/5/5ZP8TZHT2nE8a984oDv+f+WT/E2R09pxPGvfOKA7/n/lk/w9kdPacTxr3zigO/5/5ZP8TZHT2nE8a984oDv+f+WT/F2R09qBPGvfOKA7/n/lk/xdkdPagTxr3zigO/5/5ZP8HZHT2nE8a984oDv+f+WT/H2R09phPGvfOKA7/n/lk/wNkdPakTxr3zigO/5/5ZP8LZHT2oE8a984oDv+f+WT/E2R09pxPGvfOKA7/n/lk/w9kdPagTxr3zigO/5/5ZP8HZHT2rE8a984oDv+f+WT/A2R09qxPGvfOKA7/n/lk/x9kdPaYTxr3zigO/5/5ZP8PZHT2oE8a984oDv+f+WT/D2R09pxPGvfOKA7/n/lk/wdkdPaYTxr3zigO/5/5ZP8TZHT2nE8a984oDv+f+WT/H2R09pxPGvfOKA7/n/lk/w9kdPacTxr3zigO/5/5ZP8rZHT2qE8a984oDv+n+WT/K2R09qhPGvfOKA7/p/lk/ytkdPaoTxr3zigO/6f5ZP8rZHT2qE8a984oDv+n+WT/K2R09qhPGvfOKA7/p/lk/ytkdPaoTxr3zigO/6f5ZP8rZHT2qE8a984oDv+n+WT/K2R09qhPGvfOKA7/p/lk//P9/P///fz8AAIA//f9/PwAAgD8BAIA/qs2EKS4DgT9BeVCyqs2EKS4DgT9BeVCy4VEyPgAAAABZ5t60qhZ8P28SND65BgKqChfhtL0CfD8dDzk+lSQCquVS57TnyHs/XuBAPjZVAqp0GPG09mp7PxQeSz4RrQaql+X9tMzpej8yX1c+vO0Cqn+bBrUTRno/pDllPjRVg6kGRA+15YB5P2BCdD7/zYOpfKkYtT6ceD/vBoI+t1aEqaqIIrVmm3c/aBiKPlsB2KmEniy1NoN2P3Egkj4OjgWqjag2tTBadT/c6pk+MIk4qpVlQLWSKHQ/zEShPhTf/Kn/lUm1J/hyP938pz5AgAepFvxRtRnUcT9L460+yBaIqR5cWbWcyHA/n8myPqO/KqoGfF+1h+JvPz+Ctj7Mv++pzyJktesubz/N37g+AAAAAMIXZ7WLum4/JLS5PpMHzqkwIWi1VJFuPw+trz6OThmqVJhbtXt1cD8BDJY+ExU4qgCPO7WTwnQ/lZ5nPjKZfqodwxC1gV15P8L6IT6cWDqqc3nKtOTGfD/yl9g9aENhqvReh7R3kH4/IfytPWA4x6one1m0FRN/Pxh2tD31f4CqoJNhtBUBfz+AYsU9YL6fqiO7drTpzn4/ZMHdPSLmmKrimIq0r35+P8aT+j38tWOqYpyctJ4Tfj+UbAw+tVqPqsCHr7TclH0/pMgaPn6skarUesG0/Q59P7/fJj7SFmeqs5fQtO6TfD81Ni8+Y7m4qsUD27SHOXw/4VEyPgAAAABZ5t60qhZ8PyVQMj69Gyc6NZYOPDUUfD+yUDI+82UKOk8t7Dv7FHw/o1EyPvC6ejnR61U7UhZ8P8JRMj49hjG5HIIXu34WfD8kUDI+Kh4nusubDrw1FHw/p0wyPujrkLqYVHe8Rg98PxVIMj6xcMa6EVWpvM8IfD/7QzI+7kjsuhKgybwDA3w/OUIyPiaj+rpE39W8jQB8P/dDMj7hSOy6EqDJvAMDfD8PSDI+p3DGugxVqbzOCHw/o0wyPg7skLqXVHe8Rg98Px9QMj48Hie6yJsOvDYUfD++UTI+SIYxuQ6CF7t+Fnw/nlEyPpm6ejnx61U7UhZ8P65QMj70ZQo6Vi3sO/sUfD8lUDI+sRsnOjiWDjw1FHw/4VEyPgAAAABZ5t60qhZ8P+FRMj4AAAAAWebetKoWfD/hUTI+AAAAAFnm3rSqFnw/4VEyPgAAAABZ5t60qhZ8Py1rMj7BNTOuWObetIwVfD+tsjI+qDwrr1nm3rRjEnw/wiEzPiHSt69V5t60dQ18P9CxMz67oBuwS+betAsHfD9BXDQ+BgFnsDvm3rRv/3s/dho1PtyQnbAh5t606PZ7P9jlNT5CjMqw/OXetMDtez/JtzY+EP34sM3l3rRB5Hs/s4k3PgC3E7GW5d60t9p7P/tUOD6YNCqxVOXetHDRez8PEzk+zzw/sRLl3rS5yHs/VL05PpoTUrHM5N6048B7PzpNOj6NAWKxjuTetD26ez8nvDo+v0lusVzk3rQatXs/iAM7PpQvdrE35N60yrF7P80cOz4tOD+qAuTptJ6wez+IAzs+cj8yLgrk6bTKsXs/J7w6Pr7uKi8x5Om0GrV7PzpNOj75ubcvZeTptD26ez9UvTk+QpkbMKXk6bTjwHs/DxM5PiPxZjDq5Om0uch7P/tUOD7riJ0wLuXptHDRez+ziTc+UYTKMG/l6bS32ns/ybc2Pnn3+DCo5em0QeR7P9jlNT74sxMx1+XptMDtez92GjU+rjAqMfvl6bTo9ns/QVw0PtM5PzET5um0b/97P9CxMz67EFIxI+bptAsHfD/CITM+iP1hMSzm6bR1DXw/rbIyPt1FbjEt5um0YxJ8Py1rMj66LHYxLObptIwVfD/hUTI+AAAAAFnm3rSqFnw/GDg1PkMUQ6gahuK0k/V7P5g7PT4oYoqpforstECXez+cVEk+292SqcGp+7TVAHs/YHlYPmANB6rbSwe12zZ6P2GeaT7l+zyq/QIStak/eT/Kt3s+55j3qd9SHbUXJXg/8d2GPlYzaKlulSi1fvV2P9dSjz6YVAUojScztezDdT92u5Y+iO+FqFJqPLWep3Q/MZycPthG66k+w0O14rpzP+J7oD5mMEqp25pItWYZcz+r4aE+zGFKqRhaSrUR3nI/q+GhPt3rhigYWkq1Ed5yP6zhoT7d64apGFpKtRHecj+r4aE+wxxsqRhaSrUR3nI/rOGhPsMc7KkYWkq1Ed5yP6vhoT4++nyqGFpKtRHecj+r4aE+wxzsqRhaSrUR3nI/rOGhPsxhSqkYWkq1Ed5yP6zhoT4O80GqF1pKtRHecj9Txp8+5qtBque3R7VLN3M/y+GZPlSi+6k/WkC1ACp0P3DTkD43c4WpTAg1tYiLdT8OO4U+9v1nqdKJJrVQLnc/FH9vPqrY9qlsrxW1V+Z4PzokUj4WOPWppFYDtS2Nej/c2zM+3kjTqdbS4LQsBXw/uCkWPuR95qkotLu0hTt9P5cq9T3YGRmpozqZtLkofj+iQcU95MgoKReSdrRPz34/X4efPcKHrqlHaUe04Dh/P/HUhj3aYK6pPoootNJxfz9G8ns9P11AqXN3HbTog38/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/utIUPye9Cr0rP/w+tIslP1rTFD9qxAq9gD78Pl6LJT981xQ/TfQKvf04/D6XiSU/gOIUP+1zC71QKfw+PYUlP8zTFT+agRa9urf5Pu6OJT8IuhY/vKQgvapk9j4X8yU/N9oXP9lWK72/WfE+drsmP8chGT8++zS9UQPtPtYTJz+aiho/d/U8vVJP6D50ZSc/VyMbP6voP71+DOY+npwnPzKIGz9ofkG9vHrkPrHGJz/qHBw/9WxEvQDS4j4/ySc/7zMcPwDYRb0SMeM+9JEnP+1lHD/xUEa99lviPt2qJz/8lxw/YLJGvb6D4T6FxCc/RN4cPy+8Sb0Y1eE+xWMnPzxMHT90k0u9irDgPsRcJz8xJB0/2dxJvU2D4D6Zkyc/qwccPxvIPb27Pd8+kxYpP0HZGD/Zqhy9+YHbPshLLT8KuQ0/UvGCvIO33z5ucTU/CLUAP0f8ezt6s+w+p/s6PwA89T42ekI7yTn5Ph0COz9qnvg+U5+AOsry/D6xoDg/HUz7PgQwJDl16P0+SGM3P2n2/j5yjFy6ip3+PhjfNT+LcQI/bpAnu90//z5fiDM/PRsGP7OHxrvY9f8++4wwP8j5CT8tWjO84yMAP+hoLT/TnA0/NNiIvED+/z6rhyo/WaAQP11HvLwBMf8+3TwoPw3jEj9tG+q8CfX9PsquJj/AUBQ/OOoEvVjE/D5I0iU/utIUPye9Cr0rP/w+tIslP920Gz+mBVy9cZEKP6P9Ez/sfRs/f7FZvQK4CD/n7xU/JNwaPzyAUr0gUgQ/4YEaP4MIGj/V20e9dfb+PnthHz8Ymhk/k71BvQVJ+j7pqSE/hkAZP33WPb3Ddfo+KfIhP0BHGD+ZxjK9ZvD6PvO5Ij8CxxY/8l4hva+W+z6X8CM/utIUPye9Cr0rP/w+tIslP+OEEj8NXuK8rb/8Pmt5Jz/sMBA/jxGzvDfv/D4AeCk/RFgOP7BNkbxI5Pw+zhErP7+XDT/ai4S8k9b8Pum4Kz/pFRA/OEyyvHeIAD+p/yc/xTEVP690Db0f7QQ/jMcfP17JGT9baEO95egIP8mhFz/dtBs/pgVcvXGRCj+j/RM/utIUPye9Cr0rP/w+tIslP7rSFD8nvQq9Kz/8PrSLJT+60hQ/J70KvSs//D60iyU/utIUPye9Cr0rP/w+tIslP7rSFD8nvQq9Kz/8PrSLJT+60hQ/J70KvSs//D60iyU/utIUPye9Cr0rP/w+tIslP7rSFD8nvQq9Kz/8PrSLJT+60hQ/J70KvSs//D60iyU/utIUPye9Cr0rP/w+tIslP7rSFD8nvQq9Kz/8PrSLJT+60hQ/J70KvSs//D60iyU/utIUPye9Cr0rP/w+tIslP7rSFD8nvQq9Kz/8PrSLJT+60hQ/J70KvSs//D60iyU/utIUPye9Cr0rP/w+tIslP7rSFD8nvQq9Kz/8PrSLJT+60hQ/J70KvSs//D60iyU/utIUPye9Cr0rP/w+tIslP7rSFD8nvQq9Kz/8PrSLJT+60hQ/J70KvSs//D60iyU/utIUPye9Cr0rP/w+tIslP7rSFD8nvQq9Kz/8PrSLJT+60hQ/J70KvSs//D60iyU/utIUPye9Cr0rP/w+tIslP7rSFD8nvQq9Kz/8PrSLJT+60hQ/J70KvSs//D60iyU/utIUPye9Cr0rP/w+tIslP7rSFD8nvQq9Kz/8PrSLJT+60hQ/J70KvSs//D60iyU/utIUPye9Cr0rP/w+tIslP7rSFD8nvQq9Kz/8PrSLJT+60hQ/J70KvSs//D60iyU/utIUPye9Cr0rP/w+tIslP7rSFD8nvQq9Kz/8PrSLJT+60hQ/J70KvSs//D60iyU/4Y0UP2DBB73pt/s+Uv8lPzjQEz9VQ/+8sEv6Pp03Jz9dshI/IEvnvII7+D56/ig/cE0RP9w4yrz8x/U+DR4rPxa8Dz+tp6q8Wy3zPtxjLT+4Gg4/VxmLvA6h8D63oi8/T4cMP1HEW7wUUe4+J7MxPxohCz+oNCq83GTsPpVyMz/8Bwo/dTAFvFb/6j7PwTQ/sUsJP+4p57tiUeo+/ok1P+3tCD8eONy7tyTqPlvfNT87ygg/StXcu2k36j4u9DU/DN0IPy3957v+heo+fcw1P28jCT+PCP27WQ/rPo9qNT/9mgk/xc8NvM7T6z5TzzQ/0kEKP2faIbxu1Ow+qPozP1MaCz/mdDq81ZruPjK7Mj8AGgw/w55WvPSF8T5i9DA/SxcNP6VPeLxgXPU+NtMuP+kIDj9kO4y8QLn5Pi59LD/10w4/T/SZvIVE/j6nJSo/HV0PPwompLweVQE/TQQoP1qJDz8ia6q8U1ADP1pRJj/QSA8/SC6rvKvsBD9gQCU/WZkOP8Ciobyn8wU/JwYlP19KDT9q/Za8tIQGP3iyJT87fws/gKmQvOrUBj959iY/iJsJPzqvirxE8QY/j3AoP0DbBz8BjIi8YPwGP4rSKT9OlAY/KyCJvFgGBz8Wzio//RgGP0HHibxlCwc/2iorP/0YBj9Bx4m8ZQsHP9oqKz/9GAY/QceJvGULBz/aKis/5V9XvuntvT3vuti+EFhgP6ZgV74S5r09NbvYvg5YYD9jZle+Aq+9PYy92L7fV2A/UXZXvnAZvT1DxNi+RVdgPzd1Wb6tKK09NcjZvvssYD9XyFu+y1abPXkE277f718/gNpevvVagz0coty+JJdfPwg+Yb7F4lk9iaLdvpdhXz9ShGO+lPIqPcZ23r7NMF8/lYJkvi/tFT0f096+oxhfP28rZb4R4gc9Nw7fvgsIXz9e2mW+sJDsPK0o3740AF8/M7dlvrKG7Dz9+96+pw1fP4ALZr4WcN48Zhffvv4EXz+KX2a+61fQPEEy375B/F4/hERmvpIxyTyE5d6+1BJfP7a1Zr4BcLA8Z+XevskQXz+8xGa+No60PA4S377UA18/GS9nvhZS0TyyQuC+NKpePwlbaL5hYA89YTXjvmLBXT933WO+oESuPW9f5L5h21w/vB9Tvo06GT6lPdq+tzReP4o9RL7dMTo+WBnPvnYkYD+wmkK+CP85Plzezb5ihmA/EBBDvhZgNz6SLM6+kZBgP3MuRL4E+zI+HfXOvpmLYD8YREa+zBUrPgVy0L6ueGA/YzpJvo/nHz6NKdK+8GtgP19sTL5ylxI+/+/TvlhkYD8Ank++EFYEPquP1b6pYGA/36ZSvoTq7D3839a+Ol9gP6QZVb6XANU9Qd7XviBdYD9iwla+VjvEPa6A2L7dWWA/5V9XvuntvT3vuti+EFhgP1ocSL5Jf8E97qDJvpCoZD9c80q+6AG5PcHSy77yH2Q/q+lQvgVCpj16gNC+MfRiP6JuVr6ejZM9yunUvrLOYT83yFi+GRyLPfbO1r42TWE/N7pYvszhjj1YBNe+4DdhP0GFWL4sbJk9c4fXvvj/YD86GFi+joapPZUp2L5bsWA/5V9XvuntvT3vuti+EFhgP21UVr7WydQ9FRTZvmMAYD+6KVW+ly3qPTYx2b68tV8/wS5UvoPr+T1HKNm+qYJfP47EU747BgA+Vx3Zvt1vXz+bQ1K+MJX2Pdwa175CLWA/DHZOvsRX4T0yJ9K+HOphP7gpSr7sjcs9gXXMvnjHYz9aHEi+SX/BPe6gyb6QqGQ/5V9XvuntvT3vuti+EFhgP+VfV77p7b0977rYvhBYYD/lX1e+6e29Pe+62L4QWGA/5V9XvuntvT3vuti+EFhgP+VfV77p7b0977rYvhBYYD/lX1e+6e29Pe+62L4QWGA/5V9XvuntvT3vuti+EFhgP+VfV77p7b0977rYvhBYYD/lX1e+6e29Pe+62L4QWGA/5V9XvuntvT3vuti+EFhgP+VfV77p7b0977rYvhBYYD/lX1e+6e29Pe+62L4QWGA/5V9XvuntvT3vuti+EFhgP+VfV77p7b0977rYvhBYYD/lX1e+6e29Pe+62L4QWGA/5V9XvuntvT3vuti+EFhgP+VfV77p7b0977rYvhBYYD/lX1e+6e29Pe+62L4QWGA/5V9XvuntvT3vuti+EFhgP+VfV77p7b0977rYvhBYYD/lX1e+6e29Pe+62L4QWGA/5V9XvuntvT3vuti+EFhgP+VfV77p7b0977rYvhBYYD/lX1e+6e29Pe+62L4QWGA/5V9XvuntvT3vuti+EFhgP+VfV77p7b0977rYvhBYYD/lX1e+6e29Pe+62L4QWGA/5V9XvuntvT3vuti+EFhgP+VfV77p7b0977rYvhBYYD/lX1e+6e29Pe+62L4QWGA/5V9XvuntvT3vuti+EFhgP+VfV77p7b0977rYvhBYYD/lX1e+6e29Pe+62L4QWGA/5V9XvuntvT3vuti+EFhgP+VfV77p7b0977rYvhBYYD/lX1e+6e29Pe+62L4QWGA/RmdXvi4Dvz1909i+Ak5gP5tvV75DDcI9JwvZvp41YD+bX1e+oMLGPaRC2b6hGGA/OR5XvvrTzD2kW9m+ogBgP3qYVr5w4NM9uD7ZvnD1Xz9wxVW+OXHbPXLe2L49/F8/yadUvlH74j23N9i+eBdgP6VNU74a5ek9T1DXvjBHYD/HzVG+U4/vPbsz1r7fiWA/505QvshU8z1+1tS+4+JgP5jeTr5iePU96m/TvlJDYT9Djk2+lJz2PRAj0r5en2E/SmtMvnvQ9j32/NC+PPNhP3iBS76FIvY9IgnQvpk7Yj9v20q+FZ/0PRBSz754dWI/BINKvudO8j194c6+Fp5iP4/jSr7+ffA9TTrPviOMYj8jQky+NV7wPTuy0L6bImI/4IFOvk/M8T0Q/tK+MXNhP+NtUb43zPQ9nhTWvkmAYD9h5lS+I1P5PSDb2b7fTl8/ctdYvjlP/z0LQt6+V+BdPwc8Xb7vTwM+KUTjvnkyXD92FWK+f4oHPpzh6L79QFo/sVNnvqc4DD6nBu++yQ1YP8Ikbb4GJBE++pD1vvGbVT9lWHO+jvIVPpEn/L4CCFM/aFZ5vnJMGj6RHQG/4otQP5Fzfr5R0B0+s58Dv8VqTj+v/4C+/ScgPt9QBb8g8Ew/6aaBvpwAIT4K7gW/eWRMP+mmgb6cACE+Cu4Fv3lkTD/ppoG+nAAhPgruBb95ZEw/LyLjvQmW9T3ZiPm+j5dbP6Ef471hj/U94on5vmyXWz98DeO98GD1PdWQ+b6Nlls//Nvivezi9D1vo/m+Q5RbPzxS3b0Fnuc90En7vgBsWz8d09a9/OjYPVq7/L7YV1s/z8DNvdwxxT0bJf6+lFxbP5o+xb3MkrI96jn/vkprWz/0Kry9FQafPcDx/77KkVs/TPm3vQZSlj2EDAC/Eq1bP0omtb1zf5A9NhIAv8PCWz8Eq7G9axWJPYkfAL9c2Vs/Dr6xvV34iD30LQC/ANFbP91PsL25CYY98SoAv5feWz/e4K69GhqDPXYmAL/c7Fs/Hk6uvaBkgT1OQAC/quNbP1bbq71xOHg960EAv572Wz9qMKy9yjJ6PfwzAL9z+1s/c3uuvQP5gz2xlv++CCFcP7xetL3pLJY9YeP8vtamXD97G9a92dTtPcxm8b75L14/gOP4vQGxLj69ddq+fDthP9YIAr5tDEs+pd7Lvjz2Yj87wAK+P8VKPs+Xzb4MkGI/DtACvn5jSD6Xvs++tTNiPzewAr68bkQ+qunSvpawYT9HLAK+LmU9Pr4k2L522GA/lfoAvvFRMz6e3N6+OsNfP/PK/b13fyc+6f/lvtWZXj/aBfi9Q/kaPq6l7L5lhl0/7TDxvVbdDj7uLPK+/KdcP3dg6r2imAQ+nTr2vpgMXD+BK+W92uz6PZmw+L5GtFs/LyLjvQmW9T3ZiPm+j5dbP0OJ573+IPQ95E7+vgAsWj9wZuS9cVftPXTE/r7mNFo/wcncvVq53j3iF/++dnlaPzFV1L0RlNA9xHb+vqUBWz/MQdC94U7KPXvc/b6GVVs/xrrRvRCIzT3Do/2+VFRbP8fJ1b0phdY9E+/8vmRWWz+s0tu9djnkPfGd+74kaFs/LyLjvQmW9T3ZiPm+j5dbP9TK6r1QjQQ+M6b2vh7tWz++XvG9DcENPtNs876cWlw/RMv1vR2XFD4zuvC+lbxcPyRp970ERRc+BprvvpzmXD9ozvW9E8cSPq+M8r4qUFw/EhHxvWbXCD66OPi+azZbP7fP6r28ef093bH8vk5rWj9Diee9/iD0PeRO/r4ALFo/LyLjvQmW9T3ZiPm+j5dbPy8i470JlvU92Yj5vo+XWz8vIuO9CZb1PdmI+b6Pl1s/LyLjvQmW9T3ZiPm+j5dbPy8i470JlvU92Yj5vo+XWz8vIuO9CZb1PdmI+b6Pl1s/LyLjvQmW9T3ZiPm+j5dbPy8i470JlvU92Yj5vo+XWz8vIuO9CZb1PdmI+b6Pl1s/LyLjvQmW9T3ZiPm+j5dbPy8i470JlvU92Yj5vo+XWz8vIuO9CZb1PdmI+b6Pl1s/LyLjvQmW9T3ZiPm+j5dbPy8i470JlvU92Yj5vo+XWz8vIuO9CZb1PdmI+b6Pl1s/LyLjvQmW9T3ZiPm+j5dbPy8i470JlvU92Yj5vo+XWz8vIuO9CZb1PdmI+b6Pl1s/LyLjvQmW9T3ZiPm+j5dbPy8i470JlvU92Yj5vo+XWz8vIuO9CZb1PdmI+b6Pl1s/LyLjvQmW9T3ZiPm+j5dbPy8i470JlvU92Yj5vo+XWz8vIuO9CZb1PdmI+b6Pl1s/LyLjvQmW9T3ZiPm+j5dbPy8i470JlvU92Yj5vo+XWz8vIuO9CZb1PdmI+b6Pl1s/LyLjvQmW9T3ZiPm+j5dbPy8i470JlvU92Yj5vo+XWz8vIuO9CZb1PdmI+b6Pl1s/LyLjvQmW9T3ZiPm+j5dbPy8i470JlvU92Yj5vo+XWz8vIuO9CZb1PdmI+b6Pl1s/LyLjvQmW9T3ZiPm+j5dbPy8i470JlvU92Yj5vo+XWz8vIuO9CZb1PdmI+b6Pl1s/viLjvc+o9j2b/fi+PbpbPzsi471PqPk9VHb3viEbXD+yHOO9dET+PZAX9b6kr1w/kgvjva0UAj7tBvK+nWxdPxjl4r1KewU+XG/uvthFXj9uneK9qR0JPjaD6r4xLl8/DCnivUu7DD5UfOa+BxhgP45/4b0pDRA+3JrivtL1YD++neC998gSPpIi3765umE/yoTfvUGOFD6BWty+M1tiP7lQ3r1rkBU+jTjavmfZYj+GIt29TB8WPtKQ2L6NPWM/FgzcvW5AFj5obde+m4VjP74e270C+RU+0djWvj6vYz/vatq9JU0VPu/d1r7Lt2M/ff/ZvVo/FD4riNe+MZxjPx9D271TRRM+77HZvpcdYz8QTN+9lMsSPvXt3b7dC2I/FKzlvaGyEj452OO+S3lgP43u7b0KHBM+0PvqvkR5Xj/tn/e9SBIUPs7q8r45IFw/9yQBvuWhFT7uO/u+6oRZP1y4Br4C1hc+78UBv0XBVj8iUwy+5rIaPlC/Bb9B8lM/hs8RvoonHj5pYAm/cTdRPx7PFr7uDiI+pH4Mvw+5Tj/OExu+3hgmPgYLD7/3kEw/aaEevinOKT6ZCBG/58tKP9I+Ib5N6Cw+u3ISv6F7ST8w2iK+kQcvPhNME7/Iqkg/0mcjvorOLz7tlRO/ZWJIP9JnI76Kzi8+7ZUTv2ViSD/SZyO+is4vPu2VE79lYkg/4tezvCgJ9jyeLeC+nfVlP1PHs7xcAPY8di7gvm71ZT/fUrO8X8P1PNQz4L5G9GU/UBayvOMd9TzkQeC+QvFlPx0RkLzUtOM8Ji3hvjLCZT9GhVO8GGDQPKOp4b51rWU/G9XXu75TtjwGtuG+crRlPw6bMLoPeJ084sXhvre2ZT+ZL7U74SmDPI9z4b70zWU/MTwIPBrfbjyZGOG+b+RlP2rMJjwrH188fMvgvgb3ZT8OO008dfFKPFmV4L55A2Y/SFZNPPaUSjy+0eC+vPRlP3u7XDydmEI8S57gvtgAZj+JJGw8NZY6PJ9n4L6kDWY/zyV0PFHONTyOw+C+6/ZlPySxhzwySCc8lpXgvu0AZj8dX4U8qw8qPLNi4L6NDWY/jZlqPBBJPTyc8d6+cWhmPy2gFjwMdW88/pfavhB0Zz8z+Im8e1DsPNj5zr5s/mk/rN5IvYImPz28Gr2+TkRtP9HWg72O6GQ9UKazvna6bj+5kIS9e7VkPRxxtr5gMW4/lIuCvSuTYT3fvbi+R8dtP7jTfb2MVlw9C+m7vvk0bT/OVHC9MgdTPcIAwb5TRWw//u1bvQKeRT2Xh8e+/QhrPzHHQr248zU9NWnOvoKuaT9P7ya9YGQlPRC11L4XZmg/4dEKvS1mFT2PyNm+5lVnP3w75Lxq5Qc9mWHdvteQZj/fH8G8Eg39PNJ6376DHGY/4tezvCgJ9jyeLeC+nfVlPwgawLzPNfI87O7uvnA1Yj+QMq286HPpPLvx7b49fmI/RDaDvJKc1jzgBuu+h0xjP9gSMrxeUMQ8rh7nvpFVZD/yzgu8yCa8PCQI5b5x32Q/J1scvBCBwDzhwOS+r+9kP1tlSrzBmMw8eOnjvn8gZT89GYi89u7ePNVs4r78dWU/4tezvCgJ9jyeLeC+nfVlP7Qu5Lwo9Ac9TindvlOeZj++XQi9YiQUPfnT2b57VWc/bIgYvfI1HT0PDde+f+tnP6zFHr1fxSA9UeXVvjYpaD+e1RW9F70aPbxf2r7fJ2c/PxcBvVhXDT0YiOO+KAVlP5N/1byZH/88/KHrvkkKYz8IGsC8zzXyPOzu7r5wNWI/4tezvCgJ9jyeLeC+nfVlP+LXs7woCfY8ni3gvp31ZT/i17O8KAn2PJ4t4L6d9WU/4tezvCgJ9jyeLeC+nfVlP+LXs7woCfY8ni3gvp31ZT/i17O8KAn2PJ4t4L6d9WU/4tezvCgJ9jyeLeC+nfVlP+LXs7woCfY8ni3gvp31ZT/i17O8KAn2PJ4t4L6d9WU/4tezvCgJ9jyeLeC+nfVlP+LXs7woCfY8ni3gvp31ZT/i17O8KAn2PJ4t4L6d9WU/4tezvCgJ9jyeLeC+nfVlP+LXs7woCfY8ni3gvp31ZT/i17O8KAn2PJ4t4L6d9WU/4tezvCgJ9jyeLeC+nfVlP+LXs7woCfY8ni3gvp31ZT/i17O8KAn2PJ4t4L6d9WU/4tezvCgJ9jyeLeC+nfVlP+LXs7woCfY8ni3gvp31ZT/i17O8KAn2PJ4t4L6d9WU/4tezvCgJ9jyeLeC+nfVlP+LXs7woCfY8ni3gvp31ZT/i17O8KAn2PJ4t4L6d9WU/4tezvCgJ9jyeLeC+nfVlP+LXs7woCfY8ni3gvp31ZT/i17O8KAn2PJ4t4L6d9WU/4tezvCgJ9jyeLeC+nfVlP+LXs7woCfY8ni3gvp31ZT/i17O8KAn2PJ4t4L6d9WU/4tezvCgJ9jyeLeC+nfVlP+LXs7woCfY8ni3gvp31ZT/i17O8KAn2PJ4t4L6d9WU/4tezvCgJ9jyeLeC+nfVlP+LXs7woCfY8ni3gvp31ZT/i17O8KAn2PJ4t4L6d9WU/j721vH5r9zxnf9++QB9mP6sMu7yzRvs8u53dvn2RZj+jPcO8qpcAPT3F2r7YO2c/W77NvBNbBD0pM9e+gw1oP2je2bwnrwg9qCbTvtP1aD91x+a850sNPU7hzr675Gk/gX/zvFvfET3dpsq+K8tqP1nz/ry8DhY9wbzGvkabaz+iAwS9AHoZPa9ow76ASGw/y5gGvU+ZGz3a/8C+RsRsP2S5B73Uuxw9H1e/vgEZbT+ADQi99E0dPQQqvr76VG0/bqkHvdVXHT18fb2+qXdtP8ugBr374Bw9wVe9vhWAbT8FBQW9re8bPTLAvb7DbG0/zeMCvUeIGj0dv76+xzttPzQfAr0LXxk96RXBvrvDbD++UgS9+RQZPQ1Kxb6B5Gs/KugIvfVuGT0NCcu+yKhqP6vhD73Dnxo9idbRvo4jaT+1Ahm9T7QcPc5A2b4GaGc/rAgkvR7AHz390eC+5o1lP52fML1j2SM90g/ovomyYz+9aj69TwspPUh97r7k+GE/WwJNvc0wLz33svO+f4JgPyVQW72cHzY9jUv3vilzXz/sT2i9H2I9PfdE+b5e014/0TtzvWgJRD0F/vm+F45eP/2ee725pkk9qun5vneFXj/hfIC96oVNPR6J+b7jll4/Vm6BvXryTj1IVvm+n6FeP1Zugb168k49SFb5vp+hXj9WboG9evJOPUhW+b6foV4/9EFlPnTC8jlcET4/F6IhPwtFZT7mj+85ShE+P+ahIT9RWmU+1K7ZOU4QPj8soSE/ypNlPmq4njk2DT4/tp8hPxZhaz4gCyS7O0o9P5P+IT8/rnE+7XyuuwcgPD/hwyI/eS96Pls+DLxuSjo/wg4kP7dOgT62BDe84dY4P7veJD/k5YU+wF9ZvEFINz8friU/+PWHPmQ0ZrzfgjY/RxsmP+pXiT5bmG28V/g1P79pJj9KHos+KP93vCh1NT+qmSY/JieLPgKceryppjU/lWEmP+LZiz49UH28/Vs1P0yNJj9ejYw+6bl/vAMQNT/wuSY/l/eMPi8ag7wwTDU/ZmEmPyE7jj7PIoa8HvQ0P9N7Jj9h/o0+LFOEvHPSND+0rSY/2FCMPlQGb7wm4DM/xQ8oP/iThz7RoSi8JxUxP6T5Kz+bXGA+CuK2O31vLj8axjI/xQsgPnL3szwIGy8/jlI2P8V6+T0FN7U8TREzPxMvND+5lgA+pyu3PCKDNT8njDE/aD4FPhPkuDypcjY/R14wP70aDD7Vbro8ZW03P2cDLz/2nBc+k3m6PBTEOD95/iw/hgImPjxRrzz4Zzo/gGYqP7yFNT48yJc8CvY7P060Jz9dmUQ+ZiRuPJMmPT8yUCU/MbFRPnZuHzzD3z0/0X0jP/r4Wz5mW6Y75SI+Py1aIj8nxWI+fz3hOjQePj/2yiE/9EFlPnTC8jlcET4/F6IhP/RBZT50wvI5XBE+PxeiIT+TgWE+zfIUO76pPj+wQiE/MhNZPo8nzDtYvT8/CbMgPwREUD64fCI8h4NAP3t9ID9PGUw+2EI7POzFQD/AgSA/kC5QPo83HTzs4kI/2JsdP7iaWT7/aaA7qzpHPzBFFz+QZWQ+JJekumH6Sj+NLBE/dpVtPkat3Lsz30s/NvkOP5Bmcz7x/Rq8b5lIPxvtEj/FM3Y+l6UavMqQQj/9jBo/Ted2PnsaAbwyZjw/CfMhPynNdj7r0OC7iYg5P788JT9AAnQ+K0PBu+BZOj/SkyQ/gfZtPr1qZbsqAjw/rj4jP+D6Zz6PQla6C3g9P8EXIj/0QWU+dMLyOVwRPj8XoiE/9EFlPnTC8jlcET4/F6IhP/RBZT50wvI5XBE+PxeiIT/0QWU+dMLyOVwRPj8XoiE/9EFlPnTC8jlcET4/F6IhP/RBZT50wvI5XBE+PxeiIT/0QWU+dMLyOVwRPj8XoiE/9EFlPnTC8jlcET4/F6IhP/RBZT50wvI5XBE+PxeiIT/0QWU+dMLyOVwRPj8XoiE/9EFlPnTC8jlcET4/F6IhP/RBZT50wvI5XBE+PxeiIT/0QWU+dMLyOVwRPj8XoiE/9EFlPnTC8jlcET4/F6IhP/RBZT50wvI5XBE+PxeiIT/0QWU+dMLyOVwRPj8XoiE/9EFlPnTC8jlcET4/F6IhP/RBZT50wvI5XBE+PxeiIT/0QWU+dMLyOVwRPj8XoiE/9EFlPnTC8jlcET4/F6IhP/RBZT50wvI5XBE+PxeiIT/0QWU+dMLyOVwRPj8XoiE/9EFlPnTC8jlcET4/F6IhP/RBZT50wvI5XBE+PxeiIT/0QWU+dMLyOVwRPj8XoiE/9EFlPnTC8jlcET4/F6IhP/RBZT50wvI5XBE+PxeiIT/0QWU+dMLyOVwRPj8XoiE/9EFlPnTC8jlcET4/F6IhP/RBZT50wvI5XBE+PxeiIT/0QWU+dMLyOVwRPj8XoiE/9EFlPnTC8jlcET4/F6IhP/RBZT50wvI5XBE+PxeiIT/0QWU+dMLyOVwRPj8XoiE/9EFlPnTC8jlcET4/F6IhP/RBZT50wvI5XBE+PxeiIT/0QWU+dMLyOVwRPj8XoiE/UeNkPlNIPTqJpT0/5CgiP1/TYz65wr06gH48Px6XIz/SHGI+n2coO8bFOj/wsiU/g8tfPrZRgzsepjg/FEEoPwT3XD7AZbk7xk02P/gEKz8GyVk+XT7yO+HuMz87wi0/kX9WPi+OFDy5vjE/Tj4wP89qUz4skCw8NvQvP6dBMj+j61A+rbc+PKDFLj/8lzM/modPPgMFSDz4Yy4/7g80P58gTz6Z9Uk8NqguPwbVMz8DPU8+QyRIPGFCLz/dPDM/xNxPPtmiQjxCIzA/qFQyPw//UD5WcTk8TzwxP58oMT93o1I+fXYsPM5/Mj8gxC8/BstUPmZ7Gzzo4DM/0DEuP/s0Vz7HjAc8t3U1PxddLD+nk1k+XNTkOwZNNz9WOCo/As9bPhD4uTsFSDk/Q+InP57JXT5quZA7PkE7P2+EJT+jY18+rANXO8kPPT/bUCM/sXtgPqM6GTsEij4/+H4hP2cDYT6NkPI6b3A/P8phID8ftGA+7lHiOk6gPz+PLyA/y1xfPngO8TrTEj8/EvYgP8zlXD5sVxI7irY9P8XFIj9BoFk+Yd8uO2jfOz8gKiU/8jlWPgqzNjtrMzo/QlInP803Uz4m7Sw71PY4P6zsKD/2HlE+A2UdO7c/OD/r3Sk/VVpQPtAoFzvT/zc/NjIqP1VaUD7QKBc70/83PzYyKj9VWlA+0CgXO9P/Nz82Mio/2dzUvWyK2zzgVvS+lEhfP67d1L2zads841b0vphIXz/o49S9ToTaPHxX9L6PSF8/EvXUvWYV2DyBWfS+WEhfPzX71r28BJY8YOn0vrcmXz/mQ9m9ao8ZPGWl9b55814/GTrcvXCgJruqjfa+xapeP+ij3r3iQF68b872voiIXj9Z4uC9/6vMvIHN9r4rb14/BM3hvSg39rzvx/a+eGJeP4Nq4r257wi988D2vtJZXj+aIuO9sDcavTuG9r7bW14/xBTjvSk9Gr32V/a+4WheP4ll471IIyG951H2vmBkXj9ztuO99wcovR1L9r7cX14/acbjvS2HK70M6/W+gndePzhL5L2Hmze96671vll8Xj+IQOS9O5k1vWjm9b7Xbl4/dgDkvUaNJ71rZfe+pBBeP4Gz471cnAG9k0L7vg8VXT8ePN69XryaPGyTAL/ekFs/Su/PvQwNsz1dBfy+By9cPyqswr0X5Ps9KUDyvlj+XT/gn8C9zI77PcGM8L46fV4/wNrAvdzC9T0Pi/C+y5ZeP+Oswb1QBuw9EujwvvGkXj+CPsO9Oo3aPWOw8b6JsF4/cA/GvSj/wT01gPK+FMlePwEtyb3i6qQ9ODXzvkXqXj+XaMy9JQ+GPdC7876FDF8/DKrPvZ29UD3HEfS+dihfP+BX0r0rHB49mz70vsU7Xz/uLdS9itj1PMlR9L66RV8/2dzUvWyK2zzgVvS+lEhfP9nc1L1sits84Fb0vpRIXz/gOdO9d4ADPUx/877ofV8/W5LPvT3uMj3tm/G+s+1fP3XWy71xuWE9q6rvvqVWYD/YG8q9KcB2PUPD7r5shGA/XlLJvRRgaz303ey+zBNhP1XUx72qGk09HYLovrFYYj9bX8e9Jz8iPdwb5L6ummM/ZtjJvRqg5TxjDeO+YfJjP62ez71HGo487tPnvgG7Yj+JI9a9BlYPPL2f777SoWA/kyXbvXgXSztBf/a+57JeP5Yi3b1wZo06Y175vlreXT9k4Nu9QK+nOwaa+L57GV4/3g/ZvZATZTwO4/a+bJhePw0w1r1l07o8NyT1vqkSXz/Z3NS9bIrbPOBW9L6USF8/2dzUvWyK2zzgVvS+lEhfP9nc1L1sits84Fb0vpRIXz/Z3NS9bIrbPOBW9L6USF8/2dzUvWyK2zzgVvS+lEhfP9nc1L1sits84Fb0vpRIXz/Z3NS9bIrbPOBW9L6USF8/2dzUvWyK2zzgVvS+lEhfP9nc1L1sits84Fb0vpRIXz/Z3NS9bIrbPOBW9L6USF8/2dzUvWyK2zzgVvS+lEhfP9nc1L1sits84Fb0vpRIXz/Z3NS9bIrbPOBW9L6USF8/2dzUvWyK2zzgVvS+lEhfP9nc1L1sits84Fb0vpRIXz/Z3NS9bIrbPOBW9L6USF8/2dzUvWyK2zzgVvS+lEhfP9nc1L1sits84Fb0vpRIXz/Z3NS9bIrbPOBW9L6USF8/2dzUvWyK2zzgVvS+lEhfP9nc1L1sits84Fb0vpRIXz/Z3NS9bIrbPOBW9L6USF8/2dzUvWyK2zzgVvS+lEhfP9nc1L1sits84Fb0vpRIXz/Z3NS9bIrbPOBW9L6USF8/2dzUvWyK2zzgVvS+lEhfP9nc1L1sits84Fb0vpRIXz/Z3NS9bIrbPOBW9L6USF8/2dzUvWyK2zzgVvS+lEhfP9nc1L1sits84Fb0vpRIXz/Z3NS9bIrbPOBW9L6USF8/2dzUvWyK2zzgVvS+lEhfP9nc1L1sits84Fb0vpRIXz/Z3NS9bIrbPOBW9L6USF8/2dzUvWyK2zzgVvS+lEhfP9nc1L1sits84Fb0vpRIXz/Z3NS9bIrbPOBW9L6USF8/wx7VvetK3DyCyfS+AyhfPzDM1b1wz948QPz1vmjQXj/Evda9La/jPCa0976TUV4/JszXveRy6zyHtfm+rrtdP/HT2L1rSfY8Qcj7vnYeXT8zuNm9/O4BPUi7/b5CiFw/RmTavV2oCT21Zv++agVcPzTM2r1qphE93FUAvwKgWz9G7dq9ZAIZPcK5AL/+X1s/p9TavewgHj1g2AC/xkpbPx2Y2r1TEiE9OcMAv/5VWz+IStq9ZOUiPZGUAL85cVs/VvXZvStdIz0+UgC/AJlbPw2h2b3HPiI9gQEAvzzKWz/UVdm9H0wfPYlO/74CAlw/NBzZvdo/Gj1lkP6+iz1cP/BJ2b2DqhQ9N3D+vvlJXD/QG9q9Rk0QPc10/74w/ls/lHzbvQ4kDT2+vwC/M2JbP1Bh3b1ZKQs9jD8Cv9B4Wj9Ky9+9fFUKPUI5BL/ZPlk/e8rivQqeCj2ftQa/zKlXP6dD5r1a/As9OqwJv+a4VT8Zf+q9LGAOPcA6Db9ST1M/iNPvvWK/ET2IhRG//EVQP0409r0xIhY90HgWv0OYTD9/OP29j9caPby0G79afUg/ZB8CvokHHz1ItSC/p1hEP2gxBb6ydSI9YOwkv1CtQD9JVQe+wsskPWHKJ79CFT4/mx0Ivl+rJT3Z0yi/4R89P5sdCL5fqyU92dMov+EfPT+bHQi+X6slPdnTKL/hHz0/NuO7vM2b/jz9oQm/xqBXP7vUu7xZgf48K6IJv7OgVz/+bru8mMj9PEqjCb9IoFc/ZVq6vFrT+zwwpgm/Pp9XPyunnLy8Msc8oMYJvzGeVz9fuXa8TjmNPEq1Cb8bulc/OqodvLEWADz4WQm/rAJYP8hKmLsIFoS65OoIvz1OWD+Aqvk5psMmvBZECL9StFg/Pk01OxUUabwl5Ae/bexYP9t/jDtNrIq86JwHv1MVWT9dR8o7Tr+mvDhLB7+TQlk/cAnKO7Aep7yQWge/8zhZP/zC4juAOrK8MzEHvx9QWT9kavs7+Ve9vGoGB7/vZ1k/9qQDPHKww7zRDge/GGFZP1ICGTyfmNe8Sc4Gv46DWT8CnBU8GO7TvGjKBr8Ch1k/+K77Ox5wurzVpwa/MKNZP6HPdztsi228lwgGv/YSWj9+BpS8wkHcPORaBL80+Vo/jMY6vSmsqD2sKPy+OH5dP3iSbL04Z+U9J9ryvocMXz/Z5W29ggrlPXnS9L6agl4/GAlrvU/83z19sva+lRVeP7G0Zb3/jdc941T5vgyAXT8PaVu9q5bIPRuL/b46kFw/rMdLvTxTsz2ZXwG/72RbP6xxN71hk5o9r/0Dvz0yWj+MQCC9c6eAPUw9Br8AKFk/bnkIvX3tTz1u5we/vWNYP8Il5rw1fCY9HPMIvyPrVz9Xfse8u+8JPSd6Cb8KsVc/NuO7vM2b/jz9oQm/xqBXPzbju7zNm/48/aEJv8agVz9tvM68UaMQPTtdCb9evVc/hgv3vF7CNj2/jwi/ORhYP4GpDr0z4lw9snkHv7KXWD909Ra9wThuPRnkBr8G3Vg/nAUTvTbOYz364Ae/3kxYP9WGB72xwEg9Fd8Jv6ArVz8Tu+u8h2YjPYmkC7+jMFY/71DDvAzA9jzAqgy/3KlVP9KNnbypELI8F9UMv9SmVT9qs3u8myCAPFgZDL9LMFY/TF5PvPLdRDxe0gq/pQtXP8CqPrykKTE8bhgKv0SFVz/r4Vu8/yVlPJseCr+CfFc/8fmNvNWsqzztCwq/UHpXP3Cqrbyls+Q8D80Jv4GPVz8247u8zZv+PP2hCb/GoFc/NuO7vM2b/jz9oQm/xqBXPzbju7zNm/48/aEJv8agVz8247u8zZv+PP2hCb/GoFc/NuO7vM2b/jz9oQm/xqBXPzbju7zNm/48/aEJv8agVz8247u8zZv+PP2hCb/GoFc/NuO7vM2b/jz9oQm/xqBXPzbju7zNm/48/aEJv8agVz8247u8zZv+PP2hCb/GoFc/NuO7vM2b/jz9oQm/xqBXPzbju7zNm/48/aEJv8agVz8247u8zZv+PP2hCb/GoFc/NuO7vM2b/jz9oQm/xqBXPzbju7zNm/48/aEJv8agVz8247u8zZv+PP2hCb/GoFc/NuO7vM2b/jz9oQm/xqBXPzbju7zNm/48/aEJv8agVz8247u8zZv+PP2hCb/GoFc/NuO7vM2b/jz9oQm/xqBXPzbju7zNm/48/aEJv8agVz8247u8zZv+PP2hCb/GoFc/NuO7vM2b/jz9oQm/xqBXPzbju7zNm/48/aEJv8agVz8247u8zZv+PP2hCb/GoFc/NuO7vM2b/jz9oQm/xqBXPzbju7zNm/48/aEJv8agVz8247u8zZv+PP2hCb/GoFc/NuO7vM2b/jz9oQm/xqBXPzbju7zNm/48/aEJv8agVz8247u8zZv+PP2hCb/GoFc/NuO7vM2b/jz9oQm/xqBXPzbju7zNm/48/aEJv8agVz8247u8zZv+PP2hCb/GoFc/NuO7vM2b/jz9oQm/xqBXPzbju7zNm/48/aEJv8agVz8247u8zZv+PP2hCb/GoFc/wgq8vCiz/zwWgwm/IbRXP4ShvLwQhwE9bCsJv8rqVz/05r28w3oEPWCgCL/PQFg/oBPAvJvSCD315we/uLFYPz5Bw7xjhw49DQwHv6g2WT/iXse8JFwVPTQcBr+PxVk/ljLMvJfaHD3kLQW/PVFaP3tk0bxIWyQ9o1sEvznKWj9Ze9a8CAMrPYbDA7+kH1s/6HDavGdjLz1xhwO/Sj9bPyhC3bwgnTE9E5sDvwMxWz+Phd+8froyPcLZA7/YCVs/p/vgvL2SMj2UPwS/LsxaP+tk4bzo/jA9oMgEv0t6Wj8sfuC8QNUtPUJxBb87Flo/mv3dvJrlKD1CNga/vqFZP/bG3LwsRiM98oUHv/HVWD/Smd+8rz0ePQS1Cb+bd1c/kenlvMziGT0Dlwy/cZpVP7Al77yTURY9W/0Pv5FTUz+0uvq8DKwTPcG4E7+VulA/OgcEvZEcEj2JmRe/5OpNPw9oC73cvxE9Nngbv0/+Sj9NDhO92M0SPR0eH79NH0g/cn0avS+XFT04USK/cIFFPwhkIb1OSRo97uAkvx5WQz/cOSe9YSUgPYK/Jr+BtEE/QZErvS0SJj0nASi/9ZRAP2J3Lr2FXys9d70ov8ToPz8HHDC9DCwvPXIZKb/Nkj8/JK8wvfiSMD2KNCm/Dnk/PySvML34kjA9ijQpvw55Pz8krzC9+JIwPYo0Kb8OeT8/o+EkvcEzqjvJOr++Ez9tP5HYJL0vFao7ITu/vgg/bT/BmCS9rT+pO/g8v77YPm0/S+sjva/8pjt4Qb++bj5tP6tXEb2THlU7PS+/vsFObT/qO/m8SEWhOtaYvr4YeW0/MtLBvL7QvrqJQb2+lcptP668jrz/n4K7wQm8vu4Qbj/25TO8hubYu4Rtur5jZ24/vggFvODR/rufhbm+7pVuP4uMy7scFAy8c9y4vkS3bj9r0Hq7ykEcvHwuuL4X2W4/qs96u8uPHLzkY7i+xs5uPyc0PLux7yK8xwO4vkPhbj8cPvu6o1EpvOOgt74r9G4/RRS8utUmLbxF2Le+aOluP+v+fDm8sji8bFa3vuABbz8CVhy4iHo2vJY4t760B28/hSUBu9cSJ7ykWba+4TJvPyFY57uGx/y7TYCzvpa8bz8f7Ay917KNO0vyrb4AnHA/v4yQvZzhqDwNTKO+LeZxP5detr1s2vI8gXqdvv1mcj93Y7e9Q0jyPDlToL6C7HE/sPS0vfXo6zw/XKK+fp5xP/ePsL3xWOE80RClvp84cT/Ibqi9K7/OPExPqb6Rl3A/WFWcvbJptDyila6+7ctvP0GVjb0xNJY8veyzvsj4bj/7Anu9avNtPNeBuL45QW4/kStbvfM1Mzye2bu+h7xtP62QP734mwI8oeu9vgZtbT+CKyy92tTCOxzwvr6jSG0/o+EkvcEzqjvJOr++Ez9tP6PhJL3BM6o7yTq/vhM/bT/DCDG9pVnSOxsQv75pPm0/6WdLvdufFTxNPb6+H1JtP8krZb0jmkI8h868vs+BbT+8pnC9okNXPD32u745oG0/tYdrvXVTSjyl6b6+WA9tP0QJXb3bSSk8nRLFvoPbaz/u3ka99Pf4O4G5yr68vWo/sYwsvWM+mjuDTM2+iERqP3rjEr2RxRU7vYTLvu65aj+R8fq8KlMeOth5xr4w2ms/LsndvGtCsLnoo8C+WhZtP33+0ry1RyW6u8a9vnGsbT/Ir+W8CC+SOa01vr7zkW0/jVYHvRZQFTsL5b6+ymNtP0ixG71iSYw7hDW/vqZGbT+j4SS9wTOqO8k6v74TP20/o+EkvcEzqjvJOr++Ez9tP6PhJL3BM6o7yTq/vhM/bT+j4SS9wTOqO8k6v74TP20/o+EkvcEzqjvJOr++Ez9tP6PhJL3BM6o7yTq/vhM/bT+j4SS9wTOqO8k6v74TP20/o+EkvcEzqjvJOr++Ez9tP6PhJL3BM6o7yTq/vhM/bT+j4SS9wTOqO8k6v74TP20/o+EkvcEzqjvJOr++Ez9tP6PhJL3BM6o7yTq/vhM/bT+j4SS9wTOqO8k6v74TP20/o+EkvcEzqjvJOr++Ez9tP6PhJL3BM6o7yTq/vhM/bT+j4SS9wTOqO8k6v74TP20/o+EkvcEzqjvJOr++Ez9tP6PhJL3BM6o7yTq/vhM/bT+j4SS9wTOqO8k6v74TP20/o+EkvcEzqjvJOr++Ez9tP6PhJL3BM6o7yTq/vhM/bT+j4SS9wTOqO8k6v74TP20/o+EkvcEzqjvJOr++Ez9tP6PhJL3BM6o7yTq/vhM/bT+j4SS9wTOqO8k6v74TP20/o+EkvcEzqjvJOr++Ez9tP6PhJL3BM6o7yTq/vhM/bT+j4SS9wTOqO8k6v74TP20/o+EkvcEzqjvJOr++Ez9tP6PhJL3BM6o7yTq/vhM/bT+j4SS9wTOqO8k6v74TP20/o+EkvcEzqjvJOr++Ez9tP6PhJL3BM6o7yTq/vhM/bT+j4SS9wTOqO8k6v74TP20/o+EkvcEzqjvJOr++Ez9tP6PhJL3BM6o7yTq/vhM/bT+j4SS9wTOqO8k6v74TP20/i+8kvUC2qzvKxb6+kFZtP8s0Jb2ySLA704G9viyXbT8a5yW91y64OxGUu755+G0/JjgnvYycwzt8I7m+knFuP/ZCKb0wa9I7Gl62vnP4bj8IAiy9WPDjO2J7s77ogW8/z00vvQr19jvGu7C+5QFwP0XiMr3F4wQ8nmauvg5scD++XDa9XxsNPGTHrL4PtHA/j/g4vTdbEjwWLay+gs1wP6uwOr1G1BQ8aG2svpbAcD9y9Tu9EdwVPMMhrb4un3A/G6A8vQVIFTxdPK6+q2twP/6KPL3u7xI876+vvjEocD98jju9S6gOPCJwsb6v1m8/A385vQg+CDzCcbO+6nhvP4t5N719igA8CmW2vuzrbj8SqDa9tWvxO9LQur7MEW4/j+k2vTAJ4jvfYcC+cfVsP8QXOL2YidM7B7nGvuykaz/tBjq9TJ7GOwxrzb7+Mmo/BIQ8vewVvDt0/dO+IrloPxNsP71SarQ73A7avvBOZz96bEK9mLSwO4n93r6/HmY/OCNFvRdosjv7/eG+s2BlP3RYR71TSLo7UpLivhg6ZT+xwki96JTGO3r/4L7Rm2U/jDZJvbB41DsD/t2+LVZmPzgMSb1ExeE75HvavmssZz8BtEi9U6frOyql177p1mc/9IlIvUJI7zsMi9a+ZRhoP/SJSL1CSO87DIvWvmUYaD/0iUi9QkjvOwyL1r5lGGg//DbZvrhNCbqMsSU/OSEiP6g12b5U4Aq69LElP0IhIj8JLNm+4ZIWuki0JT8YIiI/uhHZvr7cNro1uiU/1iQiP3kR1r5oBJW7kPAlPxPrIj8ajNK+otETvFjiJT/7GiQ/cpfNvrAzc7y0lSU/yfAlP6T2yL5zQp68o5YlP4JRJz8/68O+7Bi/vEWaJT+nwig/npXBvrgRzbzqjCU/sXcpPyEAwL7vINa8Wn4lP0H2KT+KG76+SZ7fvJqSJT+DZyo/Nzq+vmMi3rwSvyU/LTQqP8tpvb4FceK8TrMlP0Z4Kj+6l7y+pavmvIamJT9zvSo/TWy8vlvi5bzy9yU/lXoqPzQXu74Gq+u8ygkmPxXFKj+uMLu+8B7svPPbJT966io/t9+7vsxs77wnlCQ/X/UrP8Sjvb7yGPq8xdwgPyPzLj/cr86+cv+1vK2rFz+OYTI/bbPpvrLhuruatQ4/f4UxP+SA+77uZU47MGINP2huLD+w6Pu+GpT9O3UOED+CCyo/3r76vtpmGjxIfxE/nj0pPyO1+L656zQ8P0UTP1BzKD9lEvW+FC5TPD79FT8qXyc/AH/wvgWWYzz6ZRk/Cu4lP1Fe677+Elo83ewcP3h3JD/HFua+5ic4POoZID93RyM/2zPhvryo/ztYoCI/hX8iP6gh3b5+CHw7gV4kP7omIj8MStq+imw5OoFdJT+sGiI//DbZvrhNCbqMsSU/OSEiP9+j377WTZs8rxUzP5eyED/KLd2+cCxnPMvgMT+0JRM//WnXvlTtYzve5C4/rssYP8Ig0b7c7N67xX4rP6u1Hj+PF86+NdU4vLzTKT+odCE/bSLPviRIKrwakCk/XmchP/TK0b6ihgK8I8ooP3JdIT+WWtW++JaRu2h+Jz+GjiE//DbZvrhNCbqMsSU/OSEiP3Xu3L6/f0E7m4kjPxkPIz+YI+C+00qwOx1cIT9oICQ/Dm7ivlzt2TuEpx8/wAAlP0RP477WGOY7+vgeP4BbJT+P2eK+W68gPJKJIj+lASI/5rrhvu/8bzwO2Sk/trUaP3Ne4L49qZI8W2EwP/i2Ez/fo9++1k2bPK8VMz+XshA//DbZvrhNCbqMsSU/OSEiP/w22b64TQm6jLElPzkhIj/8Ntm+uE0JuoyxJT85ISI//DbZvrhNCbqMsSU/OSEiP/w22b64TQm6jLElPzkhIj/8Ntm+uE0JuoyxJT85ISI//DbZvrhNCbqMsSU/OSEiP/w22b64TQm6jLElPzkhIj/8Ntm+uE0JuoyxJT85ISI//DbZvrhNCbqMsSU/OSEiP/w22b64TQm6jLElPzkhIj/8Ntm+uE0JuoyxJT85ISI//DbZvrhNCbqMsSU/OSEiP/w22b64TQm6jLElPzkhIj/8Ntm+uE0JuoyxJT85ISI//DbZvrhNCbqMsSU/OSEiP/w22b64TQm6jLElPzkhIj/8Ntm+uE0JuoyxJT85ISI//DbZvrhNCbqMsSU/OSEiP/w22b64TQm6jLElPzkhIj/8Ntm+uE0JuoyxJT85ISI//DbZvrhNCbqMsSU/OSEiP/w22b64TQm6jLElPzkhIj/8Ntm+uE0JuoyxJT85ISI//DbZvrhNCbqMsSU/OSEiP/w22b64TQm6jLElPzkhIj/8Ntm+uE0JuoyxJT85ISI//DbZvrhNCbqMsSU/OSEiP/w22b64TQm6jLElPzkhIj/8Ntm+uE0JuoyxJT85ISI//DbZvrhNCbqMsSU/OSEiP/w22b64TQm6jLElPzkhIj/8Ntm+uE0JuoyxJT85ISI//DbZvrhNCbqMsSU/OSEiP/w22b64TQm6jLElPzkhIj/8Ntm+uE0JuoyxJT85ISI/lzfZvg3vWbqOPSU/NpciP5A82b4qpNu66fgjP9fcIz+QStm+c3FEu2D6IT+t0CU/bmnZvhGsnbv+UR8/J1QoPz6o2b7xx+u7aBMcPwlCKz9oH9q+feInvN5eGD9OaC4/0uLavqpPYrxeaBQ/jYoxP3Pb274Ep5G8QWwQP+B6ND9aF92+1MSzvP/DDD8e8jY/qLXevn9x07zz4wk/qJo4P11I4L6JVPC8faYHP+S/OT8g1OG+sdQGvfKuBT9vqjo/02PjvpPCFL1DIAQ/KkI7P2om5b7JEyC9gTkDP8ZROz+jfua+RJonveYIAz+SAzs/Ny/nvpQ+Kr0ypgM/8Fs6P6Ry574+MCi9hvcEP69YOT+epue+bpwivR3FBj/H/jc/4NDnvqEYGr0w8gg/mls2P8j0575+dQ+9F1oLP+WDND8mFOi+G7EDvb3SDT9mkzI/AjDovgfD77zYLxA/fKswP/5I6L7lPdq8GkYSP+bwLj8IZui+KJjIvPPyEz9rgi0/g4novqCYvLwnFBU/nYEsP+qo6L5gl7i8sHwVP4gdLD8Twui+jv24vH5+FT9bEyw/P9Hovrc5uby4fxU/Fw0sPwXZ6L4NWLm8Z4AVP9cJLD/f2+i+G2O5vKiAFT+kCCw/R9zovsdkubyxgBU/eAgsP0fc6L7HZLm8sYAVP3gILD9H3Oi+x2S5vLGAFT94CCw/X5gWPhh97LznKPK+7UReP/KXFj7iney8uijyvvVEXj8ylRY+MoPtvPMn8r4MRV4/8o0WPhfx77w3JvK+LUVeP5r5FT6Qcxi9XE7yvpgsXj9DaBU+t+k7vbKT8r6gBF4/h6AUPtyDar3/1/K++c1dPxWVEz5Vtoq9T4Lyvg6/XT/cbxI+H7WgvQfh8b7Zu10/xfMRPh9fqr3PkPG+9bldP/GdET4jxrC9S1jxvtO4XT9OEhE+YtG4vfXj8L7vw10/lfUQPo7nuL2pufC+TdBdP7THED4UE7y9R5vwvsHPXT8fmRA+cTy/vTt88L5Cz10/LkcQPn79wL15GPC+h+ddP2TVDz5clsa9z7bvvsvyXT8/BRA+U5XFvSXw777x5F0/mkwRPuSKvr2gfvG+FIRdPx5/FD6IVqu9aJz1vqV+XD+Wfxw+L6wJvfNq/r4IhFo/xXAgPmgTFz1yBf6+82paPy+yHz5fM5g9EW72vvP+Wz/KQB8+XN2WPUhp9L6vl1w/Xg4fPqoxkD1fHPS+FcFcP0vTHj78PIU9ww30vk7jXD9cbB4+YpVjPVsf9L6LDl0/el4dPnKkLT3QG/S+fUxdP0QYHD5TNd08t+fzvruRXT8goBo+QFYzPNyM87480l0/0iIZPtZhiruOG/O+rgVePxzYFz4Xtoq8eabyvrgpXj+y7xY+AwvSvEJM8r47Pl4/X5gWPhh97LznKPK+7URePw21DT7Zwvq8biDhvksLYz/mMQ4+1oQSvRs7475nc2I/21APPor8P72mfOe+VjFhP3pkED4qxmu9RCzrvo4HYD/K0xA+ohF/vYal7L53il8/SlcRPgiQc71qI+2+m3BfPzatEj7e+VS9sWruvscqXz+DhhQ+5SkpvSkv8L5Ww14/X5gWPhh97LznKPK+7UReP4ycGD6PgoW8OxX0vo+9XT89Uho+pDCsuwi29b5rQF0/6X8bPoEqKTvv0/a+X+RcP4fwGz66ErY7Ej73vs7AXD+0/xk+wjG8NWEN9L5sul0/QmEVPt3eSrxgouy+D+dfP8s6ED55CMy8XsrkvqcSYj8NtQ0+2cL6vG4g4b5LC2M/X5gWPhh97LznKPK+7UReP1+YFj4Yfey85yjyvu1EXj9fmBY+GH3svOco8r7tRF4/X5gWPhh97LznKPK+7UReP1+YFj4Yfey85yjyvu1EXj9fmBY+GH3svOco8r7tRF4/X5gWPhh97LznKPK+7UReP1+YFj4Yfey85yjyvu1EXj9fmBY+GH3svOco8r7tRF4/X5gWPhh97LznKPK+7UReP1+YFj4Yfey85yjyvu1EXj9fmBY+GH3svOco8r7tRF4/X5gWPhh97LznKPK+7UReP1+YFj4Yfey85yjyvu1EXj9fmBY+GH3svOco8r7tRF4/X5gWPhh97LznKPK+7UReP1+YFj4Yfey85yjyvu1EXj9fmBY+GH3svOco8r7tRF4/X5gWPhh97LznKPK+7UReP1+YFj4Yfey85yjyvu1EXj9fmBY+GH3svOco8r7tRF4/X5gWPhh97LznKPK+7UReP1+YFj4Yfey85yjyvu1EXj9fmBY+GH3svOco8r7tRF4/X5gWPhh97LznKPK+7UReP1+YFj4Yfey85yjyvu1EXj9fmBY+GH3svOco8r7tRF4/X5gWPhh97LznKPK+7UReP1+YFj4Yfey85yjyvu1EXj9fmBY+GH3svOco8r7tRF4/X5gWPhh97LznKPK+7UReP1+YFj4Yfey85yjyvu1EXj9fmBY+GH3svOco8r7tRF4/X5gWPhh97LznKPK+7UReP1+YFj4Yfey85yjyvu1EXj9fmBY+GH3svOco8r7tRF4/3CkXPg8o57wzK/O+m/ldP/a+GD7i+te8Gvv1vmYlXT+wKhs+i8q/vJ5K+r592Vs/m0IePj5Zn7z/0v++ZiNaPzbeIT69h2+8CSsDv9UNWD/Z1iU+soUVvITPBr9AoVU/yespPu1VU7tMwgq/IOVSP/0TLj6wfC07edoOv9ftTz8YOTI++xYDPN4HE7+/w0w/DBs2PgtPSzxRSBe/hWlJP/P+OT6WjYE8opAbv5vhRT9V9T0+jS2YPLDvH78KHEI/IOdBPonpqDysVSS/RCM+P/aNRT58TLQ8S7Iovz4HOj9FC0k+w1O3PGXALL/6BjY/pkJMPmvirzx1RzC/bmQyPywrTz7naKE87kkzv+MqLz8r1FE+TgqRPAvsNb+FPiw/DDJUPhU9fzyTKTi/Ma0pPzY8Vj7uCFw8vgE6v6iAJz+/7lc+IDY6PJV4O78TvCU/jEtZPhd1Gzymlzy/ZVokP7BaWj4OXAE80W09vzJOIz8EKFs+iD3bO3EPPr+fgSI/zMNbPr1fwzvykT6/xtshPyJCXD5cCL07EAg/v6dFIT8CoVw++9a/O1FoP78wyyA/mNpcPkyJwTuZoj+/uYAgPzH4XD7UZ8I7gsA/v21aID8YA10+u7nCO4PLP79RTCA/pgRdPnnFwjsZzT+/SUogP6YEXT55xcI7Gc0/v0lKID+mBF0+ecXCOxnNP79JSiA/wQ6rPWngLr1GoQS/s6RZPxwSqz197S69EaEEv7+kWT9iKas9B0kvvXifBL8lpVk/S2irPXtBML39mgS/UaZZP6nAsT3cakq9dvUDv8bfWT8MW7g9qShnvQYMA78POlo/xYLAPc6ahr14lgG/ItFaP8adxz0ROZi98iYAv71hWz/sZ849J1SqvUj5/L6FClw/GTzRPb9Gsr1eV/u+UV5cP5AK0z2Skbe9DjT6vuSYXD+sUNU9ZTu+vZzW+L5W3Fw/HGzVPTFJvr0X8/i+ttNcP5ZC1j2R7sC9cFf4vhfzXD+2FNc9tpPDvVy59770El0/17LXPS0Exb2Cofe+HRJdP3Av2T3ktcm9gKD2vipDXT8v19g9UOHIveuv9r4+Q10/t1zWPScaw73zDfe+WkddP5EXzz1wurO9x5D3vo9yXT+aSKg9WnlFvcul/b42CV0/vBZXPfVW+TsVI/6+e9JdPzFUFz2X9R89inX7vqaSXj+sQRo9YFUgPahm/b49A14/kcQgPXEZFj0PwP6+36JdP80XKz0AyQQ9nz8Avx0lXT+hJT09wq3LPB+NAb8bZFw/VuBVPR5haDybAgO/IIBbP1q5cj1lxfI65EAEv2aqWj+vEog9ZUA0vKgHBb90Clo/q7KVPVpFvbxXRQW/HrFZP97goD3Z/we90hkFv+2VWT+HV6g9lVkkvYzJBL/RnFk/wQ6rPWngLr1GoQS/s6RZP59erz3SvS29/pwHvyK/Vz+dRrM9vuM/vQbxBr+wDlg/ww67PReqZr2NCgW/kPtYP7OLwT34+YW99ZUCv+w2Wj/CBcQ9zDuOvdJLAb+E3Vo/wB/CPfmLib0XrAG/TLdaP7bfvD2iWHq96ZUCvzlcWj/+5LQ9aUJXvW2tA7+19Fk/wQ6rPWngLr1GoQS/s6RZP92ioD0NTga9VkAFvx2AWT8mM5c9Z3vFvDeEBb+ZhFk/m2WQPa7zkrz5iwW/UpxZP2/MjT17dn+8MYYFv7ypWT8IYZM9MtagvPFABr9iIlk/EFqfPQYN67yRVQe/eENYP/+Zqj0L3Bu9YLAHv+TPVz+fXq890r0tvf6cB78iv1c/wQ6rPWngLr1GoQS/s6RZP8EOqz1p4C69RqEEv7OkWT/BDqs9aeAuvUahBL+zpFk/wQ6rPWngLr1GoQS/s6RZP8EOqz1p4C69RqEEv7OkWT/BDqs9aeAuvUahBL+zpFk/wQ6rPWngLr1GoQS/s6RZP8EOqz1p4C69RqEEv7OkWT/BDqs9aeAuvUahBL+zpFk/wQ6rPWngLr1GoQS/s6RZP8EOqz1p4C69RqEEv7OkWT/BDqs9aeAuvUahBL+zpFk/wQ6rPWngLr1GoQS/s6RZP8EOqz1p4C69RqEEv7OkWT/BDqs9aeAuvUahBL+zpFk/wQ6rPWngLr1GoQS/s6RZP8EOqz1p4C69RqEEv7OkWT/BDqs9aeAuvUahBL+zpFk/wQ6rPWngLr1GoQS/s6RZP8EOqz1p4C69RqEEv7OkWT/BDqs9aeAuvUahBL+zpFk/wQ6rPWngLr1GoQS/s6RZP8EOqz1p4C69RqEEv7OkWT/BDqs9aeAuvUahBL+zpFk/wQ6rPWngLr1GoQS/s6RZP8EOqz1p4C69RqEEv7OkWT/BDqs9aeAuvUahBL+zpFk/wQ6rPWngLr1GoQS/s6RZP8EOqz1p4C69RqEEv7OkWT/BDqs9aeAuvUahBL+zpFk/wQ6rPWngLr1GoQS/s6RZP8EOqz1p4C69RqEEv7OkWT/BDqs9aeAuvUahBL+zpFk/wQ6rPWngLr1GoQS/s6RZP8EOqz1p4C69RqEEv7OkWT/BDqs9aeAuvUahBL+zpFk/KOaqPfDpLL3p4QS/VH9ZPw5uqj1uUye9A5cFvyIWWT8olqk9fW4eveClBr/hd1g/J0yoPVqGEr1l7we/ALZXP0aSpj0vBAS97VUJvzjhVj+ijKQ9awnnvIHCCr9MBlY/JluiPYSDxLzpKwy/rSlVP0h4oD1vDKK8iZgNvylFVD8/dJ89f2OCvBcaD7+OSlM/8bWfPQtuU7wzzxC/mSJSPxIgoT0P1i28pacSv8TXUD92GqM9ZAQQvChjFL/PmE8/IZulPejx9rtr9xW/X25OPyNiqD1dn+a7xF0Xv3FfTT87l6s9A2Dvu+GrGL+3XEw/tVyvPWCCCrwN8Rm/mFpLP3l3sz2niCa8XTsbvyxPSj8Fkrc9UyJEvISPHL/qN0k/P5u7Pa0IYrxt7B2/lxVIP7V9vz1z9X68i0wfv7bsRj+JH8M9ANyMvI2lIL8HxkU/9WLGPU+jmLzG6CG/g65EP4wnyT0NZKK8LQQjv522Qz8WSMs99sypvOriI7/V8UI/RqHMPVKYrrxgcCS/23NCP9wWzT0Nd7C815kkv2ZOQj/jD809yrWwvF2NJL8FWUI/dAvNPfvbsLyxhSS/jl9CPxwJzT2a77C8uoEkv+9iQj88CM092fawvEKAJL8vZEI/HQjNPd73sLwMgCS/XWRCPx0IzT3e97C8DIAkv11kQj8dCM093vewvAyAJL9dZEI/AUhMPVjERbwKRc6+8e9pP8tQTD1z1EW8v0TOvvnvaT/OjUw990RGvBtCzr5T8Gk/4jJNPaB2R7x2Os6+YfFpPx4DXj2bsme8juLMvrIraj8X5289Y5mFvN7qyr5ChWo/y2mDPT40nbx5yse+qhRrP8nmjT09NLO8aPXEviuRaz8PXZg9vujJvLu5wb7cHWw/QNWcPTTi07yNH8C+uWNsP9HDnz1zi9q8SgG/vlSUbD9BhaM9CPXivLG/vb7CyGw/Z6qjPRUH47zH8b2+S75sP1AYpT07YOa8cVi9vjjYbD+ng6Y9OrrpvLW8vL5/8mw/3YOnPayP67za0Ly+NOtsP1Qeqj0ZiPG88ui7vl8QbT8WjKk9OnjwvEjfu74wFG0/JoilPRQb6bx6jbu+kTFtP9mBmj0WutW8BjG6vmuYbT+XHEk9Vx5jvOkdvL5uvG0/e804PAFvXDuajru+li9uP7/OFbz4CFs8PBe7vrFCbj8k5gy8JWNcPDsXvr7Kqm0/vkPauzKPTzwGxr+++lVtP/a6Zrsxszk86NbBvrzsbD8oiQM7yqQSPKPkxL5iTWw/ZQ4gPBCQtjvJZMi+P49rPzVNmTx1GeA6AJHLvsvYaj8+hOQ8HI8Wu3rEzb5/Tmo/ZicVPZQaxrsuyc6+VQBqP9DYMT1o/BW8LdzOvmPmaT8oL0U9ZdQ4vEd+zr4R6mk/AUhMPVjERbwKRc6+8e9pP/fOWT1lmUK8tVrcvhOoZj/rEmQ9FmBZvKcM2r7tKGc/bAN5PQfthLxqMtS+2GtoP27VhT3KOZy82VrNvrHbaT8JtIk9gJOmvI/yyb6Pjmo/lM6GPaG6oLw2g8q+IXdqP0MJfj1QW5G8bdXLvrxBaj8YS2c9z2J3vDZFzb4YDGo/AUhMPVjERbwKRc6+8e9pP5tnMD1eAxS89o3Ovtb4aT/qgRc9F6zQuyI2zr4cH2o/NKIFPZFmkrsho82+C0tqP5ec/TxOS3W7L1nNvkJfaj/4Lw09K9Wiu4B40L72pWk/orMsPWiM/LvDW9a+eDloP1LaSz1/viy8J9HavnwTZz/3zlk9ZZlCvLVa3L4TqGY/AUhMPVjERbwKRc6+8e9pPwFITD1YxEW8CkXOvvHvaT8BSEw9WMRFvApFzr7x72k/AUhMPVjERbwKRc6+8e9pPwFITD1YxEW8CkXOvvHvaT8BSEw9WMRFvApFzr7x72k/AUhMPVjERbwKRc6+8e9pPwFITD1YxEW8CkXOvvHvaT8BSEw9WMRFvApFzr7x72k/AUhMPVjERbwKRc6+8e9pPwFITD1YxEW8CkXOvvHvaT8BSEw9WMRFvApFzr7x72k/AUhMPVjERbwKRc6+8e9pPwFITD1YxEW8CkXOvvHvaT8BSEw9WMRFvApFzr7x72k/AUhMPVjERbwKRc6+8e9pPwFITD1YxEW8CkXOvvHvaT8BSEw9WMRFvApFzr7x72k/AUhMPVjERbwKRc6+8e9pPwFITD1YxEW8CkXOvvHvaT8BSEw9WMRFvApFzr7x72k/AUhMPVjERbwKRc6+8e9pPwFITD1YxEW8CkXOvvHvaT8BSEw9WMRFvApFzr7x72k/AUhMPVjERbwKRc6+8e9pPwFITD1YxEW8CkXOvvHvaT8BSEw9WMRFvApFzr7x72k/AUhMPVjERbwKRc6+8e9pPwFITD1YxEW8CkXOvvHvaT8BSEw9WMRFvApFzr7x72k/AUhMPVjERbwKRc6+8e9pPwFITD1YxEW8CkXOvvHvaT8BSEw9WMRFvApFzr7x72k/AUhMPVjERbwKRc6+8e9pPwFITD1YxEW8CkXOvvHvaT8BSEw9WMRFvApFzr7x72k/AA9LPb1oQ7wtYM6+JetpP2mURz3Esjy8Rq3OvnndaT95+EE99AMyvG0Sz75WzGk/m1A6PQWxI7wCaM++QcBpPyvQMD3ZKBK8zYTPvvrBaT/U4CU9Ehr8u6RIz74P2Gk/8EYaPeBE0rtBns6+UwZqP+fVDj0PDKi7IcPNvlw+aj/tvAQ9tICAu1/qzL4tdGo/KQ77PMV1Q7tMTcy+oZpqP3QK8Tz/tBO7dMjLvkW6aj++4Og81CLZuqe9yr4k9mo/XNjiPII0oLqlIsm+6E9rPzLM3zzWNYm6GwLHvknEaz/4+N88426TugjExL6TPGw/SIjkPGedw7pE28K+pqBsPwk97DzFQwa78HPBvlrobD/5yPQ8ZjUtu5p0wL4OGm0/B/b9PC/vVLsH7b++/DJtP2S7Az0Ys3u7XN+/vgQzbT9Ocgg9juyPuxM9wL41HW0/UOAMPZPzn7vY5cC+LvhsP/W7ED2GUa27TqnBvtfNbD8LuRM90pW3u1ZNwr5Fqmw/QoUVPVZevrtrkMK+R5tsPyzDFT2uLMG7rCrCvvuvbD8gPBU9MbnBu7CBwb7l0mw/vekUPdIOwrvQGsG+FehsP0y/FD3qOsK74eXAvvfybD+krxQ9KUvCu1zSwL759mw/ZK0UPZVNwruQz8C+jvdsP2StFD2VTcK7kM/Avo73bD9krRQ9lU3Cu5DPwL6O92w/DM8gv6DmMT3sPsk+tosrP5fOIL/N3zE97D/JPuCLKz8MyyC/fK0xPRRGyT6YjSs/LMEgv7IiMT0UVsk+uJIrP4JwH78ahCA9LU7KPoGTLD/PyB2/inQNPf3hyj69/C0/lGMbvy1+6TzYS8s+2hQwP/pIGb9T2MI8FS3MPp61MT/g/Ba/012gPFIqzT6xajM/xuMVv3FNkjwejc0+pTw0P10jFb9ZIIk80MXNPqnNND/8SxS//ct+PPNJzj6+WjU/3GoUv0PfgDzYkM4+By01P44GFL9E5Xg8sKXOPtt5NT8woRO/1ylwPPS4zj6cxzU/xKsTvyyQcTzDTc8+gZQ1P6QVE78KYmU8fq7PPp3zNT83EBO/gZdkPNhazz7mDzY/fOUSv/68XjwSBM0+7ts2P2BJEr8xc0s8MzPGPog3OT8vFRa/mMG0PO7qsT4RQjs/Hckev/LvJT1kWJg+HII5P7k0J7/HjVY9DuePPsyAMz++jii/xE9rPY5llT5u/zA/fJMov0TIcT0Dwpg+JjowP3ZMKL/3GHc9JwydPvuELz+4oCe//957Pce2oz4VnC4/08Mmv136fD2J96s+SXEtP6XHJb91RHY9BWG0Pt1ILD/8mCS/5DxpParjuz79eis/3E0jv18GWD2c3cE+SSQrP60ZIr+ci0U93QLGPtgvKz8xLSG/uGA3PURuyD6mais/DM8gv6DmMT3sPsk+tosrPwzPIL+g5jE97D7JPraLKz9MlyG/z5Y7PZL9yD4B2Co/hT0jv+eMUD2SO8g+s2UpP97NJL9ormQ9ZjDHPoMVKD/NfSW/7HBtPbedxj6qhyc/Wnkmv8r5dD1NLMs+diElP3hGKL8FLYA9P9vUPpwcID/7Sim//kCAPdj43T5H4xs/20kov6aBbD3+HeI++ZsbP96eJL/R4EI9gQ/fPoDGID9ZXB+/GIQRPY911z40rSg/hGUav9Wk0Txrbs8+IccvP7cqGL9N5K887KzLPnLUMj8hBRm/vaS9PJ35yz43ADI/+BAbv7dB4TxGccw+bQswP0GFHb8O/Ag9qVrMPkzPLT8nqx+/zU4iPd7vyj4HLCw/DM8gv6DmMT3sPsk+tosrPwzPIL+g5jE97D7JPraLKz8MzyC/oOYxPew+yT62iys/DM8gv6DmMT3sPsk+tosrPwzPIL+g5jE97D7JPraLKz8MzyC/oOYxPew+yT62iys/DM8gv6DmMT3sPsk+tosrPwzPIL+g5jE97D7JPraLKz8MzyC/oOYxPew+yT62iys/DM8gv6DmMT3sPsk+tosrPwzPIL+g5jE97D7JPraLKz8MzyC/oOYxPew+yT62iys/DM8gv6DmMT3sPsk+tosrPwzPIL+g5jE97D7JPraLKz8MzyC/oOYxPew+yT62iys/DM8gv6DmMT3sPsk+tosrPwzPIL+g5jE97D7JPraLKz8MzyC/oOYxPew+yT62iys/DM8gv6DmMT3sPsk+tosrPwzPIL+g5jE97D7JPraLKz8MzyC/oOYxPew+yT62iys/DM8gv6DmMT3sPsk+tosrPwzPIL+g5jE97D7JPraLKz8MzyC/oOYxPew+yT62iys/DM8gv6DmMT3sPsk+tosrPwzPIL+g5jE97D7JPraLKz8MzyC/oOYxPew+yT62iys/DM8gv6DmMT3sPsk+tosrPwzPIL+g5jE97D7JPraLKz8MzyC/oOYxPew+yT62iys/DM8gv6DmMT3sPsk+tosrPwzPIL+g5jE97D7JPraLKz8MzyC/oOYxPew+yT62iys/DM8gv6DmMT3sPsk+tosrPwzPIL+g5jE97D7JPraLKz8MzyC/oOYxPew+yT62iys/NkcgvxriLD2MUsg+rFQsPwnUHr8GeB89WMbFPkZyLj+cpxy/OCEMPW3kwT7yiTE/Q/QZv5ua6jyb+Lw+6UA1P4/xFr9yo7o8I163Pjg8OT8K3hO/GbSMPDOFsT6VIz0/Cv4QvxdxSTxW8Ks+caVAPxqHDr9QFRA8ZFOnPhJ+Qz+8uwy/U5LVO4UcpD4xeEU/OGULv4k0qTvHdaI+IsJGP4HoCb8DNoU7/j2hPo0KSD/srQe/X49AO7uQnz7s5Ek/iOEEv2Q97Dp7hp0+7iRMP702Ar96wPU6tQydPlLyTT/MwAC/Eq90O9N6nz5bZU4/11IBvwho4zsXxKQ+sP1MP4mSBb8BGC482QKrPvXvSD/S3gy/XBp6PBYvsT5XfkI/MWoUv9HOtzw2n7g+PfY6Px4RGr/RHPc8ASG/PhyTND8LiB2/J0AVPTr0wz6vKTA/4lsfvz4MJD1JwMc+LmEtP3sjIL++8Cg9qmzLPs6QKz8tCyC/+LMlPcKTzj4DuSo/OrYev+QjGz0YndA+bWIrP1CaG7+16As9lCDRPsUbLj9Eoxa/22wAPbN50T5sWjI/HcIQv9cMAz3jXdQ+2lA2P9AZC79qNw498J3YPjdpOT/t/Aa/yFIbPYHX3D7XJzs/3nQFvzt8IT1d3N4+eaI7P950Bb87fCE9XdzePnmiOz/edAW/O3whPV3c3j55ojs/gS2GPuIL773eL8++9EheP/gshj6iE++9YC/PvgVJXj96KYY+nknvvVQsz75ZSV4/OiCGPvPb771mJM++IUpeP2NZhT5TBf+9YpfOvlZFXj80kIQ+rrEHvqMPzr5lNV4/SnuDPmlUEr5MRc2+XCFeP9kogj4wChy+ixDMvo8wXj+Iq4A+fuQlvimQyr7NTV4/AAaAPrIuKr412cm+alteP1Qrfz4ZBC2+Vl3JvqVkXj/x3H0+fpIwvp6fyL60el4/Jqx9Pn+dML7kfsi+AoVeP3g4fT4UAzK+2j/IvqOJXj/Jw3w+XmczvlsAyL5Qjl4/myd8PjMvNL7vn8e+76ReP9Inez7ZpTa+OxDHvgy3Xj/5gXs+vjM2vmNHx74zql4/7vJ9PoUSM74Uyci+zE9ePyQugj6yfyq+gdvMvq5WXT/MR4w+0wz3vVHY2L6V4Fo/zCSUPsInUr3XbuC+FXNZP+7nlD4iDEK8eQHevuxQWj/46pM+ZYVOvHPZ276QBls/ymCTPvM/hLzcGtu+qElbP0Cxkj6hkrO8clfavluPWz8vkpE+WnEDvQkx2b4M81s/XNKPPjcqPL1orde+tHJcP1K6jT4kVH29dtzVvlX5XD97hos+he6fvbHy076Ucl0/PnmJPvCdvr01JdK+bdFdP/DEhz5Qzte9taDQvvITXj+Sm4Y+jczovS2Uz75xO14/gS2GPuIL773eL8++9EheP4Ethj7iC++93i/PvvRIXj+lTYY+ADHnvQsKz74ibl4/wpKGPuG51b3nqs6+n79eP/7Whj7EBsS9+jvOvhQQXz8S9oY+TOm7vYEEzr4cNF8/23KFPt+kwr3258u+HNNfP3HdgT7IJdS9nBbHvusuYT+rxns+u/frvVowwr6RcWI/yU93PjGjAr5TNsC+B71iP7VNeT5JAw6+N9vCvoibYT9oy34+MREXvs2px75vzF8/F/WBPswIHb4A48u+YDdePyAJgz60MR++S6fNvtCNXT8NFoM+G4ccvsyCzb7Asl0/1VWDPkN4Fb4yUc2+rwJeP3Xxgz4RYgu+ZG3NvghOXj/DIYU+00v/vfBFzr5QX14/gS2GPuIL773eL8++9EheP4Ethj7iC++93i/PvvRIXj+BLYY+4gvvvd4vz770SF4/gS2GPuIL773eL8++9EheP4Ethj7iC++93i/PvvRIXj+BLYY+4gvvvd4vz770SF4/gS2GPuIL773eL8++9EheP4Ethj7iC++93i/PvvRIXj+BLYY+4gvvvd4vz770SF4/gS2GPuIL773eL8++9EheP4Ethj7iC++93i/PvvRIXj+BLYY+4gvvvd4vz770SF4/gS2GPuIL773eL8++9EheP4Ethj7iC++93i/PvvRIXj+BLYY+4gvvvd4vz770SF4/gS2GPuIL773eL8++9EheP4Ethj7iC++93i/PvvRIXj+BLYY+4gvvvd4vz770SF4/gS2GPuIL773eL8++9EheP4Ethj7iC++93i/PvvRIXj+BLYY+4gvvvd4vz770SF4/gS2GPuIL773eL8++9EheP4Ethj7iC++93i/PvvRIXj+BLYY+4gvvvd4vz770SF4/gS2GPuIL773eL8++9EheP4Ethj7iC++93i/PvvRIXj+BLYY+4gvvvd4vz770SF4/gS2GPuIL773eL8++9EheP4Ethj7iC++93i/PvvRIXj+BLYY+4gvvvd4vz770SF4/gS2GPuIL773eL8++9EheP4Ethj7iC++93i/PvvRIXj+BLYY+4gvvvd4vz770SF4/gS2GPuIL773eL8++9EheP4Ethj7iC++93i/PvvRIXj+BLYY+4gvvvd4vz770SF4/FXSGPt5s8L1Hq8++iRteP2U9hz7n9fO9HwXRvlGcXT84eIg+eq74vfIV075u2Vw/chKKPman/b0vuNW+aOBbPxr8iz6ICwG+z83YvlW8Wj95K44+ALMCvlVE3L4IdFk/8aCQPr+WA77KF+C+TAhYP2mCkz4KmQO+MErkvtJwVj+nc5c+PekCvg8Q6r5nNlQ/PdycPiY8Ar5fEvK+TfxQPxCfoz5DkgG+dRv8voa0TD/H86s+LZoAvokTBL/8KUc/9gm1PkD4/r0UnAq/RaZAP83mvT7Ddv29DNcQvwHYOT8Wk8Q+ok7+vRpmFb8SZzQ/WpjGPtxpAb6rtBa/RqcyP5u2wT5qYgK+oV0Tv6KwNj+rPLc+uA0AvlM5DL807j4/2EmrPsHP+L1OuAO/d7VHP3/6oD67WPG9cyP4vlbFTj/SBZo++CDsveB27b4oRlM/mMOWPlLh673Kfei+Vz1VP5W4lj5iQfG9vK7ovt8ZVT+M9Jg+Y236vfWQ7L5kdlM/Bp+dPnk9A77DL/S+vy9QP5oYpT6hKQq+l6H/vj31Sj+zIK8+V7URvt/zBr9ux0M/bE66PgbdGb5OUQ6/Xmo7P6UsxD7YqSG+qpcUv85yMz967so+Po4nvm3WGL8Gki0/D23NPi70Kb4EYBq/2lArPw9tzT4u9Cm+BGAav9pQKz8Pbc0+LvQpvgRgGr/aUCs/JIsgPuAoKr4FvPW+w9ZYPzaMID4PLCq+0br1vubWWD+MkyA+aEIqvhuy9b7u11g/VqcgPhF/Kr49mvW+y9pYPyyKIj4k3TC+RMjyvt49WT8nWiQ+8cw3vmRW775owFk/gF4mPvzjQL4zW+q+g4RaP7z5Jz4tM0m+7Z/lvig6Wz+TRyk+4qZRvj5z4L4AAlw/C74pPopPVb5WCN6+8mBcP5MBKj4YvVe+lmHcvu2hXD9XWCo+tMZavohd2r4c7lw/kmcqPmjKWr7pdNq+XudcPwWAKj6A/1u+dpnZviQJXT8flio+GTRdvhW82L4xK10/Y74qPqfWXb5teti+Ji9dPxTsKj5i9l++5wbXvkdlXT921io+zZhfvh0u1760Yl0/3zQqPjQNXb7NM9i+o1NdP2omKD7EQla+74bavoRFXT8P1Rw+KHEwvlPi6b7w8ls/6qwGPi0u7r18DPi+/mRbP1lS8z1qJKi9YxX9vhByWz+mZ/U9G2OnvWEB/76A3Fo/mZj4PZLVrL13tv++R4haP8J2/T2QJ7a95y8Av+8hWj9h4AI+YrzGvWWDAL+Nj1k/lkcIPobj3b0YmwC/ufJYP4lVDj5SY/i9ODwAv6p5WD/QLxQ+18MJvvqn/r6AQFg/60IZPhlOFr67/vu+yElYP5ovHT4anCC+2w/5vp9/WD9vqx8+N5Ynvluw9r7Iu1g/JIsgPuAoKr4FvPW+w9ZYPySLID7gKCq+Bbz1vsPWWD8vXx8+0tgmvovi9r4iulg/z6YcPmF8H74fNPm+rohYP7rBGT65BBi+BTn7vnxqWD8DZBg+ZZYUvg8K/L5PY1g/MQIaPl/cFr7Ilvy+ug5YP2y0HT6hHR2+IN38vgGIVz9A4CE+6k8mvmpU+77wWlc/VY0lPiziML7p5ve+raVXPz9IKD49Eju+yCLzvlFVWD9crSk+ZaFDvjJ/7b4JW1k/CsgpPh+cSb4cOui+EW5aPwOKKT4D5Eu+CdTlvmzxWj9kDyk+2IxJvi4T574Gxlo/GbEnPmNqQ76aQeq+SFdaP1l/JT4pxzq+PHzuvtXFWT+8qiI+Fugwvsrz8r6fL1k/JIsgPuAoKr4FvPW+w9ZYPySLID7gKCq+Bbz1vsPWWD8kiyA+4CgqvgW89b7D1lg/JIsgPuAoKr4FvPW+w9ZYPySLID7gKCq+Bbz1vsPWWD8kiyA+4CgqvgW89b7D1lg/JIsgPuAoKr4FvPW+w9ZYPySLID7gKCq+Bbz1vsPWWD8kiyA+4CgqvgW89b7D1lg/JIsgPuAoKr4FvPW+w9ZYPySLID7gKCq+Bbz1vsPWWD8kiyA+4CgqvgW89b7D1lg/JIsgPuAoKr4FvPW+w9ZYPySLID7gKCq+Bbz1vsPWWD8kiyA+4CgqvgW89b7D1lg/JIsgPuAoKr4FvPW+w9ZYPySLID7gKCq+Bbz1vsPWWD8kiyA+4CgqvgW89b7D1lg/JIsgPuAoKr4FvPW+w9ZYPySLID7gKCq+Bbz1vsPWWD8kiyA+4CgqvgW89b7D1lg/JIsgPuAoKr4FvPW+w9ZYPySLID7gKCq+Bbz1vsPWWD8kiyA+4CgqvgW89b7D1lg/JIsgPuAoKr4FvPW+w9ZYPySLID7gKCq+Bbz1vsPWWD8kiyA+4CgqvgW89b7D1lg/JIsgPuAoKr4FvPW+w9ZYPySLID7gKCq+Bbz1vsPWWD8kiyA+4CgqvgW89b7D1lg/JIsgPuAoKr4FvPW+w9ZYPySLID7gKCq+Bbz1vsPWWD8kiyA+4CgqvgW89b7D1lg/JIsgPuAoKr4FvPW+w9ZYPySLID7gKCq+Bbz1vsPWWD8kiyA+4CgqvgW89b7D1lg/6pQgPp7cKr53JfW+EvhYPw2uID7nrCy+qJ/zvqJNWT+RxiA++yMvvjZ+8b4ExVk/BdAgPnzSMb5wFe++yEtaP6/KID5qWTS+4cTsvizMWj/szCA+JG02vjX86r6EK1s/4wMhPjvTN76ROuq+G0pbPyeuIT6qQTi+PQHrvk4HWz/02iM+yes3viv57r5c3lk/swIoPsRyN77fYva+rZ5XP6EYLT6RITe+NF7/vuzAVD/U4TE+u/E2vhHqA7/J6lE/3cM1PlZVN74TUQe/L4JPP/GFOD5ndDi+p6EJv7zDTT+eyjo+NB46vqM/C79rc0w/sk89PjzhO77D1Qy/QR1LP3RLPz41ZTq+d7cOvwbEST/RJD8+Qg41vgb+D7/dK0k/uiM8PuopL75OaQ+/VxZKP9k2Nz4Ifiq+UC0Nv6QuTD9KtzI+860nvum1Cr/BQU4/GV4xPssmJ76k2gm/9e1OP2r9Mz4fyii+MEQLv4PBTT9NJDk+X/wrvn8WDr+cXEs/y8s/PnauML48nhG/9zNIPwzBRj5EyDa+6QsVv9LiRD/J2Uw+NEk+vrOkF782DEI/PXlRPtr/Rr4FFRm/lQ1AP8+MVD5B7U++OoAZv73pPj+sMFY++hdXvghnGb9uYT4/sr1WPtj6Wb4kSRm/Cjs+P7K9Vj7Y+lm+JEkZvwo7Pj+yvVY+2PpZviRJGb8KOz4/VWZfNGTPUD8yAy4zVNLIMmjPUD/rIpszeHyevLDKLL0feOG+pYhlP1tsnryqziy9TnfhvtiIZT+O+528guosvftw4b5kimU/q8mcvAk2Lb1TX+G+t45lP3DSebxXFzW9/PjevlAjZj9bNDW8rpo9vUXe275T32Y/g5i5u6iuSL0aS9e+kuxnPx8zV7pRzFK9ny7Tvi/WaD8z/oI7HgpdvUuyzr4+zWk/U3TGO2BtYb25jsy+mkBqP4nc8jvCVWS9XBbLvvGOaj/CJxU8Y/1nva5jyb7852o/1XEVPPsGaL2gk8m+p91qPxhWIDzCeGm9QM7IvggGaz+LKys85ulqvckGyL6iLms/2i4xPOy2a71/AMi+4i5rPwVNRDy5Rm69TcrGvhJtaz+L3EA8ptBtvdbPxr6NbGs/gcIoPMyZar3N58a+3mtrPzVnzjuqAWK9NqPGvh2Faz8774W8fxIzveYSzr5HC2o/xOtSva6L0rzm4dS+FFtoP5pzlL0gCma8AcTYvkknZz8/6ZS9/X5kvHg/3L4jU2Y/go+RvY0fdLx3zN2+ZftlPynNi71aSYe8hILfvr2dZT/kf4G9ZYSevLPR4b5zIWU/4fdlvT64vrz0GuS+PalkP/JRRL04ruK8+Kvlvv5bZD94wSG90V8DvXEI5r4pVmQ/1cUBvfCuE73MLeW+ipdkP5v0zrwazyC9aJ7jvsX/ZD/wgKu8GpQpvYYd4r4DYGU/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT+YnK+8744ovR2Z4r59QWU/N9nVvNsTH72q2OS+CrFkP3vM/LyxVxW9B8PmvlgyZD/YWwe9UtMQvRyF577z/mM/gzEBvdvWE715qum+UXRjP7JP4bxL5xu964Dtvst4Yj/3ALK8OoonvdWG776A8mE/iI54vL39NL25re2+4G1iP+fAELztE0K9GXDnvvwEZD/Ubm27I9lMvcau3r5aKWY/BMwvuVYXVL21b9a+XhZoPyUpjjptxVa98bnSvvXsaD+J6Z652tVTvVoT1L5EoWg/nweCu5kqTL01Zde+DeRnP8KSFrxBaEG9XIfbvjjyZj9jLXm8ASo1vSVf376GCmY/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT94fJ68sMosvR944b6liGU/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT94fJ68sMosvR944b6liGU/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT94fJ68sMosvR944b6liGU/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT94fJ68sMosvR944b6liGU/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT94fJ68sMosvR944b6liGU/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT94fJ68sMosvR944b6liGU/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT94fJ68sMosvR944b6liGU/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT94fJ68sMosvR944b6liGU/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT94fJ68sMosvR944b6liGU/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT94fJ68sMosvR944b6liGU/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT94fJ68sMosvR944b6liGU/eHyevLDKLL0feOG+pYhlP3h8nrywyiy9H3jhvqWIZT94fJ68sMosvR944b6liGU/fRmbvAekLb0AjOC+bMJlP2ROkryp1y+9SxjevoVaZj/IVIa8Gdcyvf2L2r7UMmc/zpJyvNAdNr0XVta+0i1oP13HWbzWQjm9ePDRvm0taT9Ir0S8M/07vd3jzb4ME2o/FLM0vFQhPr37xMq+N8BqP2DQK7zTXz+9SjDJvpwWaz+PoyK8iLNAvd5Oyr5r2Go/nfUQvDs+Q72B+M2+7gpqP/l78Lvg/ka9nPTRvpklaT8LXL27jU5LvQiK077sxmg/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4gzaP7VZD7+1Com/8SHGP7VZD7+1Com/SEyzP7VZD7+1Com/wkSiP7VZD7+1Com/OsSTP7VZD7+1Com/joOIP7VZD7+1Com/mDuBP7VZD7+1Com/Ykp9P7VZD7+1Com/Ykp9P7VZD7+1Com/Ykp9P7VZD7+1Com/hUnlsoVJ5bL0BDW/9AQ1P4VJ5bKFSeWy9AQ1v/QENT8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/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AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAP5IMgT+SDIE/AACAP5QDhD+UA4Q/AACAP/SeiD/0nog/AACAP6OYjj+jmI4/AACAP5KqlT+SqpU/AACAP7GOnT+xjp0/AACAP/D+pT/w/qU/AACAPz61rj8+ta4/AACAP4xrtz+Ma7c/AACAP8zbvz/M278/AACAP+q/xz/qv8c/AACAP9rRzj/a0c4/AACAP4rL1D+Ky9Q/AACAP+pm2T/qZtk/AACAP+ld3D/pXdw/AACAP3xq3T98at0/AACAP8BZ2z98at0/AACAP+iJ1T98at0/AACAP4SOzD98at0/AACAPyH7wD98at0/AACAP05jsz98at0/AACAP5hapD98at0/AACAP4x0lD98at0/AACAP7pEhD98at0/AACAP1+9aD98at0/AACAP/KrSj98at0/AACAP018Lz98at0/AACAP4ZVGD98at0/AACAP7xeBj98at0/AACAPyB+9T58at0/AACAPy077T58at0/AACAPy077T58at0/AACAPy077T58at0/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utbk/6lwQQLRZD797gqw/6lwQQLRZD79IT58/6lwQQLRZD79IT5k/6lwQQGaz/r6utZ0/6lwQQABNuL5IT6k/6lwQQDjNY76utbk/6lwQQDXNI757gsw/6lwQQDfNY75IT98/6lwQQAFNuL6tte8/6lwQQGez/r5HT/s/6lwQQLRZD7+utf8/6lwQQLNZD79HT/s/6lwQQLRZD7+ste8/6lwQQLRZD79GT98/6lwQQLRZD796gsw/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/6lwQQLRZD7+utb8/x1UQQLRZD78EhME/0yMQQLRZD7/oRsY/PJwPQLRZD78pAs0/MpQOQLRZD7+ZudQ/4+AMQLRZD78Jcdw/fVcKQLRZD79KLOM/MM0GQLRZD78t7+c/KhcCQLRZD7+Dvek/uef0P7RZD790D+c/YGTeP7RZD7+//t8/DoDDP7RZD7+cAdY/ghanP7RZD79Djso/ggOMP7RZD7/qGr8/mEVqP7VZD7/IHbU/TKBKP7VZD78SDa4/pM4+P7RZD78DX6s/lpVQP7RZD78DX6s/8AZ9P7RZD78DX6s/wGabP7RZD78DX6s/CEq4P7RZD78DX6s/toLOP7RZD78DX6s/LmbXP7RZD78DX6s/VszRP7RZD78DX6s/osnCP7RZD78DX6s/UQ6tP7RZD78DX6s/okqTP7RZD78EX6s/qF1wP7RZD78EX6s/StY8P7RZD78EX6s/qF8RP7RZD78EX6s/hLTmPrRZD78EX6s/IE3QPrRZD78EX6s/IE3QPrRZD78EX6s/IE3QPrRZD78EX6s/hUnlsoVJ5bL0BDW/9AQ1P4VJ5bKFSeWy9AQ1v/QENT8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/ViaAP1YmgD8AAIA/V5eAP1eXgD8AAIA/AlCBPwJQgT8AAIA/VU2CP1VNgj8AAIA/T4yDP0+Mgz8AAIA/7wmFP+8JhT8AAIA/NMOGPzTDhj8AAIA/G7WIPxu1iD8AAIA/pdyKP6Xcij8AAIA/zzaNP882jT8AAIA/mcCPP5nAjz8AAIA/AHeSPwB3kj8AAIA/A1eVPwNXlT8AAIA/o12YP6NdmD8AAIA/3IebP9yHmz8AAIA/rtKeP67Snj8AAIA/FjuiPxY7oj8AAIA/Fr6lPxa+pT8AAIA/qlipP6pYqT8AAIA/0QetP9EHrT8AAIA/i8iwP4vIsD8AAIA/1Ze0P9WXtD8AAIA/rnK4P65yuD8AAIA/F1a8PxdWvD8AAIA/Cz/APws/wD8AAIA/iyrEP4sqxD8AAIA/lhXIP5YVyD8AAIA/Kv3LPyr9yz8AAIA/Rd7PP0Xezz8AAIA/57XTP+e10z8AAIA/DYHXPw2B1z8AAIA/tzzbP7c82z8AAIA/4+XeP+Pl3j8AAIA/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tFkPvxnErL4gwuK9tFkPvxnErL6ACWC9tFkPvxnErL5AFw69tFkPvxnErL5AFw69tFkPvxnErL5AFw69tFkPvxnErL6FSeWyhUnlMvQENT/0BDU/hUnlsoVJ5TL0BDU/9AQ1PwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD9vNoA/bzaAPwAAgD851oA/OdaAPwAAgD8Z2oE/GdqBPwAAgD/KPIM/yjyDPwAAgD8I+YQ/CPmEPwAAgD+QCYc/kAmHPwAAgD8caYk/HGmJPwAAgD9nEow/ZxKMPwAAgD8tAI8/LQCPPwAAgD8pLZI/KS2SPwAAgD8XlJU/F5SVPwAAgD+zL5k/sy+ZPwAAgD+3+pw/t/qcPwAAgD/g76A/4O+gPwAAgD/oCaU/6AmlPwAAgD+MQ6k/jEOpPwAAgD+Gl60/hpetPwAAgD+TALI/kwCyPwAAgD9tebY/bXm2PwAAgD/R/Lo/0fy6PwAAgD95hb8/eYW/PwAAgD8hDsQ/IQ7EPwAAgD+Ekcg/hJHIPwAAgD9eCs0/XgrNPwAAgD9qc9E/anPRPwAAgD9mx9U/ZsfVPwAAgD8KAdo/CgHaPwAAgD8QG94/EBvePwAAgD86EOI/OhDiPwAAgD8/2+U/P9vlPwAAgD/Zduk/2XbpPwAAgD/I3ew/yN3sPwAAgD/FCvA/xQrwPwAAgD+L+PI/i/jyPwAAgD/VofU/1aH1PwAAgD9hAfg/YQH4PwAAgD/pEfo/6RH6PwAAgD8mzvs/Js77PwAAgD/YMP0/2DD9PwAAgD+4NP4/uDT+PwAAgD+D1P4/g9T+PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD/xCv8/8Qr/PwAAgD+Wsfo/8Qr/PwAAgD/YxO4/8Qr/PwAAgD+y89w/8Qr/PwAAgD8c7cY/8Qr/PwAAgD8OYK4/8Qr/PwAAgD+F+5Q/8Qr/PwAAgD/v3Hg/8Qr/PwAAgD/Az0w/8Qr/PwAAgD9wLSk/8Qr/PwAAgD/2UxE/8Qr/PwAAgD9CoQg/8Qr/PwAAgD9CoQg/8Qr/PwAAgD9CoQg/8Qr/PweaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8A2zSO+UHMIvQeaF8A4zWO+FTNePAeaF8ACTbi+kvnuPQeaF8Bns/6+XhZhPgeaF8C0WQ+/MIuIPgeaF8C0WQ+/kkl6PgeaF8C0WQ+/+K86PgeaF8C0WQ+/jPmyPQeaF8C0WQ+/UHMIvQeaF8C0WQ+/brYtvgeaF8C0WQ+/aA6ZvgeaF8CzWQ+/znTKvgeaF8C0WQ+/N9vdvgeaF8Bms/6+Ntu9vgeaF8AATbi+oeluvgeaF8A2zWO+qznEvQeaF8A2zSO+UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvQeaF8C0WQ+/UHMIvSVLF8C0WQ+/gVX5vDFiFsC0WQ+/Xbu2vLDkFMC0WQ+/HHwevCvYEsC0WQ+/MPLbOypCEMC0WQ+/mM7VPDMoDcC0WQ+/HolDPc+PCcC0WQ+/ZiiRPYZ+BcC0WQ+/tBXCPd35AMC0WQ+/AAPzPb0O+L+0WQ+/bDMRPiBZ7b+0WQ+/4lsnPvTd4b+0WQ+/JDY7Pkmo1b+0WQ+/dv1LPizDyL+0WQ+/Hu1YPq85u7+0WQ+/ZEBhPt8Wrb+0WQ+/iDJkPpRsnr+0WQ+/iDJkPs9nj7+0WQ+/iDJkPlk8gL+0WQ+/iDJkPvI7Yr+0WQ+/iDJkPvCARL+0WQ+/iDJkPkCvJ7+0WQ+/iDJkPnYuDL+0WQ+/iDJkPjjM5L60WQ+/iDJkPpB7tb60WQ+/iDJkPig6i760WQ+/iDJkPvCtTb60WQ+/iDJkPpBCEr60WQ+/iDJkPuCgy720WQ+/iDJkPmDrk720WQ+/iDJkPqChgL20WQ+/iDJkPqChgL20WQ+/iDJkPqChgL20WQ+/iDJkPoVJ5bKFSeUy9AQ1P/QENT+FSeWyhUnlMvQENT/0BDU/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAP5IMgT+SDIE/AACAP5QDhD+UA4Q/AACAP/SeiD/0nog/AACAP6OYjj+jmI4/AACAP5KqlT+SqpU/AACAP7GOnT+xjp0/AACAP/D+pT/w/qU/AACAPz61rj8+ta4/AACAP4xrtz+Ma7c/AACAP8zbvz/M278/AACAP+q/xz/qv8c/AACAP9rRzj/a0c4/AACAP4rL1D+Ky9Q/AACAP+pm2T/qZtk/AACAP+ld3D/pXdw/AACAP3xq3T98at0/AACAP8BZ2z98at0/AACAP+iJ1T98at0/AACAP4SOzD98at0/AACAPyH7wD98at0/AACAP05jsz98at0/AACAP5hapD98at0/AACAP4x0lD98at0/AACAP7pEhD98at0/AACAP1+9aD98at0/AACAP/KrSj98at0/AACAP018Lz98at0/AACAP4ZVGD98at0/AACAP7xeBj98at0/AACAPyB+9T58at0/AACAPy077T58at0/AACAPy077T58at0/AACAPy077T58at0/B5oXwLRZD7++SjI/B5oXwLRZD7++SjI/B5oXwLRZD7++SjI/B5oXwLRZD7++SjI/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AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/ViaAP1YmgD8AAIA/V5eAP1eXgD8AAIA/AlCBPwJQgT8AAIA/VU2CP1VNgj8AAIA/T4yDP0+Mgz8AAIA/7wmFP+8JhT8AAIA/NMOGPzTDhj8AAIA/G7WIPxu1iD8AAIA/pdyKP6Xcij8AAIA/zzaNP882jT8AAIA/mcCPP5nAjz8AAIA/AHeSPwB3kj8AAIA/A1eVPwNXlT8AAIA/o12YP6NdmD8AAIA/3IebP9yHmz8AAIA/rtKeP67Snj8AAIA/FjuiPxY7oj8AAIA/Fr6lPxa+pT8AAIA/qlipP6pYqT8AAIA/0QetP9EHrT8AAIA/i8iwP4vIsD8AAIA/1Ze0P9WXtD8AAIA/rnK4P65yuD8AAIA/F1a8PxdWvD8AAIA/Cz/APws/wD8AAIA/iyrEP4sqxD8AAIA/lhXIP5YVyD8AAIA/Kv3LPyr9yz8AAIA/Rd7PP0Xezz8AAIA/57XTP+e10z8AAIA/DYHXPw2B1z8AAIA/tzzbP7c82z8AAIA/4+XeP+Pl3j8AAIA/kXniP5F54j8AAIA/vvTlP7705T8AAIA/aFTpP2hU6T8AAIA/lJXsP5SV7D8AAIA/OLXvPzi17z8AAIA/VrDyP1aw8j8AAIA/7oP1P+6D9T8AAIA//yz4P/8s+D8AAIA/hqj6P4ao+j8AAIA/gvP8P4Lz/D8AAIA/8Qr/P/EK/z8AAIA/+nUAQPp1AEAAAIA/E0oBQBNKAUAAAIA/pAACQKQAAkAAAIA/jJgCQIyYAkAAAIA/qBADQKgQA0AAAIA/2GcDQNhnA0AAAIA//ZwDQP2cA0AAAIA/9a4DQPWuA0AAAIA/9a4DQPWuA0AAAIA/9a4DQPWuA0AAAIA/9a4DQPWuA0AAAIA/9a4DQPWuA0AAAIA/9a4DQPWuA0AAAIA/9a4DQPWuA0AAAIA/jGAAQPWuA0AAAIA/WwjvP/WuA0AAAIA/O2DVP/WuA0AAAIA/SPW2P/WuA0AAAIA/DvSWP/WuA0AAAIA/NhJxP/WuA0AAAIA/+sE9P/WuA0AAAIA/eFAaP/WuA0AAAIA/2RYNP/WuA0AAAIA/2RYNP/WuA0AAAIA/2RYNP/WuA0DqXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDANc0jvnuCzD/qXBDAN81jvnuC1D/qXBDAAU24vhQc5j/qXBDAZ7P+vq619z/qXBDAtFkPv661/z/qXBDAs1kPv0dP+T/qXBDAtFkPv0dP6T/qXBDAtFkPv3uC1D/qXBDAtFkPv661vz/qXBDAtFkPv+Horj/qXBDAtFkPv+Looj/qXBDAtFkPv661mz/qXBDAtFkPv0hPmT/qXBDAZrP+vkhPoT/qXBDAAU24vuHosj/qXBDAOs1jvnuCxD/qXBDANc0jvnuCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/qXBDAtFkPv3uCzD/4/Q/AtFkPv0Xjyz/x5w7AtFkPv1Mhyj8PJQ3AtFkPvy1mxz+MvwrAtFkPv13bwz+gwQfAtFkPv2uqvz+ENQTAtFkPv978uj9xJQDAtFkPv0H8tT9CN/e/tFkPvxvSsD+ZRO2/tFkPv/anqz9ah+K/tFkPv1inpj/2E9e/tFkPv8z5oT/g/sq/tFkPv9rInT+MXL6/tFkPvwk+mj9qQbG/s1kPv+SClz/swaO/s1kPv/LAlT+G8pW/s1kPv7whlT/d6oe/s1kPv7whlT/CnnO/s1kPv7whlT94j1e/s1kPv7whlT8m9zu/s1kPv7whlT8WJSG/s1kPv7whlT+YaAe/s1kPv7whlT/kId6+s1kPv7whlT/o2rC+s1kPv7whlT/cmoe+s1kPv7whlT+gAEa+s1kPv7whlT+wUwe+s1kPv7whlT9g2Ki9s1kPv7whlT8AHTq9s1kPv7whlT+AELe8s1kPv7whlT8Anmu8s1kPv7whlT8Anmu8s1kPv7whlT8Anmu8s1kPv7whlT+FSeWyhUnlMvQENT/0BDU/hUnlsoVJ5TL0BDU/9AQ1PwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD+SDIE/kgyBPwAAgD+UA4Q/lAOEPwAAgD/0nog/9J6IPwAAgD+jmI4/o5iOPwAAgD+SqpU/kqqVPwAAgD+xjp0/sY6dPwAAgD/w/qU/8P6lPwAAgD8+ta4/PrWuPwAAgD+Ma7c/jGu3PwAAgD/M278/zNu/PwAAgD/qv8c/6r/HPwAAgD/a0c4/2tHOPwAAgD+Ky9Q/isvUPwAAgD/qZtk/6mbZPwAAgD/pXdw/6V3cPwAAgD98at0/fGrdPwAAgD/AWds/fGrdPwAAgD/oidU/fGrdPwAAgD+Ejsw/fGrdPwAAgD8h+8A/fGrdPwAAgD9OY7M/fGrdPwAAgD+YWqQ/fGrdPwAAgD+MdJQ/fGrdPwAAgD+6RIQ/fGrdPwAAgD9fvWg/fGrdPwAAgD/yq0o/fGrdPwAAgD9NfC8/fGrdPwAAgD+GVRg/fGrdPwAAgD+8XgY/fGrdPwAAgD8gfvU+fGrdPwAAgD8tO+0+fGrdPwAAgD8tO+0+fGrdPwAAgD8tO+0+fGrdPw=="}]} diff --git a/games/devtest/mods/gltf/models/gltf_triangle_with_vertex_stride.gltf b/games/devtest/mods/gltf/models/gltf_triangle_with_vertex_stride.gltf new file mode 100644 index 000000000..feddfbb02 --- /dev/null +++ b/games/devtest/mods/gltf/models/gltf_triangle_with_vertex_stride.gltf @@ -0,0 +1 @@ +{"scene":0,"scenes":[{"nodes":[0]}],"nodes":[{"mesh":0}],"meshes":[{"primitives":[{"attributes":{"POSITION":1},"indices":0}]}],"buffers":[{"uri":"data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAA=","byteLength":80}],"bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":6,"target":34963},{"buffer":0,"byteOffset":8,"byteLength":72,"byteStride":24,"target":34962}],"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5123,"count":3,"type":"SCALAR","max":[2],"min":[0]},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":3,"type":"VEC3","max":[1,1,0],"min":[0,0,0]}],"asset":{"version":"2.0"}} diff --git a/games/devtest/mods/gltf/models/gltf_triangle_without_indices.gltf b/games/devtest/mods/gltf/models/gltf_triangle_without_indices.gltf new file mode 100644 index 000000000..e91cc0e5a --- /dev/null +++ b/games/devtest/mods/gltf/models/gltf_triangle_without_indices.gltf @@ -0,0 +1 @@ +{"scene":0,"scenes":[{"nodes":[0]}],"nodes":[{"mesh":0}],"meshes":[{"primitives":[{"attributes":{"POSITION":0}}]}],"buffers":[{"uri":"data:application/octet-stream;base64,AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAA","byteLength":36}],"bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":36,"target":34962}],"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":3,"type":"VEC3","max":[1,1,0],"min":[0,0,0]}],"asset":{"version":"2.0"}} diff --git a/games/devtest/mods/gltf/textures/gltf_cube.png b/games/devtest/mods/gltf/textures/gltf_cube.png new file mode 100644 index 000000000..1d0191085 Binary files /dev/null and b/games/devtest/mods/gltf/textures/gltf_cube.png differ diff --git a/games/devtest/mods/gltf/textures/gltf_frog.png b/games/devtest/mods/gltf/textures/gltf_frog.png new file mode 100644 index 000000000..552ae3649 Binary files /dev/null and b/games/devtest/mods/gltf/textures/gltf_frog.png differ diff --git a/games/devtest/mods/gltf/textures/gltf_snow_man.png b/games/devtest/mods/gltf/textures/gltf_snow_man.png new file mode 100644 index 000000000..7f2784358 Binary files /dev/null and b/games/devtest/mods/gltf/textures/gltf_snow_man.png differ diff --git a/games/devtest/mods/gltf/textures/gltf_spider.png b/games/devtest/mods/gltf/textures/gltf_spider.png new file mode 100644 index 000000000..1e3d3ae8c Binary files /dev/null and b/games/devtest/mods/gltf/textures/gltf_spider.png differ diff --git a/games/devtest/mods/lighting/init.lua b/games/devtest/mods/lighting/init.lua index 7b4392fb8..20448d925 100644 --- a/games/devtest/mods/lighting/init.lua +++ b/games/devtest/mods/lighting/init.lua @@ -14,7 +14,21 @@ local lighting_sections = { {n = "speed_bright_dark", d = "Dark scene adaptation speed", min = -10, max = 10, type="log2"}, {n = "center_weight_power", d = "Power factor for center-weighting", min = 0.1, max = 10}, } - } + }, + { + n = "bloom", d = "Bloom", + entries = { + {n = "intensity", d = "Intensity", min = 0, max = 1}, + {n = "strength_factor", d = "Strength Factor", min = 0.1, max = 10}, + {n = "radius", d = "Radius", min = 0.1, max = 8}, + }, + }, + { + n = "volumetric_light", d = "Volumetric Lighting", + entries = { + {n = "strength", d = "Strength", min = 0, max = 1}, + }, + }, } local function dump_lighting(lighting) @@ -59,38 +73,40 @@ minetest.register_chatcommand("set_lighting", { local lighting = player:get_lighting() local exposure = lighting.exposure or {} - local form = { - "formspec_version[2]", - "size[15,30]", - "position[0.99,0.15]", - "anchor[1,0]", - "padding[0.05,0.1]", - "no_prepend[]" - }; - + local content = {} local line = 1 for _,section in ipairs(lighting_sections) do local parameters = section.entries or {} local state = lighting[section.n] or {} - table.insert(form, "label[1,"..line..";"..section.d.."]") + table.insert(content, "label[1,"..line..";"..section.d.."]") line = line + 1 for _,v in ipairs(parameters) do - table.insert(form, "label[2,"..line..";"..v.d.."]") - table.insert(form, "scrollbaroptions[min=0;max=1000;smallstep=10;largestep=100;thumbsize=10]") + table.insert(content, "label[2,"..line..";"..v.d.."]") + table.insert(content, "scrollbaroptions[min=0;max=1000;smallstep=10;largestep=100;thumbsize=10]") local value = state[v.n] if v.type == "log2" then value = math.log(value or 1) / math.log(2) end local sb_scale = math.floor(1000 * (math.max(v.min, value or 0) - v.min) / (v.max - v.min)) - table.insert(form, "scrollbar[2,"..(line+0.7)..";12,1;horizontal;"..section.n.."."..v.n..";"..sb_scale.."]") + table.insert(content, "scrollbar[2,"..(line+0.7)..";12,1;horizontal;"..section.n.."."..v.n..";"..sb_scale.."]") line = line + 2.7 end line = line + 1 end + local form = { + "formspec_version[2]", + "size[15,", line, "]", + "position[0.99,0.15]", + "anchor[1,0]", + "padding[0.05,0.1]", + "no_prepend[]", + } + table.insert_all(form, content) + minetest.show_formspec(player_name, "lighting", table.concat(form)) local debug_value = dump_lighting(lighting) local debug_ui = player:hud_add({type="text", position={x=0.1, y=0.3}, scale={x=1,y=1}, alignment = {x=1, y=1}, text=debug_value, number=0xFFFFFF}) diff --git a/games/devtest/mods/testabms/README.md b/games/devtest/mods/testabms/README.md new file mode 100644 index 000000000..60fa6d656 --- /dev/null +++ b/games/devtest/mods/testabms/README.md @@ -0,0 +1,6 @@ +# Test ABMs + +This mod contains a nodes and related ABM actions. +By placing these nodes, you can test basic ABM behaviours. + +There are separate tests for ABM `chance`, `interval`, `min_y`, `max_y`, `neighbor` and `without_neighbor` fields. diff --git a/games/devtest/mods/testabms/after_node.lua b/games/devtest/mods/testabms/after_node.lua new file mode 100644 index 000000000..64cdfb484 --- /dev/null +++ b/games/devtest/mods/testabms/after_node.lua @@ -0,0 +1,12 @@ + +local S = minetest.get_translator("testnodes") + +-- After ABM node +minetest.register_node("testabms:after_abm", { + description = S("After ABM processed node."), + drawtype = "normal", + tiles = { "testabms_after_node.png" }, + + groups = { dig_immediate = 3 }, +}) + diff --git a/games/devtest/mods/testabms/chances.lua b/games/devtest/mods/testabms/chances.lua new file mode 100644 index 000000000..95f416b45 --- /dev/null +++ b/games/devtest/mods/testabms/chances.lua @@ -0,0 +1,56 @@ +-- test ABMs with different chances + +local S = minetest.get_translator("testnodes") + +-- ABM chance 5 node +minetest.register_node("testabms:chance_5", { + description = S("Node for test ABM chance_5"), + drawtype = "normal", + tiles = { "testabms_wait_node.png" }, + + groups = { dig_immediate = 3 }, + + on_construct = function (pos) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "Waiting for ABM testabms:chance_5") + end, +}) + +minetest.register_abm({ + label = "testabms:chance_5", + nodenames = "testabms:chance_5", + interval = 10, + chance = 5, + action = function (pos) + minetest.swap_node(pos, {name="testabms:after_abm"}) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "ABM testabsm:chance_5 changed this node.") + end +}) + +-- ABM chance 20 node +minetest.register_node("testabms:chance_20", { + description = S("Node for test ABM chance_20"), + drawtype = "normal", + tiles = { "testabms_wait_node.png" }, + + groups = { dig_immediate = 3 }, + + on_construct = function (pos) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "Waiting for ABM testabms:chance_20") + end, +}) + +minetest.register_abm({ + label = "testabms:chance_20", + nodenames = "testabms:chance_20", + interval = 10, + chance = 20, + action = function (pos) + minetest.swap_node(pos, {name="testabms:after_abm"}) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "ABM testabsm:chance_20 changed this node.") + end +}) + diff --git a/games/devtest/mods/testabms/init.lua b/games/devtest/mods/testabms/init.lua new file mode 100644 index 000000000..7830d8436 --- /dev/null +++ b/games/devtest/mods/testabms/init.lua @@ -0,0 +1,7 @@ +local path = minetest.get_modpath(minetest.get_current_modname()) + +dofile(path.."/after_node.lua") +dofile(path.."/chances.lua") +dofile(path.."/intervals.lua") +dofile(path.."/min_max.lua") +dofile(path.."/neighbors.lua") diff --git a/games/devtest/mods/testabms/intervals.lua b/games/devtest/mods/testabms/intervals.lua new file mode 100644 index 000000000..57b58faa5 --- /dev/null +++ b/games/devtest/mods/testabms/intervals.lua @@ -0,0 +1,56 @@ +-- test ABMs with different interval + +local S = minetest.get_translator("testnodes") + +-- ABM inteval 1 node +minetest.register_node("testabms:interval_1", { + description = S("Node for test ABM interval_1"), + drawtype = "normal", + tiles = { "testabms_wait_node.png" }, + + groups = { dig_immediate = 3 }, + + on_construct = function (pos) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "Waiting for ABM testabms:interval_1") + end, +}) + +minetest.register_abm({ + label = "testabms:interval_1", + nodenames = "testabms:interval_1", + interval = 1, + chance = 1, + action = function (pos) + minetest.swap_node(pos, {name="testabms:after_abm"}) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "ABM testabsm:interval_1 changed this node.") + end +}) + +-- ABM interval 60 node +minetest.register_node("testabms:interval_60", { + description = S("Node for test ABM interval_60"), + drawtype = "normal", + tiles = { "testabms_wait_node.png" }, + + groups = { dig_immediate = 3 }, + + on_construct = function (pos) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "Waiting for ABM testabms:interval_60") + end, +}) + +minetest.register_abm({ + label = "testabms:interval_60", + nodenames = "testabms:interval_60", + interval = 60, + chance = 1, + action = function (pos) + minetest.swap_node(pos, {name="testabms:after_abm"}) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "ABM testabsm:interval_60 changed this node.") + end +}) + diff --git a/games/devtest/mods/testabms/min_max.lua b/games/devtest/mods/testabms/min_max.lua new file mode 100644 index 000000000..62f1ccd53 --- /dev/null +++ b/games/devtest/mods/testabms/min_max.lua @@ -0,0 +1,58 @@ +-- test ABMs with min_y and max_y + +local S = minetest.get_translator("testnodes") + +-- ABM min_y node +minetest.register_node("testabms:min_y", { + description = S("Node for test ABM min_y."), + drawtype = "normal", + tiles = { "testabms_wait_node.png" }, + + groups = { dig_immediate = 3 }, + + on_construct = function (pos) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "Waiting for ABM testabms:min_y at y "..pos.y.." with min_y = 0") + end, +}) + +minetest.register_abm({ + label = "testabms:min_y", + nodenames = "testabms:min_y", + interval = 10, + chance = 1, + min_y = 0, + action = function (pos) + minetest.swap_node(pos, {name="testabms:after_abm"}) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "ABM testabsm:min_y changed this node.") + end +}) + +-- ABM max_y node +minetest.register_node("testabms:max_y", { + description = S("Node for test ABM max_y."), + drawtype = "normal", + tiles = { "testabms_wait_node.png" }, + + groups = { dig_immediate = 3 }, + + on_construct = function (pos) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "Waiting for ABM testabms:max_y at y "..pos.y.." with max_y = 0") + end, +}) + +minetest.register_abm({ + label = "testabms:max_y", + nodenames = "testabms:max_y", + interval = 10, + chance = 1, + max_y = 0, + action = function (pos) + minetest.swap_node(pos, {name="testabms:after_abm"}) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "ABM testabsm:max_y changed this node.") + end +}) + diff --git a/games/devtest/mods/testabms/mod.conf b/games/devtest/mods/testabms/mod.conf new file mode 100644 index 000000000..ad74cd2ed --- /dev/null +++ b/games/devtest/mods/testabms/mod.conf @@ -0,0 +1,2 @@ +name = testabms +description = Contains some nodes for test ABMs. diff --git a/games/devtest/mods/testabms/neighbors.lua b/games/devtest/mods/testabms/neighbors.lua new file mode 100644 index 000000000..42ce47dff --- /dev/null +++ b/games/devtest/mods/testabms/neighbors.lua @@ -0,0 +1,99 @@ +-- test ABMs with neighbor and without_neighbor + +local S = minetest.get_translator("testnodes") + +-- ABM required neighbor +minetest.register_node("testabms:required_neighbor", { + description = S("Node for test ABM required_neighbor.") .. "\n" + .. S("Sensitive neighbor node is testnodes:normal."), + drawtype = "normal", + tiles = { "testabms_wait_node.png" }, + + groups = { dig_immediate = 3 }, + + on_construct = function (pos) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", + "Waiting for ABM testabms:required_neighbor " + .. "(normal drawtype testnode sensitive)") + end, +}) + +minetest.register_abm({ + label = "testabms:required_neighbor", + nodenames = "testabms:required_neighbor", + neighbors = {"testnodes:normal"}, + interval = 1, + chance = 1, + action = function (pos) + minetest.swap_node(pos, {name="testabms:after_abm"}) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", + "ABM testabsm:required_neighbor changed this node.") + end +}) + +-- ABM missing neighbor node +minetest.register_node("testabms:missing_neighbor", { + description = S("Node for test ABM missing_neighbor.") .. "\n" + .. S("Sensitive neighbor node is testnodes:normal."), + drawtype = "normal", + tiles = { "testabms_wait_node.png" }, + + groups = { dig_immediate = 3 }, + + on_construct = function (pos) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", + "Waiting for ABM testabms:missing_neighbor" + .. " (normal drawtype testnode sensitive)") + end, +}) + +minetest.register_abm({ + label = "testabms:missing_neighbor", + nodenames = "testabms:missing_neighbor", + without_neighbors = {"testnodes:normal"}, + interval = 1, + chance = 1, + action = function (pos) + minetest.swap_node(pos, {name="testabms:after_abm"}) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", + "ABM testabsm:missing_neighbor changed this node.") + end +}) + +-- ABM required and missing neighbor node +minetest.register_node("testabms:required_missing_neighbor", { + description = S("Node for test ABM required_missing_neighbor.") .. "\n" + .. S("Sensitive neighbor nodes are testnodes:normal and testnodes:glasslike."), + drawtype = "normal", + tiles = { "testabms_wait_node.png" }, + + groups = { dig_immediate = 3 }, + + on_construct = function (pos) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", + "Waiting for ABM testabms:required_missing_neighbor" + .. " (wint normal drawtype testnode and no glasslike" + .. " drawtype testnode sensitive)") + end, +}) + +minetest.register_abm({ + label = "testabms:required_missing_neighbor", + nodenames = "testabms:required_missing_neighbor", + neighbors = {"testnodes:normal"}, + without_neighbors = {"testnodes:glasslike"}, + interval = 1, + chance = 1, + action = function (pos) + minetest.swap_node(pos, {name="testabms:after_abm"}) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", + "ABM testabsm:required_missing_neighbor changed this node.") + end +}) + diff --git a/games/devtest/mods/testabms/textures/testabms_after_node.png b/games/devtest/mods/testabms/textures/testabms_after_node.png new file mode 100644 index 000000000..dab87594b Binary files /dev/null and b/games/devtest/mods/testabms/textures/testabms_after_node.png differ diff --git a/games/devtest/mods/testabms/textures/testabms_wait_node.png b/games/devtest/mods/testabms/textures/testabms_wait_node.png new file mode 100644 index 000000000..a9bd9a36f Binary files /dev/null and b/games/devtest/mods/testabms/textures/testabms_wait_node.png differ diff --git a/games/devtest/mods/testformspec/formspec.lua b/games/devtest/mods/testformspec/formspec.lua index 99ee691f1..f8f17798b 100644 --- a/games/devtest/mods/testformspec/formspec.lua +++ b/games/devtest/mods/testformspec/formspec.lua @@ -66,7 +66,7 @@ local inv_style_fs = [[ -- Some textures from textures/base/pack and Devtest, with many different sizes -- and aspect ratios. -local image_column = "image,0=logo.png,1=rare_controls.png,2=checkbox_16.png," .. +local image_column = "image,0=logo.png,1=crack_anylength.png^[invert:rgb,2=checkbox_16.png," .. "3=checkbox_32.png,4=checkbox_64.png,5=default_lava.png," .. "6=progress_bar.png,7=progress_bar_bg.png" local words = { @@ -299,7 +299,18 @@ local scroll_fs = "scrollbaroptions[max=170]".. -- lowest seen pos is: 0.1*170+6=23 (factor*max+height) "scrollbar[7.5,0;0.3,4;vertical;scrbar;0]".. "scrollbar[8,0;0.3,4;vertical;scrbarhmmm;0]".. - "dropdown[0,6;2;hmdrpdwnnn;Outside,of,container;1]" + "dropdown[0,6;2;hmdrpdwnnn;Outside,of,container;1]".. + "scroll_container[0,8;10,4;scrbar420;vertical;0.1;2]".. + "button[0.5,0.5;10,1;;Container with padding=2]".. + "list[current_player;main;0,5;8,4;]".. + "scroll_container_end[]".. + "scrollbar[10.1,8;0.5,4;vertical;scrbar420;0]".. + -- Buttons for scale comparison + "button[11,8;1,1;;0]".. + "button[11,9;1,1;;1]".. + "button[11,10;1,1;;2]".. + "button[11,11;1,1;;3]".. + "button[11,12;1,1;;4]" --style_type[label;textcolor=green] --label[0,0;Green] @@ -462,7 +473,7 @@ mouse control = true] ]], -- Scroll containers - "formspec_version[3]size[12,13]" .. + "formspec_version[7]size[12,13]" .. scroll_fs, -- Sound diff --git a/games/devtest/mods/testfullscreenfs/init.lua b/games/devtest/mods/testfullscreenfs/init.lua index e1af3ae33..7abc7f817 100644 --- a/games/devtest/mods/testfullscreenfs/init.lua +++ b/games/devtest/mods/testfullscreenfs/init.lua @@ -1,18 +1,30 @@ -local function show_fullscreen_fs(name) - local window = minetest.get_player_window_information(name) - if not window then - return false, "Unable to get window info" - end +local function window_info_equal(a, b) + return + -- size + a.size.x == b.size.x and a.size.y == b.size.y and + -- real_gui_scaling, real_hud_scaling + a.real_gui_scaling == b.real_gui_scaling and + a.real_hud_scaling == b.real_hud_scaling and + -- max_formspec_size + a.max_formspec_size.x == b.max_formspec_size.x and + a.max_formspec_size.y == b.max_formspec_size.y and + -- touch_controls + a.touch_controls == b.touch_controls +end +local last_window_info = {} + +local function show_fullscreen_fs(name, window) print(dump(window)) - local size = { x = window.max_formspec_size.x * 1.1, y = window.max_formspec_size.y * 1.1 } + local size = window.max_formspec_size local touch_text = window.touch_controls and "Touch controls enabled" or "Touch controls disabled" local fs = { "formspec_version[4]", ("size[%f,%f]"):format(size.x, size.y), - "padding[-0.01,-0.01]", + "padding[0,0]", + "bgcolor[;true]", ("button[%f,%f;1,1;%s;%s]"):format(0, 0, "tl", "TL"), ("button[%f,%f;1,1;%s;%s]"):format(size.x - 1, 0, "tr", "TR"), ("button[%f,%f;1,1;%s;%s]"):format(size.x - 1, size.y - 1, "br", "BR"), @@ -23,10 +35,37 @@ local function show_fullscreen_fs(name) } minetest.show_formspec(name, "testfullscreenfs:fs", table.concat(fs)) - return true, ("Calculated size of %f, %f"):format(size.x, size.y) + minetest.chat_send_player(name, ("Calculated size of %f, %f"):format(size.x, size.y)) + last_window_info[name] = window end - minetest.register_chatcommand("testfullscreenfs", { - func = show_fullscreen_fs, + func = function(name) + local window = minetest.get_player_window_information(name) + if not window then + return false, "Unable to get window info" + end + + show_fullscreen_fs(name, window) + return true + end, }) + +minetest.register_globalstep(function() + for name, last_window in pairs(last_window_info) do + local window = minetest.get_player_window_information(name) + if window and not window_info_equal(last_window, window) then + show_fullscreen_fs(name, window) + end + end +end) + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname == "testfullscreenfs:fs" and fields.quit then + last_window_info[player:get_player_name()] = nil + end +end) + +minetest.register_on_leaveplayer(function(player) + last_window_info[player:get_player_name()] = nil +end) diff --git a/games/devtest/mods/testhud/init.lua b/games/devtest/mods/testhud/init.lua index 9afed8fc7..4afece209 100644 --- a/games/devtest/mods/testhud/init.lua +++ b/games/devtest/mods/testhud/init.lua @@ -208,9 +208,145 @@ minetest.register_chatcommand("zoomfov", { end, }) +-- Hotbars + +local hud_hotbar_defs = { + { + type = "hotbar", + position = {x=0.2, y=0.5}, + direction = 0, + alignment = {x=1, y=-1}, + }, + { + type = "hotbar", + position = {x=0.2, y=0.5}, + direction = 2, + alignment = {x=1, y=1}, + }, + { + type = "hotbar", + position = {x=0.7, y=0.5}, + direction = 0, + offset = {x=140, y=20}, + alignment = {x=-1, y=-1}, + }, + { + type = "hotbar", + position = {x=0.7, y=0.5}, + direction = 2, + offset = {x=140, y=20}, + alignment = {x=-1, y=1}, + }, +} + + +local player_hud_hotbars= {} +minetest.register_chatcommand("hudhotbars", { + description = "Shows some test Lua HUD elements of type hotbar. " .. + "(add: Adds elements (default). remove: Removes elements)", + params = "[ add | remove ]", + func = function(name, params) + local player = minetest.get_player_by_name(name) + if not player then + return false, "No player." + end + + local id_table = player_hud_hotbars[name] + if not id_table then + id_table = {} + player_hud_hotbars[name] = id_table + end + + if params == "remove" then + for _, id in ipairs(id_table) do + player:hud_remove(id) + end + return true, "Hotbars removed." + end + + -- params == "add" or default + for _, def in ipairs(hud_hotbar_defs) do + table.insert(id_table, player:hud_add(def)) + end + return true, #hud_hotbar_defs .." Hotbars added." + end +}) + +-- Inventories + +local hud_inventory_defs = { + { + type = "inventory", + position = {x=0.2, y=0.5}, + direction = 0, + text = "main", + number = 4, + item = 2, + }, + { + type = "inventory", + position = {x=0.2, y=0.5}, + direction = 2, + text = "main", + number = 4, + item = 2, + }, + { + type = "inventory", + position = {x=0.7, y=0.5}, + direction = 1, + text = "main", + number = 4, + item = 2, + }, + { + type = "inventory", + position = {x=0.7, y=0.5}, + direction = 3, + text = "main", + number = 4, + item = 2, + }, +} + +local player_hud_inventories= {} +minetest.register_chatcommand("hudinventories", { + description = "Shows some test Lua HUD elements of type inventory. (add: Adds elements (default). remove: Removes elements)", + params = "[ add | remove ]", + func = function(name, params) + local player = minetest.get_player_by_name(name) + if not player then + return false, "No player." + end + + local id_table = player_hud_inventories[name] + if not id_table then + id_table = {} + player_hud_inventories[name] = id_table + end + + if params == "remove" then + for _, id in ipairs(id_table) do + player:hud_remove(id) + end + return true, "HUD Inventories removed." + end + + -- params == "add" or default + for _, def in ipairs(hud_inventory_defs) do + table.insert(id_table, player:hud_add(def)) + end + return true, #hud_inventory_defs .." HUD Inventories added." + end +}) + + minetest.register_on_leaveplayer(function(player) - player_font_huds[player:get_player_name()] = nil - player_waypoints[player:get_player_name()] = nil + local playername = player:get_player_name() + player_font_huds[playername] = nil + player_waypoints[playername] = nil + player_hud_hotbars[playername] = nil + player_hud_inventories[playername] = nil end) minetest.register_chatcommand("hudprint", { @@ -232,3 +368,26 @@ minetest.register_chatcommand("hudprint", { return true, s end }) + +local hud_flags = {"hotbar", "healthbar", "crosshair", "wielditem", "breathbar", + "minimap", "minimap_radar", "basic_debug", "chat"} + +minetest.register_chatcommand("hudtoggleflag", { + description = "Toggles a hud flag.", + params = "[ ".. table.concat(hud_flags, " | ") .." ]", + func = function(name, params) + local player = minetest.get_player_by_name(name) + if not player then + return false, "No player." + end + + local flags = player:hud_get_flags() + if flags[params] == nil then + return false, "Unknown hud flag." + end + + flags[params] = not flags[params] + player:hud_set_flags(flags) + return true, "Flag \"".. params .."\" set to ".. tostring(flags[params]) .. "." + end +}) diff --git a/games/devtest/mods/testnodes/drawtypes.lua b/games/devtest/mods/testnodes/drawtypes.lua index 087d09eff..4a657b739 100644 --- a/games/devtest/mods/testnodes/drawtypes.lua +++ b/games/devtest/mods/testnodes/drawtypes.lua @@ -98,6 +98,23 @@ minetest.register_node("testnodes:allfaces", { groups = { dig_immediate = 3 }, }) +minetest.register_node("testnodes:allfaces_6", { + description = S("\"allfaces 6 Textures\" Drawtype Test Node").."\n".. + S("Transparent node with visible internal backfaces"), + drawtype = "allfaces", + paramtype = "light", + tiles = { + "testnodes_allfaces.png^[colorize:red", + "testnodes_allfaces.png^[colorize:orange", + "testnodes_allfaces.png^[colorize:yellow", + "testnodes_allfaces.png^[colorize:green", + "testnodes_allfaces.png^[colorize:blue", + "testnodes_allfaces.png^[colorize:purple" + }, + + groups = { dig_immediate = 3 }, +}) + local allfaces_optional_tooltip = "".. S("Rendering depends on 'leaves_style' setting:").."\n".. S("* 'fancy': transparent with visible internal backfaces").."\n".. diff --git a/games/devtest/mods/testnodes/textures.lua b/games/devtest/mods/testnodes/textures.lua index 96f291d6a..b95fbd62e 100644 --- a/games/devtest/mods/testnodes/textures.lua +++ b/games/devtest/mods/testnodes/textures.lua @@ -52,6 +52,12 @@ minetest.register_node("testnodes:fill_positioning_reference", { groups = {dig_immediate = 3}, }) +minetest.register_node("testnodes:modifier_mask", { + description = S("[mask Modifier Test Node"), + tiles = {"testnodes_128x128_rgb.png^[mask:testnodes_mask_WRGBKW.png"}, + groups = {dig_immediate = 3}, +}) + -- Node texture transparency test local alphas = { 64, 128, 191 } diff --git a/games/devtest/mods/testnodes/textures/testnodes_128x128_rgb.png b/games/devtest/mods/testnodes/textures/testnodes_128x128_rgb.png new file mode 100644 index 000000000..060d8e67a Binary files /dev/null and b/games/devtest/mods/testnodes/textures/testnodes_128x128_rgb.png differ diff --git a/games/devtest/mods/testnodes/textures/testnodes_mask_WRGBKW.png b/games/devtest/mods/testnodes/textures/testnodes_mask_WRGBKW.png new file mode 100644 index 000000000..03ab71e3f Binary files /dev/null and b/games/devtest/mods/testnodes/textures/testnodes_mask_WRGBKW.png differ diff --git a/games/devtest/mods/testtranslations/init.lua b/games/devtest/mods/testtranslations/init.lua new file mode 100644 index 000000000..bb3696e7e --- /dev/null +++ b/games/devtest/mods/testtranslations/init.lua @@ -0,0 +1,26 @@ +local S, NS = minetest.get_translator("testtranslations") + +local function send_compare(name, text) + core.chat_send_player(name, ("%s | %s | %s"):format( + core.get_translated_string("", text), text, core.get_translated_string("fr", text))) +end + +minetest.register_chatcommand("testtranslations", { + params = "", + description = "Test translations", + privs = {}, + func = function(name, param) + core.chat_send_player(name, "Please ensure your locale is set to \"fr\"") + core.chat_send_player(name, "Untranslated | Client-side translation | Server-side translation (fr)") + send_compare(name, S("Testing .tr files: untranslated")) + send_compare(name, S("Testing .po files: untranslated")) + send_compare(name, S("Testing .mo files: untranslated")) + send_compare(name, S("Testing fuzzy .po entry: untranslated (expected)")) + send_compare(name, core.translate("translation_po", "Testing .po without context: untranslated")) + send_compare(name, core.translate("translation_mo", "Testing .mo without context: untranslated")) + for i = 0,4 do + send_compare(name, NS("@1: .po singular", "@1: .po plural", i, tostring(i))) + send_compare(name, NS("@1: .mo singular", "@1: .mo plural", i, tostring(i))) + end + end +}) diff --git a/games/devtest/mods/testtranslations/locale/testtranslations.fr.po b/games/devtest/mods/testtranslations/locale/testtranslations.fr.po new file mode 100644 index 000000000..2bcc6c7d4 --- /dev/null +++ b/games/devtest/mods/testtranslations/locale/testtranslations.fr.po @@ -0,0 +1,9 @@ +# Dummy entry. This is a test to make sure that a parser error is not thrown +# if the following line is msgctxt. +msgctxt "testtranslations" +msgid "Dummy entry" +msgstr "Dummy result" + +# Used for translating the mod title +msgid "Test translations" +msgstr "Test translations (French)" diff --git a/games/devtest/mods/testtranslations/locale/translation_mo.fr.mo b/games/devtest/mods/testtranslations/locale/translation_mo.fr.mo new file mode 100644 index 000000000..0e7190de9 Binary files /dev/null and b/games/devtest/mods/testtranslations/locale/translation_mo.fr.mo differ diff --git a/games/devtest/mods/testtranslations/locale/translation_po.fr.po b/games/devtest/mods/testtranslations/locale/translation_po.fr.po new file mode 100644 index 000000000..5aefc0f41 --- /dev/null +++ b/games/devtest/mods/testtranslations/locale/translation_po.fr.po @@ -0,0 +1,22 @@ +# Test Plural-Forms parsing +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural= (n-1+1)<=1 ? n||1?0:1 : 1?(!!2):2;" + +msgctxt "testtranslations" +msgid "Testing .po files: untranslated" +msgstr "Testing .po files: translated" + +msgctxt "testtranslations" +msgid "@1: .po singular" +msgid_plural "@1: .po plural" +msgstr[0] "@1: .po 0 and 1 (French singular)" +msgstr[1] "@1: .po >1 (French plural)" + +#, foo bar fuzzy +msgctxt "testtranslations" +msgid "Testing fuzzy .po entry: untranslated (expected)" +msgstr "Testing fuzzy .po entry: translated (wrong)" + +msgid "Testing .po without context: untranslated" +msgstr "Testing .po without context: translated" diff --git a/games/devtest/mods/testtranslations/locale/translation_tr.fr.tr b/games/devtest/mods/testtranslations/locale/translation_tr.fr.tr new file mode 100644 index 000000000..b9ac66af5 --- /dev/null +++ b/games/devtest/mods/testtranslations/locale/translation_tr.fr.tr @@ -0,0 +1,2 @@ +# textdomain: testtranslations +Testing .tr files: untranslated=Testing .tr files: translated diff --git a/games/devtest/mods/testtranslations/mod.conf b/games/devtest/mods/testtranslations/mod.conf new file mode 100644 index 000000000..1fc09cf6b --- /dev/null +++ b/games/devtest/mods/testtranslations/mod.conf @@ -0,0 +1,3 @@ +name = testtranslations +title = Test translations +description = Test mod to test translations. diff --git a/games/devtest/mods/testtranslations/test_locale/readme.txt b/games/devtest/mods/testtranslations/test_locale/readme.txt new file mode 100644 index 000000000..7a2ed4329 --- /dev/null +++ b/games/devtest/mods/testtranslations/test_locale/readme.txt @@ -0,0 +1,4 @@ +The translation files in this directory intentionally include errors (which +would be reported when someone starts the devtest game in the de locale). This +allows the unittest to check that the translation file reader also handles +files that include errors. diff --git a/games/devtest/mods/testtranslations/test_locale/translation_mo.de.mo b/games/devtest/mods/testtranslations/test_locale/translation_mo.de.mo new file mode 100644 index 000000000..ffe05cd71 Binary files /dev/null and b/games/devtest/mods/testtranslations/test_locale/translation_mo.de.mo differ diff --git a/games/devtest/mods/testtranslations/test_locale/translation_po.de.po b/games/devtest/mods/testtranslations/test_locale/translation_po.de.po new file mode 100644 index 000000000..9a64805a6 --- /dev/null +++ b/games/devtest/mods/testtranslations/test_locale/translation_po.de.po @@ -0,0 +1,42 @@ +# This file is used by the C++ unittest for testing the parser +msgid "" +msgstr "\n\n\n" +"Plural-Forms: nplurals=2; plural=n!=1;" +"\n\n\n" + +msgid "foo" + msgstr "bar" + +msgid "Untranslated" +msgstr "" + +#, fuzzy +msgid "Fuzzy entry" +msgstr "Wrong" + +msgid "Multi\\""line\n" +"string" +msgstr "Multi\\\"" "li\\ne\nresult" + +msgctxt "Something" in "between" +msgctxt "String does not end +msgstr "Lost string" +msgid "Wrong order" + +msgid "Singular form" +msgid_plural "Plural form" +msgstr[0] "Singular result" +msgstr[1] "Plural result" + +msgid "Not enough value" +msgid_plural "Not enough values" +msgstr[0] "Result" + +msgid "Partial translation" +msgid_plural "Partial translations" +msgstr[0] "Partially translated" +msgstr[1] "" + +msgctxt "context" +msgid "With context" +msgstr "Has context" diff --git a/games/devtest/mods/testtranslations/translation_mo.de.po b/games/devtest/mods/testtranslations/translation_mo.de.po new file mode 100644 index 000000000..c3f22c4ed --- /dev/null +++ b/games/devtest/mods/testtranslations/translation_mo.de.po @@ -0,0 +1,26 @@ +msgid "" +msgstr "Plural-Forms: nplurals=2; plural= n != 1;" + +msgctxt "context" +msgid "With context" +msgstr "Has context" + +msgctxt "context" +msgid "Singular form" +msgid_plural "Plural form" +msgstr[0] "Singular result" +msgstr[1] "Plural result" + +# Replace plural form delimiter in the msgstr +msgid "Corrupt singular" +msgid_plural "Corrupt plural" +msgstr[0] "Corrupt singular result" +msgstr[1] "Corrupt plural result" + +# Replace terminating NUL in the MO file +msgid "Corrupt entry" +msgstr "Corrupted result" + +# Change the address of this entry to something invalid +msgid "Removed entry" +msgstr "Removed result" diff --git a/games/devtest/mods/testtranslations/translation_mo.fr.po b/games/devtest/mods/testtranslations/translation_mo.fr.po new file mode 100644 index 000000000..e6cf6d6ea --- /dev/null +++ b/games/devtest/mods/testtranslations/translation_mo.fr.po @@ -0,0 +1,18 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n>1;" + +msgctxt "testtranslations" +msgid "Testing .mo files: untranslated" +msgstr "Testing .mo files: translated" + +msgid "Testing .mo without context: untranslated" +msgstr "Testing .mo without context: translated" + +msgctxt "testtranslations" +msgid "@1: .mo singular" +msgid_plural "@1: .mo plural" +msgstr[0] "@1: .mo 0 and 1 (French singular)" +msgstr[1] "@1: .mo >1 (French plural)" diff --git a/games/devtest/mods/unittests/color.lua b/games/devtest/mods/unittests/color.lua new file mode 100644 index 000000000..86154445c --- /dev/null +++ b/games/devtest/mods/unittests/color.lua @@ -0,0 +1,17 @@ +local function assert_colors_equal(c1, c2) + if type(c1) == "table" and type(c2) == "table" then + assert(c1.r == c2.r and c1.g == c2.g and c1.b == c2.b and c1.a == c2.a) + else + assert(c1 == c2) + end +end + +local function test_color_conversion() + assert_colors_equal(core.colorspec_to_table("#fff"), {r = 255, g = 255, b = 255, a = 255}) + assert_colors_equal(core.colorspec_to_table(0xFF00FF00), {r = 0, g = 255, b = 0, a = 255}) + assert_colors_equal(core.colorspec_to_table("#00000000"), {r = 0, g = 0, b = 0, a = 0}) + assert_colors_equal(core.colorspec_to_table("green"), {r = 0, g = 128, b = 0, a = 255}) + assert_colors_equal(core.colorspec_to_table("gren"), nil) +end + +unittests.register("test_color_conversion", test_color_conversion) diff --git a/games/devtest/mods/unittests/entity.lua b/games/devtest/mods/unittests/entity.lua index 8e8bd1c48..af91a2a94 100644 --- a/games/devtest/mods/unittests/entity.lua +++ b/games/devtest/mods/unittests/entity.lua @@ -214,3 +214,23 @@ unittests.register("test_objects_in_area", function(_, pos) return core.objects_in_area(pos:offset(-1, -1, -1), pos:offset(1, 1, 1)) end) end, {map=true}) + +-- Tests that bone rotation euler angles are preserved (see #14992) +local function test_get_bone_rot(_, pos) + local obj = core.add_entity(pos, "unittests:dummy") + for _ = 1, 100 do + local function assert_similar(euler_angles) + local _, rot = obj:get_bone_position("bonename") + assert(euler_angles:distance(rot) < 1e-3) + local override = obj:get_bone_override("bonename") + assert(euler_angles:distance(override.rotation.vec:apply(math.deg)) < 1e-3) + end + local deg = 1e3 * vector.new(math.random(), math.random(), math.random()) + obj:set_bone_position("bonename", vector.zero(), deg) + assert_similar(deg) + local rad = 3 * math.pi * vector.new(math.random(), math.random(), math.random()) + obj:set_bone_override("bonename", {rotation = {vec = rad}}) + assert_similar(rad:apply(math.deg)) + end +end +unittests.register("test_get_bone_rot", test_get_bone_rot, {map=true}) diff --git a/games/devtest/mods/unittests/init.lua b/games/devtest/mods/unittests/init.lua index eae003a2a..a967a986f 100644 --- a/games/devtest/mods/unittests/init.lua +++ b/games/devtest/mods/unittests/init.lua @@ -187,6 +187,7 @@ dofile(modpath .. "/raycast.lua") dofile(modpath .. "/inventory.lua") dofile(modpath .. "/load_time.lua") dofile(modpath .. "/on_shutdown.lua") +dofile(modpath .. "/color.lua") -------------- diff --git a/games/devtest/mods/unittests/inside_mapgen_env.lua b/games/devtest/mods/unittests/inside_mapgen_env.lua index a8df004de..f6f8513ce 100644 --- a/games/devtest/mods/unittests/inside_mapgen_env.lua +++ b/games/devtest/mods/unittests/inside_mapgen_env.lua @@ -22,9 +22,11 @@ local function do_tests() assert(core.registered_items["unittests:description_test"].on_place == true) end --- there's no (usable) communcation path between mapgen and the regular env --- so we just run the test unconditionally -do_tests() +-- first thread to get here runs the tests +if core.ipc_cas("unittests:mg_once", nil, true) then + -- this is checked from the main env + core.ipc_set("unittests:mg", { pcall(do_tests) }) +end core.register_on_generated(function(vm, pos1, pos2, blockseed) local n = tonumber(core.get_mapgen_setting("chunksize")) * 16 - 1 diff --git a/games/devtest/mods/unittests/misc.lua b/games/devtest/mods/unittests/misc.lua index 6ff5c7e84..a807a390f 100644 --- a/games/devtest/mods/unittests/misc.lua +++ b/games/devtest/mods/unittests/misc.lua @@ -153,6 +153,18 @@ local function test_urlencode() end unittests.register("test_urlencode", test_urlencode) +local function test_parse_json() + local raw = "{\"how\\u0000weird\":\n\"yes\\u0000really\",\"n\":-1234567891011,\"z\":null}" + local data = core.parse_json(raw) + assert(data["how\000weird"] == "yes\000really") + assert(data.n == -1234567891011) + assert(data.z == nil) + local null = {} + data = core.parse_json(raw, null) + assert(data.z == null) +end +unittests.register("test_parse_json", test_parse_json) + local function test_game_info() local info = minetest.get_game_info() local game_conf = Settings(info.path .. "/game.conf") @@ -254,3 +266,43 @@ local function test_gennotify_api() assert(#custom == 0, "custom ids not empty") end unittests.register("test_gennotify_api", test_gennotify_api) + +-- <=> inside_mapgen_env.lua +local function test_mapgen_env(cb) + -- emerge threads start delayed so this can take a second + local res = core.ipc_get("unittests:mg") + if res == nil then + return core.after(0, test_mapgen_env, cb) + end + -- handle error status + if res[1] then + cb() + else + cb(res[2]) + end +end +unittests.register("test_mapgen_env", test_mapgen_env, {async=true}) + +local function test_ipc_vector_preserve(cb) + -- the IPC also uses register_portable_metatable + core.ipc_set("unittests:v", vector.new(4, 0, 4)) + local v = core.ipc_get("unittests:v") + assert(type(v) == "table") + assert(vector.check(v)) +end +unittests.register("test_ipc_vector_preserve", test_ipc_vector_preserve) + +local function test_ipc_poll(cb) + core.ipc_set("unittests:flag", nil) + assert(core.ipc_poll("unittests:flag", 1) == false) + + -- Note that unlike the async result callback - which has to wait for the + -- next server step - the IPC is instant + local t0 = core.get_us_time() + core.handle_async(function() + core.ipc_set("unittests:flag", true) + end, function() end) + assert(core.ipc_poll("unittests:flag", 1000) == true, "Wait failed (or slow machine?)") + print("delta: " .. (core.get_us_time() - t0) .. "us") +end +unittests.register("test_ipc_poll", test_ipc_poll) diff --git a/games/devtest/mods/unittests/player.lua b/games/devtest/mods/unittests/player.lua index 0dbe450b0..f8945f320 100644 --- a/games/devtest/mods/unittests/player.lua +++ b/games/devtest/mods/unittests/player.lua @@ -2,7 +2,7 @@ -- HP Change Reasons -- local expect = nil -minetest.register_on_player_hpchange(function(player, hp, reason) +core.register_on_player_hpchange(function(player, hp_change, reason) if expect == nil then return end @@ -37,6 +37,104 @@ local function run_hpchangereason_tests(player) end unittests.register("test_hpchangereason", run_hpchangereason_tests, {player=true}) +-- +-- HP differences +-- + +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 old_hp = player:get_hp() + local old_hp_max = player:get_properties().hp_max + + hpchange_counter = 0 + die_counter = 0 + + 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) + -- 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) + + -- 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) + -- 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}) + -- -- Player meta -- diff --git a/irr/.github/workflows/build.yml b/irr/.github/workflows/build.yml deleted file mode 100644 index f31521bd2..000000000 --- a/irr/.github/workflows/build.yml +++ /dev/null @@ -1,310 +0,0 @@ -name: build - -# build on c/cpp changes or workflow changes -on: - - push - - pull_request - -jobs: - - linux-gl: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v4 - - name: Install deps - run: | - sudo apt-get update - sudo apt-get install g++ cmake libxi-dev libgl1-mesa-dev libpng-dev libjpeg-dev zlib1g-dev -qyy - - - name: Build - run: | - cmake . -DUSE_SDL2=OFF - make VERBOSE=1 -j2 - - - name: Test - run: | - ctest --output-on-failure - - - name: Package - run: | - make DESTDIR=$PWD/_install install - tar -c -I "gzip -9" -f irrlicht-linux.tar.gz -C ./_install/usr/local . - - - uses: actions/upload-artifact@v4 - with: - name: irrlicht-linux - path: ./irrlicht-linux.tar.gz - - linux-gles: - # Xvfb test is broken on 20.04 for unknown reasons (not our bug) - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - name: Install deps - run: | - sudo apt-get update - sudo apt-get install g++ cmake libxi-dev libgles2-mesa-dev libpng-dev libjpeg-dev zlib1g-dev xvfb -qyy - - - name: Build - run: | - cmake . -DBUILD_EXAMPLES=1 -DUSE_SDL2=OFF -DENABLE_OPENGL=OFF -DENABLE_GLES2=ON - make -j2 - - - name: Test (headless) - run: | - cd bin/Linux - ./AutomatedTest null - - - name: Test (Xvfb) - run: | - cd bin/Linux - LIBGL_ALWAYS_SOFTWARE=true xvfb-run ./AutomatedTest ogles2 - - linux-sdl: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v4 - - name: Install deps - run: | - sudo apt-get update - sudo apt-get install g++ cmake libsdl2-dev libpng-dev libjpeg-dev zlib1g-dev -qyy - - - name: Build - run: | - cmake . -DBUILD_EXAMPLES=1 -DUSE_SDL2=ON -DCMAKE_BUILD_TYPE=Debug - make -j2 - - - name: Test (headless) - run: | - cd bin/Linux - ./AutomatedTest null - - linux-sdl-gl3: - # Xvfb test is broken on 20.04 for unknown reasons (not our bug) - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - name: Install deps - run: | - sudo apt-get update - sudo apt-get install g++ cmake libsdl2-dev libpng-dev libjpeg-dev zlib1g-dev xvfb -qyy - - - name: Build - run: | - cmake . -DBUILD_EXAMPLES=1 -DUSE_SDL2=ON -DENABLE_OPENGL=OFF -DENABLE_OPENGL3=ON - make -j2 - - - name: Test (headless) - run: | - cd bin/Linux - ./AutomatedTest null - - - name: Test (Xvfb) - run: | - cd bin/Linux - LIBGL_ALWAYS_SOFTWARE=true xvfb-run ./AutomatedTest opengl3 - - linux-sdl-gles2: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v4 - - name: Install deps - run: | - sudo apt-get update - sudo apt-get install g++ cmake libsdl2-dev libpng-dev libjpeg-dev zlib1g-dev xvfb -qyy - - - name: Build - run: | - cmake . -DBUILD_EXAMPLES=1 -DUSE_SDL2=ON -DENABLE_OPENGL=OFF -DENABLE_GLES2=ON - make -j2 - - - name: Test (headless) - run: | - cd bin/Linux - ./AutomatedTest null - - - name: Test (Xvfb) - run: | - cd bin/Linux - LIBGL_ALWAYS_SOFTWARE=true xvfb-run ./AutomatedTest ogles2 - - mingw: - name: "MinGW ${{matrix.config.variant}}${{matrix.config.extras}}" - runs-on: ubuntu-22.04 - strategy: - fail-fast: false - matrix: - config: - - {variant: win32, arch: i686} - - {variant: win64, arch: x86_64} - - {variant: win32, arch: i686, extras: "-sdl"} - - {variant: win64, arch: x86_64, extras: "-sdl"} - steps: - - uses: actions/checkout@v4 - - name: Install compiler - run: | - sudo apt-get update && sudo apt-get install cmake -qyy - ./scripts/ci-get-mingw.sh - - - name: Build - run: | - ./scripts/ci-build-mingw.sh package - env: - CC: ${{matrix.config.arch}}-w64-mingw32-clang - CXX: ${{matrix.config.arch}}-w64-mingw32-clang++ - extras: ${{matrix.config.extras}} - - - uses: actions/upload-artifact@v4 - with: - name: irrlicht-${{matrix.config.variant}}${{matrix.config.extras}} - path: ./irrlicht-${{matrix.config.variant}}${{matrix.config.extras}}.zip - - macos: - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - name: Install deps - run: | - brew update --auto-update - brew install cmake libpng jpeg - env: - HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 - HOMEBREW_NO_INSTALL_CLEANUP: 1 - - - name: Build - run: | - cmake . -DCMAKE_FIND_FRAMEWORK=LAST -DBUILD_EXAMPLES=1 - make -j3 - - - name: Test (headless) - run: | - ./bin/OSX/AutomatedTest null - - macos-sdl: - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - name: Install deps - run: | - brew update --auto-update - brew install cmake libpng jpeg sdl2 - env: - HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 - HOMEBREW_NO_INSTALL_CLEANUP: 1 - - - name: Build - run: | - cmake . -DCMAKE_FIND_FRAMEWORK=LAST -DBUILD_EXAMPLES=1 -DUSE_SDL2=1 - make -j3 - - msvc: - name: VS 2019 ${{ matrix.config.arch }} ${{ matrix.sdl.label }} - runs-on: windows-2019 - env: - VCPKG_VERSION: 8eb57355a4ffb410a2e94c07b4dca2dffbee8e50 - # 2023.10.19 - vcpkg_packages: zlib libpng libjpeg-turbo - strategy: - fail-fast: false - matrix: - config: - - - arch: x86 - generator: "-G'Visual Studio 16 2019' -A Win32" - vcpkg_triplet: x86-windows - - - arch: x64 - generator: "-G'Visual Studio 16 2019' -A x64" - vcpkg_triplet: x64-windows - sdl: - - - use: FALSE - label: '(no SDL)' - vcpkg_packages: opengl-registry - - - use: TRUE - label: '(with SDL)' - vcpkg_packages: sdl2 - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Restore from cache and run vcpkg - uses: lukka/run-vcpkg@v7 - with: - vcpkgArguments: ${{env.vcpkg_packages}} ${{matrix.sdl.vcpkg_packages}} - vcpkgDirectory: '${{ github.workspace }}\vcpkg' - appendedCacheKey: ${{ matrix.config.vcpkg_triplet }} - vcpkgGitCommitId: ${{ env.VCPKG_VERSION }} - vcpkgTriplet: ${{ matrix.config.vcpkg_triplet }} - - - name: CMake - run: | - cmake ${{matrix.config.generator}} ` - -DUSE_SDL2=${{matrix.sdl.use}} ` - -DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}\vcpkg\scripts\buildsystems\vcpkg.cmake" ` - -DCMAKE_BUILD_TYPE=Release . - - - name: Build - run: cmake --build . --config Release - - - name: Create artifact folder - run: | - mkdir artifact/ - mkdir artifact/lib/ - - - name: Move dlls into artifact folder - run: move bin\Win32-VisualStudio\Release\* artifact\lib\ - - - name: Move includes into artifact folder - run: move include artifact/ - - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: msvc-${{ matrix.config.arch }}-${{matrix.sdl.use}} - path: artifact/ - - android: - name: Android ${{ matrix.arch }} - runs-on: ubuntu-20.04 - env: - ndk_version: "r25c" - ANDROID_NDK: ${{ github.workspace }}/android-ndk - strategy: - matrix: - arch: [armeabi-v7a, arm64-v8a, x86, x86_64] - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install deps - run: | - sudo rm /var/lib/man-db/auto-update - sudo apt-get update - sudo apt-get install -qyy wget unzip zip gcc-multilib make cmake - - - name: Cache NDK - id: cache-ndk - uses: actions/cache@v4 - with: - key: android-ndk-${{ env.ndk_version }}-linux - path: ${{ env.ANDROID_NDK }} - - - name: Install NDK - run: | - wget --progress=bar:force "http://dl.google.com/android/repository/android-ndk-${ndk_version}-linux.zip" - unzip -q "android-ndk-${ndk_version}-linux.zip" - rm "android-ndk-${ndk_version}-linux.zip" - mv "android-ndk-${ndk_version}" "${ANDROID_NDK}" - if: ${{ steps.cache-ndk.outputs.cache-hit != 'true' }} - - - name: Build - run: ./scripts/ci-build-android.sh ${{ matrix.arch }} - - #- name: Upload Artifact - # uses: actions/upload-artifact@v4 - # with: - # name: irrlicht-android-${{ matrix.arch }} - # path: ${{ runner.temp }}/pkg/${{ matrix.arch }} diff --git a/irr/CMakeLists.txt b/irr/CMakeLists.txt index ccc00f271..dfd6b189a 100644 --- a/irr/CMakeLists.txt +++ b/irr/CMakeLists.txt @@ -11,14 +11,5 @@ if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type: Debug or Release" FORCE) endif() -# FIXME: tests need to be moved to MT if we want to keep them - list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") -#enable_testing() add_subdirectory(src) -#add_subdirectory(test) - -#option(BUILD_EXAMPLES "Build example applications" FALSE) -#if(BUILD_EXAMPLES) -# add_subdirectory(examples) -#endif() diff --git a/irr/README.md b/irr/README.md index 96d3b0d97..50e7338a5 100644 --- a/irr/README.md +++ b/irr/README.md @@ -20,12 +20,11 @@ The following libraries are required to be installed: Aside from standard search options (`ZLIB_INCLUDE_DIR`, `ZLIB_LIBRARY`, ...) the following options are available: * `ENABLE_OPENGL` - Enable OpenGL driver * `ENABLE_OPENGL3` (default: `OFF`) - Enable OpenGL 3+ driver -* `ENABLE_GLES1` - Enable OpenGL ES driver, legacy * `ENABLE_GLES2` - Enable OpenGL ES 2+ driver * `USE_SDL2` (default: platform-dependent, usually `ON`) - Use SDL2 instead of older native device code However, IrrlichtMt cannot be built or installed separately. - + Platforms --------- diff --git a/irr/examples/AutomatedTest/main.cpp b/irr/examples/AutomatedTest/main.cpp deleted file mode 100644 index c9e5bab68..000000000 --- a/irr/examples/AutomatedTest/main.cpp +++ /dev/null @@ -1,154 +0,0 @@ -#include -#include -#include "exampleHelper.h" - -using namespace irr; - -static IrrlichtDevice *device = nullptr; -static int test_fail = 0; - -void test_irr_array(); -void test_irr_string(); - -static video::E_DRIVER_TYPE chooseDriver(core::stringc arg_) -{ - if (arg_ == "null") - return video::EDT_NULL; - if (arg_ == "ogles1") - return video::EDT_OGLES1; - if (arg_ == "ogles2") - return video::EDT_OGLES2; - if (arg_ == "opengl") - return video::EDT_OPENGL; - if (arg_ == "opengl3") - return video::EDT_OPENGL3; - std::cerr << "Unknown driver type: " << arg_.c_str() << ". Trying OpenGL." << std::endl; - return video::EDT_OPENGL; -} - -static inline void check(bool ok, const char *msg) -{ - if (!ok) { - test_fail++; - device->getLogger()->log((core::stringc("FAILED TEST: ") + msg).c_str(), ELL_ERROR); - } -} - -void run_unit_tests() -{ - std::cout << "Running unit tests:" << std::endl; - try { - test_irr_array(); - test_irr_string(); - } catch (const std::exception &e) { - std::cerr << e.what() << std::endl; - test_fail++; - } - std::cout << std::endl; -} - -int main(int argc, char *argv[]) -{ - run_unit_tests(); - - SIrrlichtCreationParameters p; - p.DriverType = chooseDriver(argc > 1 ? argv[1] : ""); - p.WindowSize = core::dimension2du(640, 480); - p.Vsync = true; - p.LoggingLevel = ELL_DEBUG; - - device = createDeviceEx(p); - if (!device) - return 1; - - { - u32 total = 0; - device->getOSOperator()->getSystemMemory(&total, nullptr); - core::stringc message = core::stringc("Total RAM in MiB: ") + core::stringc(total >> 10); - device->getLogger()->log(message.c_str(), ELL_INFORMATION); - check(total > 130 * 1024, "RAM amount"); - } - - device->setWindowCaption(L"Hello World!"); - device->setResizable(true); - - video::IVideoDriver *driver = device->getVideoDriver(); - scene::ISceneManager *smgr = device->getSceneManager(); - gui::IGUIEnvironment *guienv = device->getGUIEnvironment(); - - guienv->addStaticText(L"sample text", core::rect(10, 10, 110, 22), false); - - gui::IGUIButton *button = guienv->addButton( - core::rect(10, 30, 110, 30 + 32), 0, -1, L"sample button", - L"sample tooltip"); - - gui::IGUIEditBox *editbox = guienv->addEditBox(L"", - core::rect(10, 70, 60, 70 + 16)); - - const io::path mediaPath = getExampleMediaPath(); - - auto mesh_file = device->getFileSystem()->createAndOpenFile(mediaPath + "coolguy_opt.x"); - check(mesh_file, "mesh file loading"); - scene::IAnimatedMesh *mesh = smgr->getMesh(mesh_file); - check(mesh, "mesh loading"); - if (mesh_file) - mesh_file->drop(); - if (mesh) { - video::ITexture *tex = driver->getTexture(mediaPath + "cooltexture.png"); - check(tex, "texture loading"); - scene::IAnimatedMeshSceneNode *node = smgr->addAnimatedMeshSceneNode(mesh); - if (node) { - node->forEachMaterial([tex](video::SMaterial &mat) { - mat.Lighting = false; - mat.setTexture(0, tex); - }); - node->setFrameLoop(0, 29); - node->setAnimationSpeed(30); - } - } - - smgr->addCameraSceneNode(0, core::vector3df(0, 4, 5), core::vector3df(0, 2, 0)); - - s32 n = 0; - SEvent event; - device->getTimer()->start(); - - while (device->run()) { - if (device->getTimer()->getTime() >= 1000) { - device->getTimer()->setTime(0); - ++n; - if (n == 1) { // Tooltip display - bzero(&event, sizeof(SEvent)); - event.EventType = irr::EET_MOUSE_INPUT_EVENT; - event.MouseInput.Event = irr::EMIE_MOUSE_MOVED; - event.MouseInput.X = button->getAbsolutePosition().getCenter().X; - event.MouseInput.Y = button->getAbsolutePosition().getCenter().Y; - device->postEventFromUser(event); - } else if (n == 2) // Text input focus - guienv->setFocus(editbox); - else if (n == 3) { // Keypress for Text input - bzero(&event, sizeof(SEvent)); - event.EventType = irr::EET_KEY_INPUT_EVENT; - event.KeyInput.Char = L'a'; - event.KeyInput.Key = KEY_KEY_A; - event.KeyInput.PressedDown = true; - device->postEventFromUser(event); - event.KeyInput.PressedDown = false; - device->postEventFromUser(event); - } else - device->closeDevice(); - } - - driver->beginScene(video::ECBF_COLOR | video::ECBF_DEPTH, - video::SColor(255, 100, 100, 150)); - smgr->drawAll(); - guienv->drawAll(); - driver->endScene(); - } - - check(core::stringw(L"a") == editbox->getText(), "EditBox text"); - - device->getLogger()->log("Done.", ELL_INFORMATION); - device->drop(); - return test_fail > 0 ? 1 : 0; -} diff --git a/irr/examples/AutomatedTest/test_array.cpp b/irr/examples/AutomatedTest/test_array.cpp deleted file mode 100644 index 42959e913..000000000 --- a/irr/examples/AutomatedTest/test_array.cpp +++ /dev/null @@ -1,138 +0,0 @@ -#include -#include "test_helper.h" - -using namespace irr; -using core::array; - -static void test_basics() -{ - array v; - v.push_back(1); // 1 - v.push_front(2); // 2, 1 - v.insert(4, 0); // 4, 2, 1 - v.insert(3, 1); // 4, 3, 2, 1 - v.insert(0, 4); // 4, 3, 2, 1, 0 - UASSERTEQ(v.size(), 5); - UASSERTEQ(v[0], 4); - UASSERTEQ(v[1], 3); - UASSERTEQ(v[2], 2); - UASSERTEQ(v[3], 1); - UASSERTEQ(v[4], 0); - array w = v; - UASSERTEQ(w.size(), 5); - UASSERT(w == v); - w.clear(); - UASSERTEQ(w.size(), 0); - UASSERTEQ(w.allocated_size(), 0); - UASSERT(w.empty()); - w = v; - UASSERTEQ(w.size(), 5); - w.set_used(3); - UASSERTEQ(w.size(), 3); - UASSERTEQ(w[0], 4); - UASSERTEQ(w[1], 3); - UASSERTEQ(w[2], 2); - UASSERTEQ(w.getLast(), 2); - w.set_used(20); - UASSERTEQ(w.size(), 20); - w = v; - w.sort(); - UASSERTEQ(w.size(), 5); - UASSERTEQ(w[0], 0); - UASSERTEQ(w[1], 1); - UASSERTEQ(w[2], 2); - UASSERTEQ(w[3], 3); - UASSERTEQ(w[4], 4); - w.erase(0); - UASSERTEQ(w.size(), 4); - UASSERTEQ(w[0], 1); - UASSERTEQ(w[1], 2); - UASSERTEQ(w[2], 3); - UASSERTEQ(w[3], 4); - w.erase(1, 2); - UASSERTEQ(w.size(), 2); - UASSERTEQ(w[0], 1); - UASSERTEQ(w[1], 4); - w.swap(v); - UASSERTEQ(w.size(), 5); - UASSERTEQ(v.size(), 2); -} - -static void test_linear_searches() -{ - // Populate the array with 0, 1, 2, ..., 100, 100, 99, 98, 97, ..., 0 - array arr; - for (int i = 0; i <= 100; i++) - arr.push_back(i); - for (int i = 100; i >= 0; i--) - arr.push_back(i); - s32 end = arr.size() - 1; - for (int i = 0; i <= 100; i++) { - s32 index = arr.linear_reverse_search(i); - UASSERTEQ(index, end - i); - } - for (int i = 0; i <= 100; i++) { - s32 index = arr.linear_search(i); - UASSERTEQ(index, i); - } -} - -static void test_binary_searches() -{ - const auto &values = {3, 5, 1, 2, 5, 10, 19, 9, 7, 1, 2, 5, 8, 15}; - array arr; - for (int value : values) { - arr.push_back(value); - } - // Test the const form first, it uses a linear search without sorting - const array &carr = arr; - UASSERTEQ(carr.binary_search(20), -1); - UASSERTEQ(carr.binary_search(0), -1); - UASSERTEQ(carr.binary_search(1), 2); - - // Sorted: 1, 1, 2, 2, 3, 5, 5, 5, 7, 8, 9, 10, 15, 19 - UASSERTEQ(arr.binary_search(20), -1); - UASSERTEQ(arr.binary_search(0), -1); - - for (int value : values) { - s32 i = arr.binary_search(value); - UASSERTNE(i, -1); - UASSERTEQ(arr[i], value); - } - - s32 first, last; - first = arr.binary_search_multi(1, last); - UASSERTEQ(first, 0); - UASSERTEQ(last, 1); - - first = arr.binary_search_multi(2, last); - UASSERTEQ(first, 2); - UASSERTEQ(last, 3); - - first = arr.binary_search_multi(3, last); - UASSERTEQ(first, 4); - UASSERTEQ(last, 4); - - first = arr.binary_search_multi(4, last); - UASSERTEQ(first, -1); - - first = arr.binary_search_multi(5, last); - UASSERTEQ(first, 5); - UASSERTEQ(last, 7); - - first = arr.binary_search_multi(7, last); - UASSERTEQ(first, 8); - UASSERTEQ(last, 8); - - first = arr.binary_search_multi(19, last); - UASSERTEQ(first, 13); - UASSERTEQ(last, 13); -} - -void test_irr_array() -{ - test_basics(); - test_linear_searches(); - test_binary_searches(); - std::cout << " test_irr_array PASSED" << std::endl; -} diff --git a/irr/examples/AutomatedTest/test_helper.h b/irr/examples/AutomatedTest/test_helper.h deleted file mode 100644 index 5229eff29..000000000 --- a/irr/examples/AutomatedTest/test_helper.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include -#include - -class TestFailedException : public std::exception -{ -}; - -// Asserts the comparison specified by CMP is true, or fails the current unit test -#define UASSERTCMP(CMP, actual, expected) \ - do { \ - const auto &a = (actual); \ - const auto &e = (expected); \ - if (!CMP(a, e)) { \ - std::cout \ - << "Test assertion failed: " << #actual << " " << #CMP << " " \ - << #expected << std::endl \ - << " at " << __FILE__ << ":" << __LINE__ << std::endl \ - << " actual: " << a << std::endl \ - << " expected: " \ - << e << std::endl; \ - throw TestFailedException(); \ - } \ - } while (0) - -#define CMPEQ(a, e) (a == e) -#define CMPTRUE(a, e) (a) -#define CMPNE(a, e) (a != e) - -#define UASSERTEQ(actual, expected) UASSERTCMP(CMPEQ, actual, expected) -#define UASSERTNE(actual, nexpected) UASSERTCMP(CMPNE, actual, nexpected) -#define UASSERT(actual) UASSERTCMP(CMPTRUE, actual, true) diff --git a/irr/examples/AutomatedTest/test_string.cpp b/irr/examples/AutomatedTest/test_string.cpp deleted file mode 100644 index 4d1291f18..000000000 --- a/irr/examples/AutomatedTest/test_string.cpp +++ /dev/null @@ -1,205 +0,0 @@ -#include -#include -#include -#include -#include "test_helper.h" - -using namespace irr; -using namespace irr::core; - -#define CMPSTR(a, b) (!strcmp(a, b)) -#define UASSERTSTR(actual, expected) UASSERTCMP(CMPSTR, actual.c_str(), expected) - -static void test_basics() -{ - // ctor - stringc s; - UASSERTEQ(s.c_str()[0], '\0'); - s = stringc(0.1234567); - UASSERTSTR(s, "0.123457"); - s = stringc(0x1p+53); - UASSERTSTR(s, "9007199254740992.000000"); - s = stringc(static_cast(-102400)); - UASSERTSTR(s, "-102400"); - s = stringc(static_cast(102400)); - UASSERTSTR(s, "102400"); - s = stringc(static_cast(-1024000)); - UASSERTSTR(s, "-1024000"); - s = stringc(static_cast(1024000)); - UASSERTSTR(s, "1024000"); - s = stringc("YESno", 3); - UASSERTSTR(s, "YES"); - s = stringc(L"test", 4); - UASSERTSTR(s, "test"); - s = stringc("Hello World!"); - UASSERTSTR(s, "Hello World!"); - // operator= - s = stringw(L"abcdef"); - UASSERTSTR(s, "abcdef"); - s = L"abcdef"; - UASSERTSTR(s, "abcdef"); - s = static_cast(nullptr); - UASSERTSTR(s, ""); - // operator+ - s = s + stringc("foo"); - UASSERTSTR(s, "foo"); - s = s + L"bar"; - UASSERTSTR(s, "foobar"); - // the rest - s = "f"; - UASSERTEQ(s[0], 'f'); - const auto &sref = s; - UASSERTEQ(sref[0], 'f'); - UASSERT(sref == "f"); - UASSERT(sref == stringc("f")); - s = "a"; - UASSERT(sref < stringc("aa")); - UASSERT(sref < stringc("b")); - UASSERT(stringc("Z") < sref); - UASSERT(!(sref < stringc("a"))); - UASSERT(sref.lower_ignore_case("AA")); - UASSERT(sref.lower_ignore_case("B")); - UASSERT(!sref.lower_ignore_case("A")); - s = "dog"; - UASSERT(sref != "cat"); - UASSERT(sref != stringc("cat")); -} - -static void test_methods() -{ - stringc s; - const auto &sref = s; - s = "irrlicht"; - UASSERTEQ(sref.size(), 8); - UASSERT(!sref.empty()); - s.clear(); - UASSERTEQ(sref.size(), 0); - UASSERT(sref.empty()); - UASSERT(sref[0] == 0); - s = "\tAz#`"; - s.make_lower(); - UASSERTSTR(s, "\taz#`"); - s.make_upper(); - UASSERTSTR(s, "\tAZ#`"); - UASSERT(sref.equals_ignore_case("\taz#`")); - UASSERT(sref.equals_substring_ignore_case("Z#`", 2)); - s = "irrlicht"; - UASSERT(sref.equalsn(stringc("irr"), 3)); - UASSERT(sref.equalsn("irr", 3)); - s = "fo"; - s.append('o'); - UASSERTSTR(s, "foo"); - s.append("bar", 1); - UASSERTSTR(s, "foob"); - s.append("ar", 999999); - UASSERTSTR(s, "foobar"); - s = "nyan"; - s.append(stringc("cat")); - UASSERTSTR(s, "nyancat"); - s.append(stringc("sam"), 1); - UASSERTSTR(s, "nyancats"); - s = "fbar"; - s.insert(1, "ooXX", 2); - UASSERTSTR(s, "foobar"); - UASSERTEQ(sref.findFirst('o'), 1); - UASSERTEQ(sref.findFirst('X'), -1); - UASSERTEQ(sref.findFirstChar("abff", 2), 3); - UASSERTEQ(sref.findFirstCharNotInList("fobb", 2), 3); - UASSERTEQ(sref.findLast('o'), 2); - UASSERTEQ(sref.findLast('X'), -1); - UASSERTEQ(sref.findLastChar("abrr", 2), 4); - UASSERTEQ(sref.findLastCharNotInList("rabb", 2), 3); - UASSERTEQ(sref.findNext('o', 2), 2); - UASSERTEQ(sref.findLast('o', 1), 1); - s = "ob-oob"; - UASSERTEQ(sref.find("ob", 1), 4); - UASSERTEQ(sref.find("ob"), 0); - UASSERTEQ(sref.find("?"), -1); - s = "HOMEOWNER"; - stringc s2 = sref.subString(2, 4); - UASSERTSTR(s2, "MEOW"); - s2 = sref.subString(2, 4, true); - UASSERTSTR(s2, "meow"); - s = "land"; - s.replace('l', 's'); - UASSERTSTR(s, "sand"); - s = ">dog<"; - s.replace("dog", "cat"); - UASSERTSTR(s, ">cat<"); - s.replace("cat", "horse"); - UASSERTSTR(s, ">horse<"); - s.replace("horse", "gnu"); - UASSERTSTR(s, ">gnu<"); - s = " h e l p "; - s.remove(' '); - UASSERTSTR(s, "help"); - s.remove("el"); - UASSERTSTR(s, "hp"); - s = "irrlicht"; - s.removeChars("it"); - UASSERTSTR(s, "rrlch"); - s = "\r\nfoo bar "; - s.trim(); - UASSERTSTR(s, "foo bar"); - s = "foxo"; - s.erase(2); - UASSERTSTR(s, "foo"); - s = "a"; - s.append('\0'); - s.append('b'); - UASSERTEQ(s.size(), 3); - s.validate(); - UASSERTEQ(s.size(), 1); - UASSERTEQ(s.lastChar(), 'a'); - std::vector res; - s = "a,,b,c"; - s.split(res, ",aa", 1, true, false); - UASSERTEQ(res.size(), 3); - UASSERTSTR(res[0], "a"); - UASSERTSTR(res[2], "c"); - res.clear(); - s.split(res, ",", 1, false, true); - UASSERTEQ(res.size(), 7); - UASSERTSTR(res[0], "a"); - UASSERTSTR(res[2], ""); - for (int i = 0; i < 3; i++) - UASSERTSTR(res[2 * i + 1], ","); -} - -static void test_conv() -{ - // locale-independent - - stringw out; - utf8ToWString(out, "†††"); - UASSERTEQ(out.size(), 3); - for (int i = 0; i < 3; i++) - UASSERTEQ(static_cast(out[i]), 0x2020); - - stringc out2; - wStringToUTF8(out2, L"†††"); - UASSERTEQ(out2.size(), 9); - for (int i = 0; i < 3; i++) { - UASSERTEQ(static_cast(out2[3 * i]), 0xe2); - UASSERTEQ(static_cast(out2[3 * i + 1]), 0x80); - UASSERTEQ(static_cast(out2[3 * i + 2]), 0xa0); - } - - // locale-dependent - if (!setlocale(LC_CTYPE, "C.UTF-8")) - setlocale(LC_CTYPE, "UTF-8"); // macOS - - stringw out3; - multibyteToWString(out3, "†††"); - UASSERTEQ(out3.size(), 3); - for (int i = 0; i < 3; i++) - UASSERTEQ(static_cast(out3[i]), 0x2020); -} - -void test_irr_string() -{ - test_basics(); - test_methods(); - test_conv(); - std::cout << " test_irr_string PASSED" << std::endl; -} diff --git a/irr/examples/CMakeLists.txt b/irr/examples/CMakeLists.txt deleted file mode 100644 index 03553048e..000000000 --- a/irr/examples/CMakeLists.txt +++ /dev/null @@ -1,17 +0,0 @@ -set(IRREXAMPLES - # removed -) -if(UNIX) - list(APPEND IRREXAMPLES AutomatedTest) -endif() - -foreach(exname IN ITEMS ${IRREXAMPLES}) - file(GLOB sources "${CMAKE_CURRENT_SOURCE_DIR}/${exname}/*.cpp") - add_executable(${exname} ${sources}) - - target_include_directories(${exname} PRIVATE - ${CMAKE_SOURCE_DIR}/include - ${CMAKE_CURRENT_SOURCE_DIR}/${exname} - ) - target_link_libraries(${exname} IrrlichtMt) -endforeach() diff --git a/irr/include/CIndexBuffer.h b/irr/include/CIndexBuffer.h new file mode 100644 index 000000000..904b0ab9a --- /dev/null +++ b/irr/include/CIndexBuffer.h @@ -0,0 +1,103 @@ +// Copyright (C) 2002-2012 Nikolaus Gebhardt +// This file is part of the "Irrlicht Engine". +// For conditions of distribution and use, see copyright notice in irrlicht.h + +#pragma once + +#include +#include "IIndexBuffer.h" + +// Define to receive warnings when violating the hw mapping hints +//#define INDEXBUFFER_HINT_DEBUG + +#ifdef INDEXBUFFER_HINT_DEBUG +#include "../src/os.h" +#endif + +namespace irr +{ +namespace scene +{ +//! Template implementation of the IIndexBuffer interface +template +class CIndexBuffer final : public IIndexBuffer +{ +public: + //! Default constructor for empty buffer + CIndexBuffer() + { +#ifdef _DEBUG + setDebugName("CIndexBuffer"); +#endif + } + + video::E_INDEX_TYPE getType() const override + { + static_assert(sizeof(T) == 2 || sizeof(T) == 4, "invalid index type"); + return sizeof(T) == 2 ? video::EIT_16BIT : video::EIT_32BIT; + } + + const void *getData() const override + { + return Data.data(); + } + + void *getData() override + { + return Data.data(); + } + + u32 getCount() const override + { + return static_cast(Data.size()); + } + + E_HARDWARE_MAPPING getHardwareMappingHint() const override + { + return MappingHint; + } + + void setHardwareMappingHint(E_HARDWARE_MAPPING NewMappingHint) override + { + MappingHint = NewMappingHint; + } + + void setDirty() override + { + ++ChangedID; +#ifdef INDEXBUFFER_HINT_DEBUG + if (MappingHint == EHM_STATIC && HWBuffer) { + char buf[100]; + snprintf_irr(buf, sizeof(buf), "CIndexBuffer @ %p modified, but it has a static hint", this); + os::Printer::log(buf, ELL_WARNING); + } +#endif + } + + u32 getChangedID() const override { return ChangedID; } + + void setHWBuffer(void *ptr) const override + { + HWBuffer = ptr; + } + + void *getHWBuffer() const override + { + return HWBuffer; + } + + u32 ChangedID = 1; + + //! hardware mapping hint + E_HARDWARE_MAPPING MappingHint = EHM_NEVER; + mutable void *HWBuffer = nullptr; + + //! Indices of this buffer + std::vector Data; +}; + +//! Standard 16-bit buffer +typedef CIndexBuffer SIndexBuffer; + +} // end namespace scene +} // end namespace irr diff --git a/irr/include/CMeshBuffer.h b/irr/include/CMeshBuffer.h index 8f8158ff1..9a6d4426f 100644 --- a/irr/include/CMeshBuffer.h +++ b/irr/include/CMeshBuffer.h @@ -4,8 +4,10 @@ #pragma once -#include "irrArray.h" +#include #include "IMeshBuffer.h" +#include "CVertexBuffer.h" +#include "CIndexBuffer.h" namespace irr { @@ -13,16 +15,24 @@ namespace scene { //! Template implementation of the IMeshBuffer interface template -class CMeshBuffer : public IMeshBuffer +class CMeshBuffer final : public IMeshBuffer { public: //! Default constructor for empty meshbuffer CMeshBuffer() : - ChangedID_Vertex(1), ChangedID_Index(1), MappingHint_Vertex(EHM_NEVER), MappingHint_Index(EHM_NEVER), HWBuffer(NULL), PrimitiveType(EPT_TRIANGLES) + PrimitiveType(EPT_TRIANGLES) { #ifdef _DEBUG setDebugName("CMeshBuffer"); #endif + Vertices = new CVertexBuffer(); + Indices = new SIndexBuffer(); + } + + ~CMeshBuffer() + { + Vertices->drop(); + Indices->drop(); } //! Get material of this meshbuffer @@ -39,53 +49,24 @@ public: return Material; } - //! Get pointer to vertices - /** \return Pointer to vertices. */ - const void *getVertices() const override + const scene::IVertexBuffer *getVertexBuffer() const override { - return Vertices.const_pointer(); + return Vertices; } - //! Get pointer to vertices - /** \return Pointer to vertices. */ - void *getVertices() override + scene::IVertexBuffer *getVertexBuffer() override { - return Vertices.pointer(); + return Vertices; } - //! Get number of vertices - /** \return Number of vertices. */ - u32 getVertexCount() const override + const scene::IIndexBuffer *getIndexBuffer() const override { - return Vertices.size(); + return Indices; } - //! Get type of index data which is stored in this meshbuffer. - /** \return Index type of this buffer. */ - video::E_INDEX_TYPE getIndexType() const override + scene::IIndexBuffer *getIndexBuffer() override { - return video::EIT_16BIT; - } - - //! Get pointer to indices - /** \return Pointer to indices. */ - const u16 *getIndices() const override - { - return Indices.const_pointer(); - } - - //! Get pointer to indices - /** \return Pointer to indices. */ - u16 *getIndices() override - { - return Indices.pointer(); - } - - //! Get number of indices - /** \return Number of indices. */ - u32 getIndexCount() const override - { - return Indices.size(); + return Indices; } //! Get the axis aligned bounding box @@ -107,102 +88,34 @@ public: /** should be called if the mesh changed. */ void recalculateBoundingBox() override { - if (!Vertices.empty()) { - BoundingBox.reset(Vertices[0].Pos); - const irr::u32 vsize = Vertices.size(); + if (Vertices->getCount()) { + BoundingBox.reset(Vertices->getPosition(0)); + const irr::u32 vsize = Vertices->getCount(); for (u32 i = 1; i < vsize; ++i) - BoundingBox.addInternalPoint(Vertices[i].Pos); + BoundingBox.addInternalPoint(Vertices->getPosition(i)); } else BoundingBox.reset(0, 0, 0); } - //! Get type of vertex data stored in this buffer. - /** \return Type of vertex data. */ - video::E_VERTEX_TYPE getVertexType() const override - { - return T::getType(); - } - - //! returns position of vertex i - const core::vector3df &getPosition(u32 i) const override - { - return Vertices[i].Pos; - } - - //! returns position of vertex i - core::vector3df &getPosition(u32 i) override - { - return Vertices[i].Pos; - } - - //! returns normal of vertex i - const core::vector3df &getNormal(u32 i) const override - { - return Vertices[i].Normal; - } - - //! returns normal of vertex i - core::vector3df &getNormal(u32 i) override - { - return Vertices[i].Normal; - } - - //! returns texture coord of vertex i - const core::vector2df &getTCoords(u32 i) const override - { - return Vertices[i].TCoords; - } - - //! returns texture coord of vertex i - core::vector2df &getTCoords(u32 i) override - { - return Vertices[i].TCoords; - } - //! Append the vertices and indices to the current buffer - /** Only works for compatible types, i.e. either the same type - or the main buffer is of standard type. Otherwise, behavior is - undefined. - */ void append(const void *const vertices, u32 numVertices, const u16 *const indices, u32 numIndices) override { if (vertices == getVertices()) return; const u32 vertexCount = getVertexCount(); - u32 i; + const u32 indexCount = getIndexCount(); - Vertices.reallocate(vertexCount + numVertices); - for (i = 0; i < numVertices; ++i) { - Vertices.push_back(static_cast(vertices)[i]); - BoundingBox.addInternalPoint(static_cast(vertices)[i].Pos); + auto *vt = static_cast(vertices); + Vertices->Data.insert(Vertices->Data.end(), vt, vt + numVertices); + for (u32 i = vertexCount; i < getVertexCount(); i++) + BoundingBox.addInternalPoint(Vertices->getPosition(i)); + + Indices->Data.insert(Indices->Data.end(), indices, indices + numIndices); + if (vertexCount != 0) { + for (u32 i = indexCount; i < getIndexCount(); i++) + Indices->Data[i] += vertexCount; } - - Indices.reallocate(getIndexCount() + numIndices); - for (i = 0; i < numIndices; ++i) { - Indices.push_back(indices[i] + vertexCount); - } - } - - //! get the current hardware mapping hint - E_HARDWARE_MAPPING getHardwareMappingHint_Vertex() const override - { - return MappingHint_Vertex; - } - - //! get the current hardware mapping hint - E_HARDWARE_MAPPING getHardwareMappingHint_Index() const override - { - return MappingHint_Index; - } - - //! set the hardware mapping hint, for driver - void setHardwareMappingHint(E_HARDWARE_MAPPING NewMappingHint, E_BUFFER_TYPE Buffer = EBT_VERTEX_AND_INDEX) override - { - if (Buffer == EBT_VERTEX_AND_INDEX || Buffer == EBT_VERTEX) - MappingHint_Vertex = NewMappingHint; - if (Buffer == EBT_VERTEX_AND_INDEX || Buffer == EBT_INDEX) - MappingHint_Index = NewMappingHint; } //! Describe what kind of primitive geometry is used by the meshbuffer @@ -217,47 +130,12 @@ public: return PrimitiveType; } - //! flags the mesh as changed, reloads hardware buffers - void setDirty(E_BUFFER_TYPE Buffer = EBT_VERTEX_AND_INDEX) override - { - if (Buffer == EBT_VERTEX_AND_INDEX || Buffer == EBT_VERTEX) - ++ChangedID_Vertex; - if (Buffer == EBT_VERTEX_AND_INDEX || Buffer == EBT_INDEX) - ++ChangedID_Index; - } - - //! Get the currently used ID for identification of changes. - /** This shouldn't be used for anything outside the VideoDriver. */ - u32 getChangedID_Vertex() const override { return ChangedID_Vertex; } - - //! Get the currently used ID for identification of changes. - /** This shouldn't be used for anything outside the VideoDriver. */ - u32 getChangedID_Index() const override { return ChangedID_Index; } - - void setHWBuffer(void *ptr) const override - { - HWBuffer = ptr; - } - - void *getHWBuffer() const override - { - return HWBuffer; - } - - u32 ChangedID_Vertex; - u32 ChangedID_Index; - - //! hardware mapping hint - E_HARDWARE_MAPPING MappingHint_Vertex; - E_HARDWARE_MAPPING MappingHint_Index; - mutable void *HWBuffer; - //! Material for this meshbuffer. video::SMaterial Material; - //! Vertices of this buffer - core::array Vertices; - //! Indices into the vertices of this buffer. - core::array Indices; + //! Vertex buffer + CVertexBuffer *Vertices; + //! Index buffer + SIndexBuffer *Indices; //! Bounding box of this meshbuffer. core::aabbox3d BoundingBox; //! Primitive type used for rendering (triangles, lines, ...) diff --git a/irr/include/CVertexBuffer.h b/irr/include/CVertexBuffer.h new file mode 100644 index 000000000..4b3f33688 --- /dev/null +++ b/irr/include/CVertexBuffer.h @@ -0,0 +1,136 @@ +// Copyright (C) 2002-2012 Nikolaus Gebhardt +// This file is part of the "Irrlicht Engine". +// For conditions of distribution and use, see copyright notice in irrlicht.h + +#pragma once + +#include +#include "IVertexBuffer.h" + +// Define to receive warnings when violating the hw mapping hints +//#define VERTEXBUFFER_HINT_DEBUG + +#ifdef VERTEXBUFFER_HINT_DEBUG +#include "../src/os.h" +#endif + +namespace irr +{ +namespace scene +{ +//! Template implementation of the IVertexBuffer interface +template +class CVertexBuffer final : public IVertexBuffer +{ +public: + //! Default constructor for empty buffer + CVertexBuffer() + { +#ifdef _DEBUG + setDebugName("CVertexBuffer"); +#endif + } + + const void *getData() const override + { + return Data.data(); + } + + void *getData() override + { + return Data.data(); + } + + u32 getCount() const override + { + return static_cast(Data.size()); + } + + video::E_VERTEX_TYPE getType() const override + { + return T::getType(); + } + + const core::vector3df &getPosition(u32 i) const override + { + return Data[i].Pos; + } + + core::vector3df &getPosition(u32 i) override + { + return Data[i].Pos; + } + + const core::vector3df &getNormal(u32 i) const override + { + return Data[i].Normal; + } + + core::vector3df &getNormal(u32 i) override + { + return Data[i].Normal; + } + + const core::vector2df &getTCoords(u32 i) const override + { + return Data[i].TCoords; + } + + core::vector2df &getTCoords(u32 i) override + { + return Data[i].TCoords; + } + + E_HARDWARE_MAPPING getHardwareMappingHint() const override + { + return MappingHint; + } + + void setHardwareMappingHint(E_HARDWARE_MAPPING NewMappingHint) override + { + MappingHint = NewMappingHint; + } + + void setDirty() override + { + ++ChangedID; +#ifdef VERTEXBUFFER_HINT_DEBUG + if (MappingHint == EHM_STATIC && HWBuffer) { + char buf[100]; + snprintf_irr(buf, sizeof(buf), "CVertexBuffer @ %p modified, but it has a static hint", this); + os::Printer::log(buf, ELL_WARNING); + } +#endif + } + + u32 getChangedID() const override { return ChangedID; } + + void setHWBuffer(void *ptr) const override + { + HWBuffer = ptr; + } + + void *getHWBuffer() const override + { + return HWBuffer; + } + + u32 ChangedID = 1; + + //! hardware mapping hint + E_HARDWARE_MAPPING MappingHint = EHM_NEVER; + mutable void *HWBuffer = nullptr; + + //! Vertices of this buffer + std::vector Data; +}; + +//! Standard buffer +typedef CVertexBuffer SVertexBuffer; +//! Buffer with two texture coords per vertex, e.g. for lightmaps +typedef CVertexBuffer SVertexBufferLightMap; +//! Buffer with vertices having tangents stored, e.g. for normal mapping +typedef CVertexBuffer SVertexBufferTangents; + +} // end namespace scene +} // end namespace irr diff --git a/irr/include/EDriverTypes.h b/irr/include/EDriverTypes.h index 33f987889..f19e65ace 100644 --- a/irr/include/EDriverTypes.h +++ b/irr/include/EDriverTypes.h @@ -24,9 +24,6 @@ enum E_DRIVER_TYPE primitives. */ EDT_OPENGL, - //! OpenGL-ES 1.x driver, for embedded and mobile systems - EDT_OGLES1, - //! OpenGL-ES 2.x driver, for embedded and mobile systems /** Supports shaders etc. */ EDT_OGLES2, diff --git a/irr/include/EMaterialProps.h b/irr/include/EMaterialProps.h index 6f37c95b8..765084340 100644 --- a/irr/include/EMaterialProps.h +++ b/irr/include/EMaterialProps.h @@ -18,12 +18,6 @@ enum E_MATERIAL_PROP //! Corresponds to SMaterial::PointCloud. EMP_POINTCLOUD = 0x2, - //! Corresponds to SMaterial::GouraudShading. - EMP_GOURAUD_SHADING = 0x4, - - //! Corresponds to SMaterial::Lighting. - EMP_LIGHTING = 0x8, - //! Corresponds to SMaterial::ZBuffer. EMP_ZBUFFER = 0x10, @@ -48,9 +42,6 @@ enum E_MATERIAL_PROP //! Corresponds to SMaterial::FogEnable. EMP_FOG_ENABLE = 0x800, - //! Corresponds to SMaterial::NormalizeNormals. - EMP_NORMALIZE_NORMALS = 0x1000, - //! Corresponds to SMaterialLayer::TextureWrapU, TextureWrapV and //! TextureWrapW. EMP_TEXTURE_WRAP = 0x2000, @@ -61,9 +52,6 @@ enum E_MATERIAL_PROP //! Corresponds to SMaterial::ColorMask. EMP_COLOR_MASK = 0x8000, - //! Corresponds to SMaterial::ColorMaterial. - EMP_COLOR_MATERIAL = 0x10000, - //! Corresponds to SMaterial::UseMipMaps. EMP_USE_MIP_MAPS = 0x20000, diff --git a/irr/include/EVideoTypes.h b/irr/include/EVideoTypes.h new file mode 100644 index 000000000..fe90f0652 --- /dev/null +++ b/irr/include/EVideoTypes.h @@ -0,0 +1,75 @@ +// Copyright (C) 2002-2012 Nikolaus Gebhardt +// This file is part of the "Irrlicht Engine". +// For conditions of distribution and use, see copyright notice in irrlicht.h + +#pragma once + +#include "SMaterial.h" // MATERIAL_MAX_TEXTURES + +namespace irr::video +{ + +//! enumeration for geometry transformation states +enum E_TRANSFORMATION_STATE +{ + //! View transformation + ETS_VIEW = 0, + //! World transformation + ETS_WORLD, + //! Projection transformation + ETS_PROJECTION, + //! Texture 0 transformation + //! Use E_TRANSFORMATION_STATE(ETS_TEXTURE_0 + texture_number) to access other texture transformations + ETS_TEXTURE_0, + //! Only used internally + ETS_COUNT = ETS_TEXTURE_0 + MATERIAL_MAX_TEXTURES +}; + +//! Special render targets, which usually map to dedicated hardware +/** These render targets (besides 0 and 1) need not be supported by gfx cards */ +enum E_RENDER_TARGET +{ + //! Render target is the main color frame buffer + ERT_FRAME_BUFFER = 0, + //! Render target is a render texture + ERT_RENDER_TEXTURE, + //! Multi-Render target textures + ERT_MULTI_RENDER_TEXTURES, + //! Render target is the main color frame buffer + ERT_STEREO_LEFT_BUFFER, + //! Render target is the right color buffer (left is the main buffer) + ERT_STEREO_RIGHT_BUFFER, + //! Render to both stereo buffers at once + ERT_STEREO_BOTH_BUFFERS, + //! Auxiliary buffer 0 + ERT_AUX_BUFFER0, + //! Auxiliary buffer 1 + ERT_AUX_BUFFER1, + //! Auxiliary buffer 2 + ERT_AUX_BUFFER2, + //! Auxiliary buffer 3 + ERT_AUX_BUFFER3, + //! Auxiliary buffer 4 + ERT_AUX_BUFFER4 +}; + +//! Enum for the flags of clear buffer +enum E_CLEAR_BUFFER_FLAG +{ + ECBF_NONE = 0, + ECBF_COLOR = 1, + ECBF_DEPTH = 2, + ECBF_STENCIL = 4, + ECBF_ALL = ECBF_COLOR | ECBF_DEPTH | ECBF_STENCIL +}; + +//! Enum for the types of fog distributions to choose from +enum E_FOG_TYPE +{ + EFT_FOG_EXP = 0, + EFT_FOG_LINEAR, + EFT_FOG_EXP2 +}; + +} // irr::video + diff --git a/irr/include/IAnimatedMesh.h b/irr/include/IAnimatedMesh.h index 80b3bc3ca..2a1c1f4b1 100644 --- a/irr/include/IAnimatedMesh.h +++ b/irr/include/IAnimatedMesh.h @@ -19,11 +19,8 @@ irr::scene::SMeshBuffer etc. */ class IAnimatedMesh : public IMesh { public: - //! Gets the frame count of the animated mesh. - /** Note that the play-time is usually getFrameCount()-1 as it stops as soon as the last frame-key is reached. - \return The amount of frames. If the amount is 1, - it is a static, non animated mesh. */ - virtual u32 getFrameCount() const = 0; + //! Gets the maximum frame number, 0 if the mesh is static. + virtual f32 getMaxFrameNumber() const = 0; //! Gets the animation speed of the animated mesh. /** \return The number of frames per second to play the @@ -39,19 +36,10 @@ public: virtual void setAnimationSpeed(f32 fps) = 0; //! Returns the IMesh interface for a frame. - /** \param frame: Frame number as zero based index. The maximum - frame number is getFrameCount() - 1; - \param detailLevel: Level of detail. 0 is the lowest, 255 the - highest level of detail. Most meshes will ignore the detail level. - \param startFrameLoop: Because some animated meshes (.MD2) are - blended between 2 static frames, and maybe animated in a loop, - the startFrameLoop and the endFrameLoop have to be defined, to - prevent the animation to be blended between frames which are - outside of this loop. - If startFrameLoop and endFrameLoop are both -1, they are ignored. - \param endFrameLoop: see startFrameLoop. - \return Returns the animated mesh based on a detail level. */ - virtual IMesh *getMesh(s32 frame, s32 detailLevel = 255, s32 startFrameLoop = -1, s32 endFrameLoop = -1) = 0; + /** \param frame: Frame number, >= 0, <= getMaxFrameNumber() + Linear interpolation is used if this is between two frames. + \return Returns the animated mesh for the given frame */ + virtual IMesh *getMesh(f32 frame) = 0; //! Returns the type of the animated mesh. /** In most cases it is not necessary to use this method. diff --git a/irr/include/IAnimatedMeshSceneNode.h b/irr/include/IAnimatedMeshSceneNode.h index 65fdaaadf..8f9f6d661 100644 --- a/irr/include/IAnimatedMeshSceneNode.h +++ b/irr/include/IAnimatedMeshSceneNode.h @@ -63,7 +63,7 @@ public: virtual void setCurrentFrame(f32 frame) = 0; //! Sets the frame numbers between the animation is looped. - /** The default is 0 to getFrameCount()-1 of the mesh. + /** The default is 0 to getMaxFrameNumber() of the mesh. Number of played frames is end-start. It interpolates toward the last frame but stops when it is reached. It does not interpolate back to start even when looping. @@ -71,7 +71,7 @@ public: \param begin: Start frame number of the loop. \param end: End frame number of the loop. \return True if successful, false if not. */ - virtual bool setFrameLoop(s32 begin, s32 end) = 0; + virtual bool setFrameLoop(f32 begin, f32 end) = 0; //! Sets the speed with which the animation is played. /** \param framesPerSecond: Frames per second played. */ @@ -108,9 +108,9 @@ public: //! Returns the currently displayed frame number. virtual f32 getFrameNr() const = 0; //! Returns the current start frame number. - virtual s32 getStartFrame() const = 0; + virtual f32 getStartFrame() const = 0; //! Returns the current end frame number. - virtual s32 getEndFrame() const = 0; + virtual f32 getEndFrame() const = 0; //! Sets looping mode which is on by default. /** If set to false, animations will not be played looped. */ diff --git a/irr/include/IAttributes.h b/irr/include/IAttributes.h index 906d334a2..4606c5710 100644 --- a/irr/include/IAttributes.h +++ b/irr/include/IAttributes.h @@ -23,27 +23,13 @@ namespace io class IAttributes : public virtual IReferenceCounted { public: - //! Returns amount of attributes in this collection of attributes. - virtual u32 getAttributeCount() const = 0; - - //! Returns attribute name by index. - //! \param index: Index value, must be between 0 and getAttributeCount()-1. - virtual const c8 *getAttributeName(s32 index) const = 0; - //! Returns the type of an attribute //! \param attributeName: Name for the attribute virtual E_ATTRIBUTE_TYPE getAttributeType(const c8 *attributeName) const = 0; - //! Returns attribute type by index. - //! \param index: Index value, must be between 0 and getAttributeCount()-1. - virtual E_ATTRIBUTE_TYPE getAttributeType(s32 index) const = 0; - //! Returns if an attribute with a name exists virtual bool existsAttribute(const c8 *attributeName) const = 0; - //! Returns attribute index from name, -1 if not found - virtual s32 findAttribute(const c8 *attributeName) const = 0; - //! Removes all attributes virtual void clear() = 0; @@ -65,13 +51,6 @@ public: //! \return Returns value of the attribute previously set by setAttribute() virtual s32 getAttributeAsInt(const c8 *attributeName, irr::s32 defaultNotFound = 0) const = 0; - //! Gets an attribute as integer value - //! \param index: Index value, must be between 0 and getAttributeCount()-1. - virtual s32 getAttributeAsInt(s32 index) const = 0; - - //! Sets an attribute as integer value - virtual void setAttribute(s32 index, s32 value) = 0; - /* Float Attribute @@ -90,13 +69,6 @@ public: //! \return Returns value of the attribute previously set by setAttribute() virtual f32 getAttributeAsFloat(const c8 *attributeName, irr::f32 defaultNotFound = 0.f) const = 0; - //! Gets an attribute as float value - //! \param index: Index value, must be between 0 and getAttributeCount()-1. - virtual f32 getAttributeAsFloat(s32 index) const = 0; - - //! Sets an attribute as float value - virtual void setAttribute(s32 index, f32 value) = 0; - /* Bool Attribute */ @@ -112,13 +84,6 @@ public: //! \param defaultNotFound Value returned when attributeName was not found //! \return Returns value of the attribute previously set by setAttribute() virtual bool getAttributeAsBool(const c8 *attributeName, bool defaultNotFound = false) const = 0; - - //! Gets an attribute as boolean value - //! \param index: Index value, must be between 0 and getAttributeCount()-1. - virtual bool getAttributeAsBool(s32 index) const = 0; - - //! Sets an attribute as boolean value - virtual void setAttribute(s32 index, bool value) = 0; }; } // end namespace io diff --git a/irr/include/IEventReceiver.h b/irr/include/IEventReceiver.h index a484bfb84..7fb9e5f4e 100644 --- a/irr/include/IEventReceiver.h +++ b/irr/include/IEventReceiver.h @@ -347,6 +347,9 @@ struct SEvent //! Type of mouse event EMOUSE_INPUT_EVENT Event; + + //! Is this a simulated mouse event generated by Minetest itself? + bool Simulated; }; //! Any kind of keyboard event. @@ -538,6 +541,11 @@ struct SEvent struct SUserEvent UserEvent; struct SApplicationEvent ApplicationEvent; }; + + SEvent() { + // would be left uninitialized in many places otherwise + MouseInput.Simulated = false; + } }; //! Interface of an object which can receive events. diff --git a/irr/include/IFileSystem.h b/irr/include/IFileSystem.h index 1fe9fe6f2..f144bbaee 100644 --- a/irr/include/IFileSystem.h +++ b/irr/include/IFileSystem.h @@ -85,113 +85,6 @@ public: See IReferenceCounted::drop() for more information. */ virtual IWriteFile *createAndWriteFile(const path &filename, bool append = false) = 0; - //! Adds an archive to the file system. - /** After calling this, the Irrlicht Engine will also search and open - files directly from this archive. This is useful for hiding data from - the end user, speeding up file access and making it possible to access - for example Quake3 .pk3 files, which are just renamed .zip files. By - default Irrlicht supports ZIP, PAK, TAR, PNK, and directories as - archives. You can provide your own archive types by implementing - IArchiveLoader and passing an instance to addArchiveLoader. - Irrlicht supports AES-encrypted zip files, and the advanced compression - techniques lzma and bzip2. - \param filename: Filename of the archive to add to the file system. - \param ignoreCase: If set to true, files in the archive can be accessed without - writing all letters in the right case. - \param ignorePaths: If set to true, files in the added archive can be accessed - without its complete path. - \param archiveType: If no specific E_FILE_ARCHIVE_TYPE is selected then - the type of archive will depend on the extension of the file name. If - you use a different extension then you can use this parameter to force - a specific type of archive. - \param password An optional password, which is used in case of encrypted archives. - \param retArchive A pointer that will be set to the archive that is added. - \return True if the archive was added successfully, false if not. */ - virtual bool addFileArchive(const path &filename, bool ignoreCase = true, - bool ignorePaths = true, - E_FILE_ARCHIVE_TYPE archiveType = EFAT_UNKNOWN, - const core::stringc &password = "", - IFileArchive **retArchive = 0) = 0; - - //! Adds an archive to the file system. - /** After calling this, the Irrlicht Engine will also search and open - files directly from this archive. This is useful for hiding data from - the end user, speeding up file access and making it possible to access - for example Quake3 .pk3 files, which are just renamed .zip files. By - default Irrlicht supports ZIP, PAK, TAR, PNK, and directories as - archives. You can provide your own archive types by implementing - IArchiveLoader and passing an instance to addArchiveLoader. - Irrlicht supports AES-encrypted zip files, and the advanced compression - techniques lzma and bzip2. - If you want to add a directory as an archive, prefix its name with a - slash in order to let Irrlicht recognize it as a folder mount (mypath/). - Using this technique one can build up a search order, because archives - are read first, and can be used more easily with relative filenames. - \param file: Archive to add to the file system. - \param ignoreCase: If set to true, files in the archive can be accessed without - writing all letters in the right case. - \param ignorePaths: If set to true, files in the added archive can be accessed - without its complete path. - \param archiveType: If no specific E_FILE_ARCHIVE_TYPE is selected then - the type of archive will depend on the extension of the file name. If - you use a different extension then you can use this parameter to force - a specific type of archive. - \param password An optional password, which is used in case of encrypted archives. - \param retArchive A pointer that will be set to the archive that is added. - \return True if the archive was added successfully, false if not. */ - virtual bool addFileArchive(IReadFile *file, bool ignoreCase = true, - bool ignorePaths = true, - E_FILE_ARCHIVE_TYPE archiveType = EFAT_UNKNOWN, - const core::stringc &password = "", - IFileArchive **retArchive = 0) = 0; - - //! Adds an archive to the file system. - /** \param archive: The archive to add to the file system. - \return True if the archive was added successfully, false if not. */ - virtual bool addFileArchive(IFileArchive *archive) = 0; - - //! Get the number of archives currently attached to the file system - virtual u32 getFileArchiveCount() const = 0; - - //! Removes an archive from the file system. - /** This will close the archive and free any file handles, but will not - close resources which have already been loaded and are now cached, for - example textures and meshes. - \param index: The index of the archive to remove - \return True on success, false on failure */ - virtual bool removeFileArchive(u32 index) = 0; - - //! Removes an archive from the file system. - /** This will close the archive and free any file handles, but will not - close resources which have already been loaded and are now cached, for - example textures and meshes. Note that a relative filename might be - interpreted differently on each call, depending on the current working - directory. In case you want to remove an archive that was added using - a relative path name, you have to change to the same working directory - again. This means, that the filename given on creation is not an - identifier for the archive, but just a usual filename that is used for - locating the archive to work with. - \param filename The archive pointed to by the name will be removed - \return True on success, false on failure */ - virtual bool removeFileArchive(const path &filename) = 0; - - //! Removes an archive from the file system. - /** This will close the archive and free any file handles, but will not - close resources which have already been loaded and are now cached, for - example textures and meshes. - \param archive The archive to remove. - \return True on success, false on failure */ - virtual bool removeFileArchive(const IFileArchive *archive) = 0; - - //! Changes the search order of attached archives. - /** - \param sourceIndex: The index of the archive to change - \param relative: The relative change in position, archives with a lower index are searched first */ - virtual bool moveFileArchive(u32 sourceIndex, s32 relative) = 0; - - //! Get the archive at a given index. - virtual IFileArchive *getFileArchive(u32 index) = 0; - //! Adds an external archive loader to the engine. /** Use this function to add support for new archive types to the engine, for example proprietary or encrypted file storage. */ diff --git a/irr/include/IGUISkin.h b/irr/include/IGUISkin.h index 36b510606..b323983ae 100644 --- a/irr/include/IGUISkin.h +++ b/irr/include/IGUISkin.h @@ -437,6 +437,10 @@ public: virtual void draw3DButtonPaneStandard(IGUIElement *element, const core::rect &rect, const core::rect *clip = 0) = 0; + virtual void drawColored3DButtonPaneStandard(IGUIElement* element, + const core::rect& rect, + const core::rect* clip=0, + const video::SColor* colors=0) = 0; //! draws a pressed 3d button pane /** Used for drawing for example buttons in pressed state. @@ -450,6 +454,10 @@ public: virtual void draw3DButtonPanePressed(IGUIElement *element, const core::rect &rect, const core::rect *clip = 0) = 0; + virtual void drawColored3DButtonPanePressed(IGUIElement* element, + const core::rect& rect, + const core::rect* clip=0, + const video::SColor* colors=0) = 0; //! draws a sunken 3d pane /** Used for drawing the background of edit, combo or check boxes. diff --git a/irr/include/IIndexBuffer.h b/irr/include/IIndexBuffer.h index 3d5b8e76a..01282f0c8 100644 --- a/irr/include/IIndexBuffer.h +++ b/irr/include/IIndexBuffer.h @@ -7,45 +7,39 @@ #include "IReferenceCounted.h" #include "irrArray.h" #include "EHardwareBufferFlags.h" +#include "EPrimitiveTypes.h" #include "SVertexIndex.h" namespace irr { -namespace video -{ - -} - namespace scene { class IIndexBuffer : public virtual IReferenceCounted { public: + //! Get type of index data which is stored in this meshbuffer. + /** \return Index type of this buffer. */ + virtual video::E_INDEX_TYPE getType() const = 0; + + //! Get access to indices. + /** \return Pointer to indices array. */ + virtual const void *getData() const = 0; + + //! Get access to indices. + /** \return Pointer to indices array. */ virtual void *getData() = 0; - virtual video::E_INDEX_TYPE getType() const = 0; - virtual void setType(video::E_INDEX_TYPE IndexType) = 0; - - virtual u32 stride() const = 0; - - virtual u32 size() const = 0; - virtual void push_back(const u32 &element) = 0; - virtual u32 operator[](u32 index) const = 0; - virtual u32 getLast() = 0; - virtual void setValue(u32 index, u32 value) = 0; - virtual void set_used(u32 usedNow) = 0; - virtual void reallocate(u32 new_size) = 0; - virtual u32 allocated_size() const = 0; - - virtual void *pointer() = 0; + //! Get amount of indices in this meshbuffer. + /** \return Number of indices in this buffer. */ + virtual u32 getCount() const = 0; //! get the current hardware mapping hint virtual E_HARDWARE_MAPPING getHardwareMappingHint() const = 0; //! set the hardware mapping hint, for driver - virtual void setHardwareMappingHint(E_HARDWARE_MAPPING NewMappingHint) = 0; + virtual void setHardwareMappingHint(E_HARDWARE_MAPPING newMappingHint) = 0; //! flags the meshbuffer as changed, reloads hardware buffers virtual void setDirty() = 0; @@ -53,6 +47,35 @@ public: //! Get the currently used ID for identification of changes. /** This shouldn't be used for anything outside the VideoDriver. */ virtual u32 getChangedID() const = 0; + + //! Used by the VideoDriver to remember the buffer link. + virtual void setHWBuffer(void *ptr) const = 0; + virtual void *getHWBuffer() const = 0; + + //! Calculate how many geometric primitives would be drawn + u32 getPrimitiveCount(E_PRIMITIVE_TYPE primitiveType) const + { + const u32 indexCount = getCount(); + switch (primitiveType) { + case scene::EPT_POINTS: + return indexCount; + case scene::EPT_LINE_STRIP: + return indexCount - 1; + case scene::EPT_LINE_LOOP: + return indexCount; + case scene::EPT_LINES: + return indexCount / 2; + case scene::EPT_TRIANGLE_STRIP: + return (indexCount - 2); + case scene::EPT_TRIANGLE_FAN: + return (indexCount - 2); + case scene::EPT_TRIANGLES: + return indexCount / 3; + case scene::EPT_POINT_SPRITES: + return indexCount; + } + return 0; + } }; } // end namespace scene diff --git a/irr/include/IMesh.h b/irr/include/IMesh.h index 6d06eb762..8ee180d5d 100644 --- a/irr/include/IMesh.h +++ b/irr/include/IMesh.h @@ -86,6 +86,17 @@ public: mesh buffer. */ virtual IMeshBuffer *getMeshBuffer(const video::SMaterial &material) const = 0; + //! Minetest binds textures (node tiles, object textures) to models. + // glTF allows multiple primitives (mesh buffers) to reference the same texture. + // This is reflected here: This function gets the texture slot for a mesh buffer. + /** \param meshbufNr: Zero based index of the mesh buffer. The maximum value is + getMeshBufferCount() - 1; + \return number of texture slot to bind to the given mesh buffer */ + virtual u32 getTextureSlot(u32 meshbufNr) const + { + return meshbufNr; + } + //! Get an axis aligned bounding box of the mesh. /** \return Bounding box of this mesh. */ virtual const core::aabbox3d &getBoundingBox() const = 0; diff --git a/irr/include/IMeshBuffer.h b/irr/include/IMeshBuffer.h index c69a12d1d..55c05211a 100644 --- a/irr/include/IMeshBuffer.h +++ b/irr/include/IMeshBuffer.h @@ -7,8 +7,8 @@ #include "IReferenceCounted.h" #include "SMaterial.h" #include "aabbox3d.h" -#include "S3DVertex.h" -#include "SVertexIndex.h" +#include "IVertexBuffer.h" +#include "IIndexBuffer.h" #include "EHardwareBufferFlags.h" #include "EPrimitiveTypes.h" @@ -46,39 +46,17 @@ public: /** \return Material of this buffer. */ virtual const video::SMaterial &getMaterial() const = 0; - //! Get type of vertex data which is stored in this meshbuffer. - /** \return Vertex type of this buffer. */ - virtual video::E_VERTEX_TYPE getVertexType() const = 0; + /// Get the vertex buffer + virtual const scene::IVertexBuffer *getVertexBuffer() const = 0; - //! Get access to vertex data. The data is an array of vertices. - /** Which vertex type is used can be determined by getVertexType(). - \return Pointer to array of vertices. */ - virtual const void *getVertices() const = 0; + /// Get the vertex buffer + virtual scene::IVertexBuffer *getVertexBuffer() = 0; - //! Get access to vertex data. The data is an array of vertices. - /** Which vertex type is used can be determined by getVertexType(). - \return Pointer to array of vertices. */ - virtual void *getVertices() = 0; + /// Get the index buffer + virtual const scene::IIndexBuffer *getIndexBuffer() const = 0; - //! Get amount of vertices in meshbuffer. - /** \return Number of vertices in this buffer. */ - virtual u32 getVertexCount() const = 0; - - //! Get type of index data which is stored in this meshbuffer. - /** \return Index type of this buffer. */ - virtual video::E_INDEX_TYPE getIndexType() const = 0; - - //! Get access to indices. - /** \return Pointer to indices array. */ - virtual const u16 *getIndices() const = 0; - - //! Get access to indices. - /** \return Pointer to indices array. */ - virtual u16 *getIndices() = 0; - - //! Get amount of indices in this meshbuffer. - /** \return Number of indices in this buffer. */ - virtual u32 getIndexCount() const = 0; + /// Get the index buffer + virtual scene::IIndexBuffer *getIndexBuffer() = 0; //! Get the axis aligned bounding box of this meshbuffer. /** \return Axis aligned bounding box of this buffer. */ @@ -92,24 +70,6 @@ public: //! Recalculates the bounding box. Should be called if the mesh changed. virtual void recalculateBoundingBox() = 0; - //! returns position of vertex i - virtual const core::vector3df &getPosition(u32 i) const = 0; - - //! returns position of vertex i - virtual core::vector3df &getPosition(u32 i) = 0; - - //! returns normal of vertex i - virtual const core::vector3df &getNormal(u32 i) const = 0; - - //! returns normal of vertex i - virtual core::vector3df &getNormal(u32 i) = 0; - - //! returns texture coord of vertex i - virtual const core::vector2df &getTCoords(u32 i) const = 0; - - //! returns texture coord of vertex i - virtual core::vector2df &getTCoords(u32 i) = 0; - //! Append the vertices and indices to the current buffer /** Only works for compatible vertex types. \param vertices Pointer to a vertex array. @@ -118,29 +78,123 @@ public: \param numIndices Number of indices in array. */ virtual void append(const void *const vertices, u32 numVertices, const u16 *const indices, u32 numIndices) = 0; - //! get the current hardware mapping hint - virtual E_HARDWARE_MAPPING getHardwareMappingHint_Vertex() const = 0; + /* Leftover functions that are now just helpers for accessing the respective buffer. */ - //! get the current hardware mapping hint - virtual E_HARDWARE_MAPPING getHardwareMappingHint_Index() const = 0; + //! Get type of vertex data which is stored in this meshbuffer. + /** \return Vertex type of this buffer. */ + inline video::E_VERTEX_TYPE getVertexType() const + { + return getVertexBuffer()->getType(); + } + + //! Get access to vertex data. The data is an array of vertices. + /** Which vertex type is used can be determined by getVertexType(). + \return Pointer to array of vertices. */ + inline const void *getVertices() const + { + return getVertexBuffer()->getData(); + } + + //! Get access to vertex data. The data is an array of vertices. + /** Which vertex type is used can be determined by getVertexType(). + \return Pointer to array of vertices. */ + inline void *getVertices() + { + return getVertexBuffer()->getData(); + } + + //! Get amount of vertices in meshbuffer. + /** \return Number of vertices in this buffer. */ + inline u32 getVertexCount() const + { + return getVertexBuffer()->getCount(); + } + + //! Get type of index data which is stored in this meshbuffer. + /** \return Index type of this buffer. */ + inline video::E_INDEX_TYPE getIndexType() const + { + return getIndexBuffer()->getType(); + } + + //! Get access to indices. + /** \return Pointer to indices array. */ + inline const u16 *getIndices() const + { + _IRR_DEBUG_BREAK_IF(getIndexBuffer()->getType() != video::EIT_16BIT); + return static_cast(getIndexBuffer()->getData()); + } + + //! Get access to indices. + /** \return Pointer to indices array. */ + inline u16 *getIndices() + { + _IRR_DEBUG_BREAK_IF(getIndexBuffer()->getType() != video::EIT_16BIT); + return static_cast(getIndexBuffer()->getData()); + } + + //! Get amount of indices in this meshbuffer. + /** \return Number of indices in this buffer. */ + inline u32 getIndexCount() const + { + return getIndexBuffer()->getCount(); + } + + //! returns position of vertex i + inline const core::vector3df &getPosition(u32 i) const + { + return getVertexBuffer()->getPosition(i); + } + + //! returns position of vertex i + inline core::vector3df &getPosition(u32 i) + { + return getVertexBuffer()->getPosition(i); + } + + //! returns normal of vertex i + inline const core::vector3df &getNormal(u32 i) const + { + return getVertexBuffer()->getNormal(i); + } + + //! returns normal of vertex i + inline core::vector3df &getNormal(u32 i) + { + return getVertexBuffer()->getNormal(i); + } + + //! returns texture coord of vertex i + inline const core::vector2df &getTCoords(u32 i) const + { + return getVertexBuffer()->getTCoords(i); + } + + //! returns texture coord of vertex i + inline core::vector2df &getTCoords(u32 i) + { + return getVertexBuffer()->getTCoords(i); + } //! set the hardware mapping hint, for driver - virtual void setHardwareMappingHint(E_HARDWARE_MAPPING newMappingHint, E_BUFFER_TYPE buffer = EBT_VERTEX_AND_INDEX) = 0; + inline void setHardwareMappingHint(E_HARDWARE_MAPPING newMappingHint, E_BUFFER_TYPE buffer = EBT_VERTEX_AND_INDEX) + { + if (buffer == EBT_VERTEX_AND_INDEX || buffer == EBT_VERTEX) + getVertexBuffer()->setHardwareMappingHint(newMappingHint); + if (buffer == EBT_VERTEX_AND_INDEX || buffer == EBT_INDEX) + getIndexBuffer()->setHardwareMappingHint(newMappingHint); + } //! flags the meshbuffer as changed, reloads hardware buffers - virtual void setDirty(E_BUFFER_TYPE buffer = EBT_VERTEX_AND_INDEX) = 0; + inline void setDirty(E_BUFFER_TYPE buffer = EBT_VERTEX_AND_INDEX) + { + if (buffer == EBT_VERTEX_AND_INDEX || buffer == EBT_VERTEX) + getVertexBuffer()->setDirty(); + if (buffer == EBT_VERTEX_AND_INDEX || buffer == EBT_INDEX) + getIndexBuffer()->setDirty(); + } - //! Get the currently used ID for identification of changes. - /** This shouldn't be used for anything outside the VideoDriver. */ - virtual u32 getChangedID_Vertex() const = 0; - - //! Get the currently used ID for identification of changes. - /** This shouldn't be used for anything outside the VideoDriver. */ - virtual u32 getChangedID_Index() const = 0; - - //! Used by the VideoDriver to remember the buffer link. - virtual void setHWBuffer(void *ptr) const = 0; - virtual void *getHWBuffer() const = 0; + /* End helpers */ //! Describe what kind of primitive geometry is used by the meshbuffer /** Note: Default is EPT_TRIANGLES. Using other types is fine for rendering. @@ -153,32 +207,13 @@ public: virtual E_PRIMITIVE_TYPE getPrimitiveType() const = 0; //! Calculate how many geometric primitives are used by this meshbuffer - virtual u32 getPrimitiveCount() const + u32 getPrimitiveCount() const { - const u32 indexCount = getIndexCount(); - switch (getPrimitiveType()) { - case scene::EPT_POINTS: - return indexCount; - case scene::EPT_LINE_STRIP: - return indexCount - 1; - case scene::EPT_LINE_LOOP: - return indexCount; - case scene::EPT_LINES: - return indexCount / 2; - case scene::EPT_TRIANGLE_STRIP: - return (indexCount - 2); - case scene::EPT_TRIANGLE_FAN: - return (indexCount - 2); - case scene::EPT_TRIANGLES: - return indexCount / 3; - case scene::EPT_POINT_SPRITES: - return indexCount; - } - return 0; + return getIndexBuffer()->getPrimitiveCount(getPrimitiveType()); } //! Calculate size of vertices and indices in memory - virtual size_t getSize() const + size_t getSize() const { size_t ret = 0; switch (getVertexType()) { diff --git a/irr/include/IReferenceCounted.h b/irr/include/IReferenceCounted.h index 6f85bb904..68aa20fb6 100644 --- a/irr/include/IReferenceCounted.h +++ b/irr/include/IReferenceCounted.h @@ -42,7 +42,7 @@ class IReferenceCounted public: //! Constructor. IReferenceCounted() : - DebugName(0), ReferenceCounter(1) + ReferenceCounter(1) { } @@ -51,6 +51,10 @@ public: { } + // Reference counted objects can be neither copied nor moved. + IReferenceCounted(const IReferenceCounted &) = delete; + IReferenceCounted &operator=(const IReferenceCounted &) = delete; + //! Grabs the object. Increments the reference counter by one. /** Someone who calls grab() to an object, should later also call drop() to it. If an object never gets as much drop() as @@ -132,6 +136,7 @@ public: return ReferenceCounter; } +#ifdef _DEBUG //! Returns the debug name of the object. /** The Debugname may only be set and changed by the object itself. This method should only be used in Debug mode. @@ -153,7 +158,10 @@ protected: private: //! The debug name. - const c8 *DebugName; + const c8 *DebugName = nullptr; +#endif + +private: //! The reference counter. Mutable to do reference counting on const objects. mutable s32 ReferenceCounter; diff --git a/irr/include/ISceneNode.h b/irr/include/ISceneNode.h index 7a03a2256..1eab3a3fd 100644 --- a/irr/include/ISceneNode.h +++ b/irr/include/ISceneNode.h @@ -10,6 +10,7 @@ #include "EDebugSceneTypes.h" #include "SMaterial.h" #include "irrString.h" +#include "irrArray.h" #include "aabbox3d.h" #include "matrix4.h" #include "IAttributes.h" diff --git a/irr/include/ISkinnedMesh.h b/irr/include/ISkinnedMesh.h index 9cc7469cb..869327bcd 100644 --- a/irr/include/ISkinnedMesh.h +++ b/irr/include/ISkinnedMesh.h @@ -159,15 +159,17 @@ public: core::array Weights; //! Unnecessary for loaders, will be overwritten on finalize - core::matrix4 GlobalMatrix; + core::matrix4 GlobalMatrix; // loaders may still choose to set this (temporarily) to calculate absolute vertex data. core::matrix4 GlobalAnimatedMatrix; core::matrix4 LocalAnimatedMatrix; + + //! These should be set by loaders. core::vector3df Animatedposition; core::vector3df Animatedscale; core::quaternion Animatedrotation; - core::matrix4 GlobalInversedMatrix; // the x format pre-calculates this - + // The .x and .gltf formats pre-calculate this + std::optional GlobalInversedMatrix; private: //! Internal members used by CSkinnedMesh friend class CSkinnedMesh; @@ -199,6 +201,9 @@ public: //! Adds a new meshbuffer to the mesh, access it as last one virtual SSkinMeshBuffer *addMeshBuffer() = 0; + //! Adds a new meshbuffer to the mesh, access it as last one + virtual void addMeshBuffer(SSkinMeshBuffer *meshbuf) = 0; + //! Adds a new joint to the mesh, access it as last one virtual SJoint *addJoint(SJoint *parent = 0) = 0; diff --git a/irr/include/IVertexBuffer.h b/irr/include/IVertexBuffer.h index a4ec5efd9..e5be83904 100644 --- a/irr/include/IVertexBuffer.h +++ b/irr/include/IVertexBuffer.h @@ -17,24 +17,47 @@ namespace scene class IVertexBuffer : public virtual IReferenceCounted { public: - virtual void *getData() = 0; + //! Get type of vertex data which is stored in this meshbuffer. + /** \return Vertex type of this buffer. */ virtual video::E_VERTEX_TYPE getType() const = 0; - virtual void setType(video::E_VERTEX_TYPE vertexType) = 0; - virtual u32 stride() const = 0; - virtual u32 size() const = 0; - virtual void push_back(const video::S3DVertex &element) = 0; - virtual video::S3DVertex &operator[](const u32 index) const = 0; - virtual video::S3DVertex &getLast() = 0; - virtual void set_used(u32 usedNow) = 0; - virtual void reallocate(u32 new_size) = 0; - virtual u32 allocated_size() const = 0; - virtual video::S3DVertex *pointer() = 0; + + //! Get access to vertex data. The data is an array of vertices. + /** Which vertex type is used can be determined by getVertexType(). + \return Pointer to array of vertices. */ + virtual const void *getData() const = 0; + + //! Get access to vertex data. The data is an array of vertices. + /** Which vertex type is used can be determined by getVertexType(). + \return Pointer to array of vertices. */ + virtual void *getData() = 0; + + //! Get amount of vertices in meshbuffer. + /** \return Number of vertices in this buffer. */ + virtual u32 getCount() const = 0; + + //! returns position of vertex i + virtual const core::vector3df &getPosition(u32 i) const = 0; + + //! returns position of vertex i + virtual core::vector3df &getPosition(u32 i) = 0; + + //! returns normal of vertex i + virtual const core::vector3df &getNormal(u32 i) const = 0; + + //! returns normal of vertex i + virtual core::vector3df &getNormal(u32 i) = 0; + + //! returns texture coord of vertex i + virtual const core::vector2df &getTCoords(u32 i) const = 0; + + //! returns texture coord of vertex i + virtual core::vector2df &getTCoords(u32 i) = 0; //! get the current hardware mapping hint virtual E_HARDWARE_MAPPING getHardwareMappingHint() const = 0; //! set the hardware mapping hint, for driver - virtual void setHardwareMappingHint(E_HARDWARE_MAPPING NewMappingHint) = 0; + virtual void setHardwareMappingHint(E_HARDWARE_MAPPING newMappingHint) = 0; //! flags the meshbuffer as changed, reloads hardware buffers virtual void setDirty() = 0; @@ -42,6 +65,10 @@ public: //! Get the currently used ID for identification of changes. /** This shouldn't be used for anything outside the VideoDriver. */ virtual u32 getChangedID() const = 0; + + //! Used by the VideoDriver to remember the buffer link. + virtual void setHWBuffer(void *ptr) const = 0; + virtual void *getHWBuffer() const = 0; }; } // end namespace scene diff --git a/irr/include/IVideoDriver.h b/irr/include/IVideoDriver.h index 2d651c6bf..af8d97fef 100644 --- a/irr/include/IVideoDriver.h +++ b/irr/include/IVideoDriver.h @@ -9,14 +9,16 @@ #include "ITexture.h" #include "irrArray.h" #include "matrix4.h" -#include "plane3d.h" #include "dimension2d.h" #include "position2d.h" -#include "IMeshBuffer.h" #include "EDriverTypes.h" #include "EDriverFeatures.h" +#include "EPrimitiveTypes.h" +#include "EVideoTypes.h" #include "SExposedVideoData.h" #include "SOverrideMaterial.h" +#include "S3DVertex.h" // E_VERTEX_TYPE +#include "SVertexIndex.h" // E_INDEX_TYPE namespace irr { @@ -29,6 +31,8 @@ class IWriteFile; namespace scene { class IMeshBuffer; +class IVertexBuffer; +class IIndexBuffer; class IMesh; class IMeshManipulator; class ISceneNode; @@ -36,77 +40,12 @@ class ISceneNode; namespace video { -struct S3DVertex; -struct S3DVertex2TCoords; -struct S3DVertexTangents; class IImageLoader; class IImageWriter; class IMaterialRenderer; class IGPUProgrammingServices; class IRenderTarget; -//! enumeration for geometry transformation states -enum E_TRANSFORMATION_STATE -{ - //! View transformation - ETS_VIEW = 0, - //! World transformation - ETS_WORLD, - //! Projection transformation - ETS_PROJECTION, - //! Texture 0 transformation - //! Use E_TRANSFORMATION_STATE(ETS_TEXTURE_0 + texture_number) to access other texture transformations - ETS_TEXTURE_0, - //! Only used internally - ETS_COUNT = ETS_TEXTURE_0 + MATERIAL_MAX_TEXTURES -}; - -//! Special render targets, which usually map to dedicated hardware -/** These render targets (besides 0 and 1) need not be supported by gfx cards */ -enum E_RENDER_TARGET -{ - //! Render target is the main color frame buffer - ERT_FRAME_BUFFER = 0, - //! Render target is a render texture - ERT_RENDER_TEXTURE, - //! Multi-Render target textures - ERT_MULTI_RENDER_TEXTURES, - //! Render target is the main color frame buffer - ERT_STEREO_LEFT_BUFFER, - //! Render target is the right color buffer (left is the main buffer) - ERT_STEREO_RIGHT_BUFFER, - //! Render to both stereo buffers at once - ERT_STEREO_BOTH_BUFFERS, - //! Auxiliary buffer 0 - ERT_AUX_BUFFER0, - //! Auxiliary buffer 1 - ERT_AUX_BUFFER1, - //! Auxiliary buffer 2 - ERT_AUX_BUFFER2, - //! Auxiliary buffer 3 - ERT_AUX_BUFFER3, - //! Auxiliary buffer 4 - ERT_AUX_BUFFER4 -}; - -//! Enum for the flags of clear buffer -enum E_CLEAR_BUFFER_FLAG -{ - ECBF_NONE = 0, - ECBF_COLOR = 1, - ECBF_DEPTH = 2, - ECBF_STENCIL = 4, - ECBF_ALL = ECBF_COLOR | ECBF_DEPTH | ECBF_STENCIL -}; - -//! Enum for the types of fog distributions to choose from -enum E_FOG_TYPE -{ - EFT_FOG_EXP = 0, - EFT_FOG_LINEAR, - EFT_FOG_EXP2 -}; - const c8 *const FogTypeNames[] = { "FogExp", "FogLinear", @@ -114,6 +53,17 @@ const c8 *const FogTypeNames[] = { 0, }; +struct SFrameStats { + //! Number of draw calls + u32 Drawcalls = 0; + //! Count of primitives drawn + u32 PrimitivesDrawn = 0; + //! Number of hardware buffers uploaded (new or updated) + u32 HWBuffersUploaded = 0; + //! Sum of uploaded hardware buffer size + u32 HWBuffersUploadedSize = 0; +}; + //! Interface to driver which is able to perform 2d and 3d graphics functions. /** This interface is one of the most important interfaces of the Irrlicht Engine: All rendering and texture manipulation is done with @@ -182,7 +132,6 @@ public: MaxSupportedTextures (int) The maximum number of simultaneous textures supported by the fixed function pipeline of the (hw) driver. The actual supported number of textures supported by the engine can be lower. MaxLights (int) Number of hardware lights supported in the fixed function pipeline of the driver, typically 6-8. Use light manager or deferred shading for more. MaxAnisotropy (int) Number of anisotropy levels supported for filtering. At least 1, max is typically at 16 or 32. - MaxUserClipPlanes (int) Number of additional clip planes, which can be set by the user via dedicated driver methods. MaxAuxBuffers (int) Special render buffers, which are currently not really usable inside Irrlicht. Only supported by OpenGL MaxMultipleRenderTargets (int) Number of render targets which can be bound simultaneously. Rendering to MRTs is done via shaders. MaxIndices (int) Number of indices which can be used in one render call (i.e. one mesh buffer). @@ -195,12 +144,6 @@ public: */ virtual const io::IAttributes &getDriverAttributes() const = 0; - //! Check if the driver was recently reset. - /** For d3d devices you will need to recreate the RTTs if the - driver was reset. Should be queried right after beginScene(). - */ - virtual bool checkDriverReset() = 0; - //! Sets transformation matrices. /** \param state Transformation type to be set, e.g. view, world, or projection. @@ -360,7 +303,10 @@ public: virtual void removeAllTextures() = 0; //! Remove hardware buffer - virtual void removeHardwareBuffer(const scene::IMeshBuffer *mb) = 0; + virtual void removeHardwareBuffer(const scene::IVertexBuffer *vb) = 0; + + //! Remove hardware buffer + virtual void removeHardwareBuffer(const scene::IIndexBuffer *ib) = 0; //! Remove all hardware buffers virtual void removeAllHardwareBuffers() = 0; @@ -799,6 +745,17 @@ public: /** \param mb Buffer to draw */ virtual void drawMeshBuffer(const scene::IMeshBuffer *mb) = 0; + /** + * Draws a mesh from individual vertex and index buffers. + * @param vb vertices to use + * @param ib indices to use + * @param primCount amount of primitives + * @param pType primitive type + */ + virtual void drawBuffers(const scene::IVertexBuffer *vb, + const scene::IIndexBuffer *ib, u32 primCount, + scene::E_PRIMITIVE_TYPE pType = scene::EPT_TRIANGLES) = 0; + //! Draws normals of a mesh buffer /** \param mb Buffer to draw the normals of \param length length scale factor of the normals @@ -856,12 +813,8 @@ public: \return Approximate amount of frames per second drawn. */ virtual s32 getFPS() const = 0; - //! Returns amount of primitives (mostly triangles) which were drawn in the last frame. - /** Together with getFPS() very useful method for statistics. - \param mode Defines if the primitives drawn are accumulated or - counted per frame. - \return Amount of primitives drawn in the last frame. */ - virtual u32 getPrimitiveCountDrawn(u32 mode = 0) const = 0; + //! Return some statistics about the last frame + virtual SFrameStats getFrameStats() const = 0; //! Gets name of this video driver. /** \return Returns the name of the video driver, e.g. in case @@ -1109,26 +1062,6 @@ public: \return Pointer to loaded texture, or 0 if not found. */ virtual video::ITexture *findTexture(const io::path &filename) = 0; - //! Set or unset a clipping plane. - /** There are at least 6 clipping planes available for the user - to set at will. - \param index The plane index. Must be between 0 and - MaxUserClipPlanes. - \param plane The plane itself. - \param enable If true, enable the clipping plane else disable - it. - \return True if the clipping plane is usable. */ - virtual bool setClipPlane(u32 index, const core::plane3df &plane, bool enable = false) = 0; - - //! Enable or disable a clipping plane. - /** There are at least 6 clipping planes available for the user - to set at will. - \param index The plane index. Must be between 0 and - MaxUserClipPlanes. - \param enable If true, enable the clipping plane else disable - it. */ - virtual void enableClipPlane(u32 index, bool enable) = 0; - //! Set the minimum number of vertices for which a hw buffer will be created /** \param count Number of vertices to set as minimum. */ virtual void setMinHardwareBufferVertexCount(u32 count) = 0; diff --git a/irr/include/IrrlichtDevice.h b/irr/include/IrrlichtDevice.h index 11619010c..777a23420 100644 --- a/irr/include/IrrlichtDevice.h +++ b/irr/include/IrrlichtDevice.h @@ -6,14 +6,16 @@ #include "IReferenceCounted.h" #include "dimension2d.h" -#include "IVideoDriver.h" #include "EDriverTypes.h" #include "EDeviceTypes.h" #include "IEventReceiver.h" #include "ICursorControl.h" #include "ITimer.h" #include "IOSOperator.h" +#include "irrArray.h" #include "IrrCompileConfig.h" +#include "position2d.h" +#include "SColor.h" // video::ECOLOR_FORMAT namespace irr { @@ -38,6 +40,9 @@ class ISceneManager; namespace video { class IContextManager; +class IImage; +class ITexture; +class IVideoDriver; extern "C" IRRLICHT_API bool IRRCALLCONV isDriverSupported(E_DRIVER_TYPE driver); } // end namespace video diff --git a/irr/src/KHR/khrplatform.h b/irr/include/KHR/khrplatform.h similarity index 100% rename from irr/src/KHR/khrplatform.h rename to irr/include/KHR/khrplatform.h diff --git a/irr/include/SAnimatedMesh.h b/irr/include/SAnimatedMesh.h index 8fdaae0ee..dd7306633 100644 --- a/irr/include/SAnimatedMesh.h +++ b/irr/include/SAnimatedMesh.h @@ -4,10 +4,10 @@ #pragma once +#include #include "IAnimatedMesh.h" #include "IMesh.h" #include "aabbox3d.h" -#include "irrArray.h" namespace irr { @@ -15,7 +15,7 @@ namespace scene { //! Simple implementation of the IAnimatedMesh interface. -struct SAnimatedMesh : public IAnimatedMesh +struct SAnimatedMesh final : public IAnimatedMesh { //! constructor SAnimatedMesh(scene::IMesh *mesh = 0, scene::E_ANIMATED_MESH_TYPE type = scene::EAMT_UNKNOWN) : @@ -32,15 +32,13 @@ struct SAnimatedMesh : public IAnimatedMesh virtual ~SAnimatedMesh() { // drop meshes - for (u32 i = 0; i < Meshes.size(); ++i) - Meshes[i]->drop(); + for (auto *mesh : Meshes) + 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 Meshes.size(); + return static_cast(Meshes.size() - 1); } //! Gets the default animation speed of the animated mesh. @@ -59,19 +57,14 @@ struct SAnimatedMesh : public IAnimatedMesh } //! Returns the IMesh interface for a frame. - /** \param frame: Frame number as zero based index. The maximum frame number is - getFrameCount() - 1; - \param detailLevel: Level of detail. 0 is the lowest, - 255 the highest level of detail. Most meshes will ignore the detail level. - \param startFrameLoop: start frame - \param endFrameLoop: end frame - \return The animated mesh based on a detail level. */ - IMesh *getMesh(s32 frame, s32 detailLevel = 255, s32 startFrameLoop = -1, s32 endFrameLoop = -1) override + /** \param frame: Frame number as zero based index. + \return The animated mesh based for the given frame */ + IMesh *getMesh(f32 frame) override { if (Meshes.empty()) - return 0; + return nullptr; - return Meshes[frame]; + return Meshes[static_cast(frame)]; } //! adds a Mesh @@ -161,7 +154,7 @@ struct SAnimatedMesh : public IAnimatedMesh } //! All meshes defining the animated mesh - core::array Meshes; + std::vector Meshes; //! The bounding box of this mesh core::aabbox3d Box; diff --git a/irr/include/SMaterial.h b/irr/include/SMaterial.h index c803f5fde..8c0a51dfd 100644 --- a/irr/include/SMaterial.h +++ b/irr/include/SMaterial.h @@ -6,7 +6,6 @@ #include "SColor.h" #include "matrix4.h" -#include "irrArray.h" #include "irrMath.h" #include "EMaterialTypes.h" #include "EMaterialProps.h" @@ -195,29 +194,6 @@ enum E_ANTI_ALIASING_MODE EAAM_ALPHA_TO_COVERAGE = 4 }; -//! These flags allow to define the interpretation of vertex color when lighting is enabled -/** Without lighting being enabled the vertex color is the only value defining the fragment color. -Once lighting is enabled, the four values for diffuse, ambient, emissive, and specular take over. -With these flags it is possible to define which lighting factor shall be defined by the vertex color -instead of the lighting factor which is the same for all faces of that material. -The default is to use vertex color for the diffuse value, another pretty common value is to use -vertex color for both diffuse and ambient factor. */ -enum E_COLOR_MATERIAL -{ - //! Don't use vertex color for lighting - ECM_NONE = 0, - //! Use vertex color for diffuse light, this is default - ECM_DIFFUSE, - //! Use vertex color for ambient light - ECM_AMBIENT, - //! Use vertex color for emissive light - ECM_EMISSIVE, - //! Use vertex color for specular light - ECM_SPECULAR, - //! Use vertex color for both diffuse and ambient light - ECM_DIFFUSE_AND_AMBIENT -}; - //! Names for polygon offset direction const c8 *const PolygonOffsetDirectionNames[] = { "Back", @@ -263,16 +239,14 @@ class SMaterial public: //! Default constructor. Creates a solid, lit material with white colors SMaterial() : - MaterialType(EMT_SOLID), AmbientColor(255, 255, 255, 255), - DiffuseColor(255, 255, 255, 255), EmissiveColor(0, 0, 0, 0), - SpecularColor(255, 255, 255, 255), Shininess(0.0f), + MaterialType(EMT_SOLID), ColorParam(0, 0, 0, 0), MaterialTypeParam(0.0f), Thickness(1.0f), ZBuffer(ECFN_LESSEQUAL), - AntiAliasing(EAAM_SIMPLE), ColorMask(ECP_ALL), ColorMaterial(ECM_DIFFUSE), + AntiAliasing(EAAM_SIMPLE), ColorMask(ECP_ALL), BlendOperation(EBO_NONE), BlendFactor(0.0f), PolygonOffsetDepthBias(0.f), PolygonOffsetSlopeScale(0.f), Wireframe(false), PointCloud(false), - GouraudShading(true), Lighting(true), ZWriteEnable(EZW_AUTO), + ZWriteEnable(EZW_AUTO), BackfaceCulling(true), FrontfaceCulling(false), FogEnable(false), - NormalizeNormals(false), UseMipMaps(true) + UseMipMaps(true) { } @@ -282,42 +256,9 @@ public: //! Type of the material. Specifies how everything is blended together E_MATERIAL_TYPE MaterialType; - //! How much ambient light (a global light) is reflected by this material. - /** The default is full white, meaning objects are completely - globally illuminated. Reduce this if you want to see diffuse - or specular light effects. */ - SColor AmbientColor; - - //! How much diffuse light coming from a light source is reflected by this material. - /** The default is full white. */ - SColor DiffuseColor; - - //! Light emitted by this material. Default is to emit no light. - SColor EmissiveColor; - - //! How much specular light (highlights from a light) is reflected. - /** The default is to reflect white specular light. See - SMaterial::Shininess on how to enable specular lights. */ - SColor SpecularColor; - - //! Value affecting the size of specular highlights. - /** A value of 20 is common. If set to 0, no specular - highlights are being used. To activate, simply set the - shininess of a material to a value in the range [0.5;128]: - \code - sceneNode->getMaterial(0).Shininess = 20.0f; - \endcode - - You can change the color of the highlights using - \code - sceneNode->getMaterial(0).SpecularColor.set(255,255,255,255); - \endcode - - The specular color of the dynamic lights - (SLight::SpecularColor) will influence the the highlight color - too, but they are set to a useful value by default when - creating the light scene node.*/ - f32 Shininess; + //! Custom color parameter, can be used by custom shader materials. + // See MainShaderConstantSetter in Minetest. + SColor ColorParam; //! Free parameter, dependent on the material type. /** Mostly ignored, used for example in @@ -345,14 +286,6 @@ public: depth or stencil buffer, or using Red and Green for Stereo rendering. */ u8 ColorMask : 4; - //! Defines the interpretation of vertex color in the lighting equation - /** Values should be chosen from E_COLOR_MATERIAL. - When lighting is enabled, vertex color can be used instead of the - material values for light modulation. This allows to easily change e.g. the - diffuse light behavior of each face. The default, ECM_DIFFUSE, will result in - a very similar rendering as with lighting turned off, just with light shading. */ - u8 ColorMaterial : 3; - //! Store the blend operation of choice /** Values to be chosen from E_BLEND_OPERATION. */ E_BLEND_OPERATION BlendOperation : 4; @@ -393,12 +326,6 @@ public: //! Draw as point cloud or filled triangles? Default: false bool PointCloud : 1; - //! Flat or Gouraud shading? Default: true - bool GouraudShading : 1; - - //! Will this material be lighted? Default: true - bool Lighting : 1; - //! Is the zbuffer writable or is it read-only. Default: EZW_AUTO. /** If this parameter is not EZW_OFF, you probably also want to set ZBuffer to values other than ECFN_DISABLED */ @@ -413,10 +340,6 @@ public: //! Is fog enabled? Default: false bool FogEnable : 1; - //! Should normals be normalized? - /** Always use this if the mesh lit and scaled. Default: false */ - bool NormalizeNormals : 1; - //! Shall mipmaps be used if available /** Sometimes, disabling mipmap usage can be useful. Default: true */ bool UseMipMaps : 1; @@ -487,26 +410,18 @@ public: { bool different = MaterialType != b.MaterialType || - AmbientColor != b.AmbientColor || - DiffuseColor != b.DiffuseColor || - EmissiveColor != b.EmissiveColor || - SpecularColor != b.SpecularColor || - Shininess != b.Shininess || + ColorParam != b.ColorParam || MaterialTypeParam != b.MaterialTypeParam || Thickness != b.Thickness || Wireframe != b.Wireframe || PointCloud != b.PointCloud || - GouraudShading != b.GouraudShading || - Lighting != b.Lighting || ZBuffer != b.ZBuffer || ZWriteEnable != b.ZWriteEnable || BackfaceCulling != b.BackfaceCulling || FrontfaceCulling != b.FrontfaceCulling || FogEnable != b.FogEnable || - NormalizeNormals != b.NormalizeNormals || AntiAliasing != b.AntiAliasing || ColorMask != b.ColorMask || - ColorMaterial != b.ColorMaterial || BlendOperation != b.BlendOperation || BlendFactor != b.BlendFactor || PolygonOffsetDepthBias != b.PolygonOffsetDepthBias || diff --git a/irr/include/SMesh.h b/irr/include/SMesh.h index e865a5d2d..15fa65115 100644 --- a/irr/include/SMesh.h +++ b/irr/include/SMesh.h @@ -4,17 +4,17 @@ #pragma once +#include #include "IMesh.h" #include "IMeshBuffer.h" #include "aabbox3d.h" -#include "irrArray.h" namespace irr { namespace scene { //! Simple implementation of the IMesh interface. -struct SMesh : public IMesh +struct SMesh final : public IMesh { //! constructor SMesh() @@ -28,15 +28,15 @@ struct SMesh : public IMesh virtual ~SMesh() { // drop buffers - for (u32 i = 0; i < MeshBuffers.size(); ++i) - MeshBuffers[i]->drop(); + for (auto *buf : MeshBuffers) + buf->drop(); } //! clean mesh virtual void clear() { - for (u32 i = 0; i < MeshBuffers.size(); ++i) - MeshBuffers[i]->drop(); + for (auto *buf : MeshBuffers) + buf->drop(); MeshBuffers.clear(); BoundingBox.reset(0.f, 0.f, 0.f); } @@ -44,7 +44,7 @@ struct SMesh : public IMesh //! returns amount of mesh buffers. u32 getMeshBufferCount() const override { - return MeshBuffers.size(); + return static_cast(MeshBuffers.size()); } //! returns pointer to a mesh buffer @@ -57,14 +57,24 @@ struct SMesh : public IMesh /** reverse search */ IMeshBuffer *getMeshBuffer(const video::SMaterial &material) const override { - for (s32 i = (s32)MeshBuffers.size() - 1; i >= 0; --i) { - if (material == MeshBuffers[i]->getMaterial()) - return MeshBuffers[i]; + for (auto it = MeshBuffers.rbegin(); it != MeshBuffers.rend(); it++) { + if (material == (*it)->getMaterial()) + return *it; } - - return 0; + return nullptr; } + u32 getTextureSlot(u32 meshbufNr) const override + { + return TextureSlots.at(meshbufNr); + } + + void setTextureSlot(u32 meshbufNr, u32 textureSlot) + { + TextureSlots.at(meshbufNr) = textureSlot; + } + + //! returns an axis aligned bounding box const core::aabbox3d &getBoundingBox() const override { @@ -81,8 +91,8 @@ struct SMesh : public IMesh void recalculateBoundingBox() { bool hasMeshBufferBBox = false; - for (u32 i = 0; i < MeshBuffers.size(); ++i) { - const core::aabbox3df &bb = MeshBuffers[i]->getBoundingBox(); + for (auto *buf : MeshBuffers) { + const core::aabbox3df &bb = buf->getBoundingBox(); if (!bb.isEmpty()) { if (!hasMeshBufferBBox) { hasMeshBufferBBox = true; @@ -104,25 +114,28 @@ struct SMesh : public IMesh if (buf) { buf->grab(); MeshBuffers.push_back(buf); + TextureSlots.push_back(getMeshBufferCount() - 1); } } //! set the hardware mapping hint, for driver void setHardwareMappingHint(E_HARDWARE_MAPPING newMappingHint, E_BUFFER_TYPE buffer = EBT_VERTEX_AND_INDEX) override { - for (u32 i = 0; i < MeshBuffers.size(); ++i) - MeshBuffers[i]->setHardwareMappingHint(newMappingHint, buffer); + for (auto *buf : MeshBuffers) + buf->setHardwareMappingHint(newMappingHint, buffer); } //! flags the meshbuffer as changed, reloads hardware buffers void setDirty(E_BUFFER_TYPE buffer = EBT_VERTEX_AND_INDEX) override { - for (u32 i = 0; i < MeshBuffers.size(); ++i) - MeshBuffers[i]->setDirty(buffer); + for (auto *buf : MeshBuffers) + buf->setDirty(buffer); } //! The meshbuffers of this mesh - core::array MeshBuffers; + std::vector MeshBuffers; + //! Mapping from meshbuffer number to bindable texture slot + std::vector TextureSlots; //! The bounding box of this mesh core::aabbox3d BoundingBox; diff --git a/irr/include/SOverrideMaterial.h b/irr/include/SOverrideMaterial.h index 6de6e6ebb..1ae324211 100644 --- a/irr/include/SOverrideMaterial.h +++ b/irr/include/SOverrideMaterial.h @@ -4,6 +4,7 @@ #pragma once +#include #include "SMaterial.h" namespace irr @@ -57,7 +58,7 @@ struct SOverrideMaterial }; //! To overwrite SMaterial::MaterialType - core::array MaterialTypes; + std::vector MaterialTypes; //! Default constructor SOverrideMaterial() : @@ -83,9 +84,8 @@ struct SOverrideMaterial void apply(SMaterial &material) { if (Enabled) { - for (u32 i = 0; i < MaterialTypes.size(); ++i) { - const SMaterialTypeReplacement &mtr = MaterialTypes[i]; - if (mtr.Original < 0 || (s32)mtr.Original == material.MaterialType) + for (const auto &mtr : MaterialTypes) { + if (mtr.Original < 0 || mtr.Original == (s32)material.MaterialType) material.MaterialType = (E_MATERIAL_TYPE)mtr.Replacement; } for (u32 f = 0; f < 32; ++f) { @@ -98,12 +98,6 @@ struct SOverrideMaterial case EMP_POINTCLOUD: material.PointCloud = Material.PointCloud; break; - case EMP_GOURAUD_SHADING: - material.GouraudShading = Material.GouraudShading; - break; - case EMP_LIGHTING: - material.Lighting = Material.Lighting; - break; case EMP_ZBUFFER: material.ZBuffer = Material.ZBuffer; break; @@ -140,9 +134,6 @@ struct SOverrideMaterial case EMP_FOG_ENABLE: material.FogEnable = Material.FogEnable; break; - case EMP_NORMALIZE_NORMALS: - material.NormalizeNormals = Material.NormalizeNormals; - break; case EMP_TEXTURE_WRAP: for (u32 i = 0; i < MATERIAL_MAX_TEXTURES; ++i) { if (EnableLayerProps[i]) { @@ -158,9 +149,6 @@ struct SOverrideMaterial case EMP_COLOR_MASK: material.ColorMask = Material.ColorMask; break; - case EMP_COLOR_MATERIAL: - material.ColorMaterial = Material.ColorMaterial; - break; case EMP_USE_MIP_MAPS: material.UseMipMaps = Material.UseMipMaps; break; diff --git a/irr/include/SSkinMeshBuffer.h b/irr/include/SSkinMeshBuffer.h index 5ced6057d..303207d93 100644 --- a/irr/include/SSkinMeshBuffer.h +++ b/irr/include/SSkinMeshBuffer.h @@ -5,7 +5,10 @@ #pragma once #include "IMeshBuffer.h" +#include "CVertexBuffer.h" +#include "CIndexBuffer.h" #include "S3DVertex.h" +#include "irrArray.h" namespace irr { @@ -13,19 +16,36 @@ namespace scene { //! A mesh buffer able to choose between S3DVertex2TCoords, S3DVertex and S3DVertexTangents at runtime -struct SSkinMeshBuffer : public IMeshBuffer +struct SSkinMeshBuffer final : public IMeshBuffer { //! Default constructor SSkinMeshBuffer(video::E_VERTEX_TYPE vt = video::EVT_STANDARD) : - ChangedID_Vertex(1), ChangedID_Index(1), VertexType(vt), - PrimitiveType(EPT_TRIANGLES), - MappingHint_Vertex(EHM_NEVER), MappingHint_Index(EHM_NEVER), - HWBuffer(NULL), + VertexType(vt), PrimitiveType(EPT_TRIANGLES), BoundingBoxNeedsRecalculated(true) { #ifdef _DEBUG setDebugName("SSkinMeshBuffer"); #endif + Vertices_Tangents = new SVertexBufferTangents(); + Vertices_2TCoords = new SVertexBufferLightMap(); + Vertices_Standard = new SVertexBuffer(); + Indices = new SIndexBuffer(); + } + + //! Constructor for standard vertices + SSkinMeshBuffer(std::vector &&vertices, std::vector &&indices) : + SSkinMeshBuffer() + { + Vertices_Standard->Data = std::move(vertices); + Indices->Data = std::move(indices); + } + + ~SSkinMeshBuffer() + { + Vertices_Tangents->drop(); + Vertices_2TCoords->drop(); + Vertices_Standard->drop(); + Indices->drop(); } //! Get Material of this buffer. @@ -40,83 +60,53 @@ struct SSkinMeshBuffer : public IMeshBuffer return Material; } + const scene::IVertexBuffer *getVertexBuffer() const override + { + switch (VertexType) { + case video::EVT_2TCOORDS: + return Vertices_2TCoords; + case video::EVT_TANGENTS: + return Vertices_Tangents; + default: + return Vertices_Standard; + } + } + + scene::IVertexBuffer *getVertexBuffer() override + { + switch (VertexType) { + case video::EVT_2TCOORDS: + return Vertices_2TCoords; + case video::EVT_TANGENTS: + return Vertices_Tangents; + default: + return Vertices_Standard; + } + } + + const scene::IIndexBuffer *getIndexBuffer() const override + { + return Indices; + } + + scene::IIndexBuffer *getIndexBuffer() override + { + return Indices; + } + //! Get standard vertex at given index virtual video::S3DVertex *getVertex(u32 index) { switch (VertexType) { case video::EVT_2TCOORDS: - return (video::S3DVertex *)&Vertices_2TCoords[index]; + return &Vertices_2TCoords->Data[index]; case video::EVT_TANGENTS: - return (video::S3DVertex *)&Vertices_Tangents[index]; + return &Vertices_Tangents->Data[index]; default: - return &Vertices_Standard[index]; + return &Vertices_Standard->Data[index]; } } - //! Get pointer to vertex array - const void *getVertices() const override - { - switch (VertexType) { - case video::EVT_2TCOORDS: - return Vertices_2TCoords.const_pointer(); - case video::EVT_TANGENTS: - return Vertices_Tangents.const_pointer(); - default: - return Vertices_Standard.const_pointer(); - } - } - - //! Get pointer to vertex array - void *getVertices() override - { - switch (VertexType) { - case video::EVT_2TCOORDS: - return Vertices_2TCoords.pointer(); - case video::EVT_TANGENTS: - return Vertices_Tangents.pointer(); - default: - return Vertices_Standard.pointer(); - } - } - - //! Get vertex count - u32 getVertexCount() const override - { - switch (VertexType) { - case video::EVT_2TCOORDS: - return Vertices_2TCoords.size(); - case video::EVT_TANGENTS: - return Vertices_Tangents.size(); - default: - return Vertices_Standard.size(); - } - } - - //! Get type of index data which is stored in this meshbuffer. - /** \return Index type of this buffer. */ - video::E_INDEX_TYPE getIndexType() const override - { - return video::EIT_16BIT; - } - - //! Get pointer to index array - const u16 *getIndices() const override - { - return Indices.const_pointer(); - } - - //! Get pointer to index array - u16 *getIndices() override - { - return Indices.pointer(); - } - - //! Get index count - u32 getIndexCount() const override - { - return Indices.size(); - } - //! Get bounding box const core::aabbox3d &getBoundingBox() const override { @@ -129,6 +119,28 @@ struct SSkinMeshBuffer : public IMeshBuffer BoundingBox = box; } +private: + template void recalculateBoundingBox(const CVertexBuffer *buf) + { + if (!buf->getCount()) { + BoundingBox.reset(0, 0, 0); + } else { + auto &vertices = buf->Data; + BoundingBox.reset(vertices[0].Pos); + for (size_t i = 1; i < vertices.size(); ++i) + BoundingBox.addInternalPoint(vertices[i].Pos); + } + } + + template static void copyVertex(const T1 &src, T2 &dst) + { + dst.Pos = src.Pos; + dst.Normal = src.Normal; + dst.Color = src.Color; + dst.TCoords = src.TCoords; + } +public: + //! Recalculate bounding box void recalculateBoundingBox() override { @@ -139,57 +151,30 @@ struct SSkinMeshBuffer : public IMeshBuffer switch (VertexType) { case video::EVT_STANDARD: { - if (Vertices_Standard.empty()) - BoundingBox.reset(0, 0, 0); - else { - BoundingBox.reset(Vertices_Standard[0].Pos); - for (u32 i = 1; i < Vertices_Standard.size(); ++i) - BoundingBox.addInternalPoint(Vertices_Standard[i].Pos); - } + recalculateBoundingBox(Vertices_Standard); break; } case video::EVT_2TCOORDS: { - if (Vertices_2TCoords.empty()) - BoundingBox.reset(0, 0, 0); - else { - BoundingBox.reset(Vertices_2TCoords[0].Pos); - for (u32 i = 1; i < Vertices_2TCoords.size(); ++i) - BoundingBox.addInternalPoint(Vertices_2TCoords[i].Pos); - } + recalculateBoundingBox(Vertices_2TCoords); break; } case video::EVT_TANGENTS: { - if (Vertices_Tangents.empty()) - BoundingBox.reset(0, 0, 0); - else { - BoundingBox.reset(Vertices_Tangents[0].Pos); - for (u32 i = 1; i < Vertices_Tangents.size(); ++i) - BoundingBox.addInternalPoint(Vertices_Tangents[i].Pos); - } + recalculateBoundingBox(Vertices_Tangents); break; } } } - //! Get vertex type - video::E_VERTEX_TYPE getVertexType() const override - { - return VertexType; - } - //! Convert to 2tcoords vertex type void convertTo2TCoords() { if (VertexType == video::EVT_STANDARD) { - for (u32 n = 0; n < Vertices_Standard.size(); ++n) { - video::S3DVertex2TCoords Vertex; - Vertex.Color = Vertices_Standard[n].Color; - Vertex.Pos = Vertices_Standard[n].Pos; - Vertex.Normal = Vertices_Standard[n].Normal; - Vertex.TCoords = Vertices_Standard[n].TCoords; - Vertices_2TCoords.push_back(Vertex); + video::S3DVertex2TCoords Vertex; + for (const auto &Vertex_Standard : Vertices_Standard->Data) { + copyVertex(Vertex_Standard, Vertex); + Vertices_2TCoords->Data.push_back(Vertex); } - Vertices_Standard.clear(); + Vertices_Standard->Data.clear(); VertexType = video::EVT_2TCOORDS; } } @@ -198,134 +183,28 @@ struct SSkinMeshBuffer : public IMeshBuffer void convertToTangents() { if (VertexType == video::EVT_STANDARD) { - for (u32 n = 0; n < Vertices_Standard.size(); ++n) { - video::S3DVertexTangents Vertex; - Vertex.Color = Vertices_Standard[n].Color; - Vertex.Pos = Vertices_Standard[n].Pos; - Vertex.Normal = Vertices_Standard[n].Normal; - Vertex.TCoords = Vertices_Standard[n].TCoords; - Vertices_Tangents.push_back(Vertex); + video::S3DVertexTangents Vertex; + for (const auto &Vertex_Standard : Vertices_Standard->Data) { + copyVertex(Vertex_Standard, Vertex); + Vertices_Tangents->Data.push_back(Vertex); } - Vertices_Standard.clear(); + Vertices_Standard->Data.clear(); VertexType = video::EVT_TANGENTS; } else if (VertexType == video::EVT_2TCOORDS) { - for (u32 n = 0; n < Vertices_2TCoords.size(); ++n) { - video::S3DVertexTangents Vertex; - Vertex.Color = Vertices_2TCoords[n].Color; - Vertex.Pos = Vertices_2TCoords[n].Pos; - Vertex.Normal = Vertices_2TCoords[n].Normal; - Vertex.TCoords = Vertices_2TCoords[n].TCoords; - Vertices_Tangents.push_back(Vertex); + video::S3DVertexTangents Vertex; + for (const auto &Vertex_2TCoords : Vertices_2TCoords->Data) { + copyVertex(Vertex_2TCoords, Vertex); + Vertices_Tangents->Data.push_back(Vertex); } - Vertices_2TCoords.clear(); + Vertices_2TCoords->Data.clear(); VertexType = video::EVT_TANGENTS; } } - //! returns position of vertex i - const core::vector3df &getPosition(u32 i) const override - { - switch (VertexType) { - case video::EVT_2TCOORDS: - return Vertices_2TCoords[i].Pos; - case video::EVT_TANGENTS: - return Vertices_Tangents[i].Pos; - default: - return Vertices_Standard[i].Pos; - } - } - - //! returns position of vertex i - core::vector3df &getPosition(u32 i) override - { - switch (VertexType) { - case video::EVT_2TCOORDS: - return Vertices_2TCoords[i].Pos; - case video::EVT_TANGENTS: - return Vertices_Tangents[i].Pos; - default: - return Vertices_Standard[i].Pos; - } - } - - //! returns normal of vertex i - const core::vector3df &getNormal(u32 i) const override - { - switch (VertexType) { - case video::EVT_2TCOORDS: - return Vertices_2TCoords[i].Normal; - case video::EVT_TANGENTS: - return Vertices_Tangents[i].Normal; - default: - return Vertices_Standard[i].Normal; - } - } - - //! returns normal of vertex i - core::vector3df &getNormal(u32 i) override - { - switch (VertexType) { - case video::EVT_2TCOORDS: - return Vertices_2TCoords[i].Normal; - case video::EVT_TANGENTS: - return Vertices_Tangents[i].Normal; - default: - return Vertices_Standard[i].Normal; - } - } - - //! returns texture coords of vertex i - const core::vector2df &getTCoords(u32 i) const override - { - switch (VertexType) { - case video::EVT_2TCOORDS: - return Vertices_2TCoords[i].TCoords; - case video::EVT_TANGENTS: - return Vertices_Tangents[i].TCoords; - default: - return Vertices_Standard[i].TCoords; - } - } - - //! returns texture coords of vertex i - core::vector2df &getTCoords(u32 i) override - { - switch (VertexType) { - case video::EVT_2TCOORDS: - return Vertices_2TCoords[i].TCoords; - case video::EVT_TANGENTS: - return Vertices_Tangents[i].TCoords; - default: - return Vertices_Standard[i].TCoords; - } - } - //! append the vertices and indices to the current buffer - void append(const void *const vertices, u32 numVertices, const u16 *const indices, u32 numIndices) override {} - - //! get the current hardware mapping hint for vertex buffers - E_HARDWARE_MAPPING getHardwareMappingHint_Vertex() const override + void append(const void *const vertices, u32 numVertices, const u16 *const indices, u32 numIndices) override { - return MappingHint_Vertex; - } - - //! get the current hardware mapping hint for index buffers - E_HARDWARE_MAPPING getHardwareMappingHint_Index() const override - { - return MappingHint_Index; - } - - //! set the hardware mapping hint, for driver - void setHardwareMappingHint(E_HARDWARE_MAPPING NewMappingHint, E_BUFFER_TYPE Buffer = EBT_VERTEX_AND_INDEX) override - { - if (Buffer == EBT_VERTEX) - MappingHint_Vertex = NewMappingHint; - else if (Buffer == EBT_INDEX) - MappingHint_Index = NewMappingHint; - else if (Buffer == EBT_VERTEX_AND_INDEX) { - MappingHint_Vertex = NewMappingHint; - MappingHint_Index = NewMappingHint; - } + _IRR_DEBUG_BREAK_IF(true); } //! Describe what kind of primitive geometry is used by the meshbuffer @@ -340,41 +219,14 @@ struct SSkinMeshBuffer : public IMeshBuffer return PrimitiveType; } - //! flags the mesh as changed, reloads hardware buffers - void setDirty(E_BUFFER_TYPE Buffer = EBT_VERTEX_AND_INDEX) override - { - if (Buffer == EBT_VERTEX_AND_INDEX || Buffer == EBT_VERTEX) - ++ChangedID_Vertex; - if (Buffer == EBT_VERTEX_AND_INDEX || Buffer == EBT_INDEX) - ++ChangedID_Index; - } - - u32 getChangedID_Vertex() const override { return ChangedID_Vertex; } - - u32 getChangedID_Index() const override { return ChangedID_Index; } - - void setHWBuffer(void *ptr) const override - { - HWBuffer = ptr; - } - - void *getHWBuffer() const override - { - return HWBuffer; - } - //! Call this after changing the positions of any vertex. void boundingBoxNeedsRecalculated(void) { BoundingBoxNeedsRecalculated = true; } - core::array Vertices_Tangents; - core::array Vertices_2TCoords; - core::array Vertices_Standard; - core::array Indices; + SVertexBufferTangents *Vertices_Tangents; + SVertexBufferLightMap *Vertices_2TCoords; + SVertexBuffer *Vertices_Standard; + SIndexBuffer *Indices; - u32 ChangedID_Vertex; - u32 ChangedID_Index; - - // ISkinnedMesh::SJoint *AttachedJoint; core::matrix4 Transformation; video::SMaterial Material; @@ -385,13 +237,7 @@ struct SSkinMeshBuffer : public IMeshBuffer //! Primitive type used for rendering (triangles, lines, ...) E_PRIMITIVE_TYPE PrimitiveType; - // hardware mapping hint - E_HARDWARE_MAPPING MappingHint_Vertex : 3; - E_HARDWARE_MAPPING MappingHint_Index : 3; - - mutable void *HWBuffer; - - bool BoundingBoxNeedsRecalculated : 1; + bool BoundingBoxNeedsRecalculated; }; } // end namespace scene diff --git a/irr/include/SViewFrustum.h b/irr/include/SViewFrustum.h index 06983cc5e..cd898e032 100644 --- a/irr/include/SViewFrustum.h +++ b/irr/include/SViewFrustum.h @@ -9,7 +9,7 @@ #include "line3d.h" #include "aabbox3d.h" #include "matrix4.h" -#include "IVideoDriver.h" +#include "EVideoTypes.h" namespace irr { diff --git a/irr/include/coreutil.h b/irr/include/coreutil.h index 60014c4a7..73d1c4b43 100644 --- a/irr/include/coreutil.h +++ b/irr/include/coreutil.h @@ -63,7 +63,7 @@ inline io::path &getFileNameExtension(io::path &dest, const io::path &source) } //! delete path from filename -inline io::path &deletePathFromFilename(io::path &filename) +inline io::path deletePathFromFilename(const io::path &filename) { // delete path from filename const fschar_t *s = filename.c_str(); @@ -73,11 +73,10 @@ inline io::path &deletePathFromFilename(io::path &filename) while (*p != '/' && *p != '\\' && p != s) p--; - if (p != s) { + if (p != s) ++p; - filename = p; - } - return filename; + + return p; } //! trim paths diff --git a/irr/include/irrArray.h b/irr/include/irrArray.h index b6f573a79..9f390e79b 100644 --- a/irr/include/irrArray.h +++ b/irr/include/irrArray.h @@ -45,6 +45,10 @@ public: { } + //! Move constructor + array(std::vector &&data) : + m_data(std::move(data)), is_sorted(false) {} + //! Reallocates the array, make it bigger or smaller. /** \param new_size New size of array. \param canShrink Specifies whether the array is reallocated even if @@ -167,13 +171,6 @@ public: return *this; } - array &operator=(std::vector &&other) - { - m_data = std::move(other); - is_sorted = false; - return *this; - } - //! Equality operator bool operator==(const array &other) const { @@ -400,16 +397,6 @@ public: std::swap(is_sorted, other.is_sorted); } - //! Pull the contents of this array as a vector. - // The array is left empty. - std::vector steal() - { - std::vector ret = std::move(m_data); - m_data.clear(); - is_sorted = true; - return ret; - } - typedef T value_type; typedef u32 size_type; diff --git a/irr/include/irrString.h b/irr/include/irrString.h index a583c9e4b..76e0548d3 100644 --- a/irr/include/irrString.h +++ b/irr/include/irrString.h @@ -11,7 +11,6 @@ #include #include #include -#include /* HACK: import these string methods from MT's util/string.h */ extern std::wstring utf8_to_wide(std::string_view input); @@ -65,6 +64,7 @@ static inline u32 locale_upper(u32 x) template class string { + using stl_type = std::basic_string; public: typedef T char_type; @@ -79,6 +79,10 @@ public: *this = other; } + string(const stl_type &str) : str(str) {} + + string(stl_type &&str) : str(std::move(str)) {} + //! Constructor from other string types template string(const string &other) @@ -169,13 +173,24 @@ public: return *this; } - // no longer allowed! - _IRR_DEBUG_BREAK_IF((void *)c == (void *)c_str()); + if constexpr (sizeof(T) != sizeof(B)) { + _IRR_DEBUG_BREAK_IF( + (uintptr_t)c >= (uintptr_t)(str.data()) && + (uintptr_t)c < (uintptr_t)(str.data() + str.size())); + } + + if ((void *)c == (void *)c_str()) + return *this; u32 len = calclen(c); - str.resize(len); + // In case `c` is a pointer to our own buffer, we may not resize first + // or it can become invalid. + if (len > str.size()) + str.resize(len); for (u32 l = 0; l < len; ++l) - str[l] = (T)c[l]; + str[l] = static_cast(c[l]); + if (len < str.size()) + str.resize(len); return *this; } @@ -814,13 +829,6 @@ public: friend size_t wStringToUTF8(stringc &destination, const wchar_t *source); private: - typedef std::basic_string stl_type; - - //! Private constructor - string(stl_type &&str) : - str(str) - { - } //! strlen wrapper template diff --git a/src/irr_ptr.h b/irr/include/irr_ptr.h similarity index 87% rename from src/irr_ptr.h rename to irr/include/irr_ptr.h index fc4a0f558..48717976b 100644 --- a/src/irr_ptr.h +++ b/irr/include/irr_ptr.h @@ -20,8 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once #include #include -#include "irrlichttypes.h" -#include "IReferenceCounted.h" +namespace irr { class IReferenceCounted; } /** Shared pointer for IrrLicht objects. * @@ -37,15 +36,13 @@ with this program; if not, write to the Free Software Foundation, Inc., * from such object is a bug and may lead to a crash. Indirect construction * is possible though; see the @c grab free function for details and use cases. */ -template ::value>::type> +template class irr_ptr { ReferenceCounted *value = nullptr; public: - irr_ptr() {} + irr_ptr() noexcept = default; irr_ptr(std::nullptr_t) noexcept {} @@ -53,15 +50,15 @@ public: irr_ptr(irr_ptr &&b) noexcept { reset(b.release()); } - template ::value>::type> + template , bool> = true> irr_ptr(const irr_ptr &b) noexcept { grab(b.get()); } - template ::value>::type> + template , bool> = true> irr_ptr(irr_ptr &&b) noexcept { reset(b.release()); @@ -88,16 +85,16 @@ public: return *this; } - template ::value>::type> + template , bool> = true> irr_ptr &operator=(const irr_ptr &b) noexcept { grab(b.get()); return *this; } - template ::value>::type> + template , bool> = true> irr_ptr &operator=(irr_ptr &&b) noexcept { reset(b.release()); @@ -128,6 +125,8 @@ public: */ void reset(ReferenceCounted *object = nullptr) noexcept { + static_assert(std::is_base_of_v, + "Class is not an IReferenceCounted"); if (value) value->drop(); value = object; @@ -138,6 +137,8 @@ public: */ void grab(ReferenceCounted *object) noexcept { + static_assert(std::is_base_of_v, + "Class is not an IReferenceCounted"); if (object) object->grab(); reset(object); @@ -152,6 +153,7 @@ public: * in this function and decreased when the returned pointer is destroyed. */ template +[[nodiscard]] irr_ptr grab(ReferenceCounted *object) noexcept { irr_ptr ptr; diff --git a/irr/include/matrix4.h b/irr/include/matrix4.h index 374fc6e4a..8fce0157a 100644 --- a/irr/include/matrix4.h +++ b/irr/include/matrix4.h @@ -24,7 +24,12 @@ namespace core { //! 4x4 matrix. Mostly used as transformation matrix for 3d calculations. -/** The matrix is a D3D style matrix, row major with translations in the 4th row. */ +/** Conventions: Matrices are considered to be in row-major order. + * Multiplication of a matrix A with a row vector v is the premultiplication vA. + * Translations are thus in the 4th row. + * The matrix product AB yields a matrix C such that vC = (vB)A: + * B is applied first, then A. + */ template class CMatrix4 { @@ -242,17 +247,11 @@ public: //! Translate a vector by the inverse of the translation part of this matrix. void inverseTranslateVect(vector3df &vect) const; - //! Rotate a vector by the inverse of the rotation part of this matrix. - void inverseRotateVect(vector3df &vect) const; + //! Scale a vector, then rotate by the inverse of the rotation part of this matrix. + [[nodiscard]] vector3d scaleThenInvRotVect(const vector3d &vect) const; - //! Rotate a vector by the rotation part of this matrix. - void rotateVect(vector3df &vect) const; - - //! An alternate transform vector method, writing into a second vector - void rotateVect(core::vector3df &out, const core::vector3df &in) const; - - //! An alternate transform vector method, writing into an array of 3 floats - void rotateVect(T *out, const core::vector3df &in) const; + //! Rotate and scale a vector. Applies both rotation & scale part of the matrix. + [[nodiscard]] vector3d rotateAndScaleVect(const vector3d &vect) const; //! Transforms the vector by this matrix /** This operation is performed as if the vector was 4d with the 4th component =1 */ @@ -1154,39 +1153,23 @@ inline bool CMatrix4::isIdentity_integer_base() const } template -inline void CMatrix4::rotateVect(vector3df &vect) const +inline vector3d CMatrix4::rotateAndScaleVect(const vector3d &v) const { - vector3d tmp(static_cast(vect.X), static_cast(vect.Y), static_cast(vect.Z)); - vect.X = static_cast(tmp.X * M[0] + tmp.Y * M[4] + tmp.Z * M[8]); - vect.Y = static_cast(tmp.X * M[1] + tmp.Y * M[5] + tmp.Z * M[9]); - vect.Z = static_cast(tmp.X * M[2] + tmp.Y * M[6] + tmp.Z * M[10]); -} - -//! An alternate transform vector method, writing into a second vector -template -inline void CMatrix4::rotateVect(core::vector3df &out, const core::vector3df &in) const -{ - out.X = in.X * M[0] + in.Y * M[4] + in.Z * M[8]; - out.Y = in.X * M[1] + in.Y * M[5] + in.Z * M[9]; - out.Z = in.X * M[2] + in.Y * M[6] + in.Z * M[10]; -} - -//! An alternate transform vector method, writing into an array of 3 floats -template -inline void CMatrix4::rotateVect(T *out, const core::vector3df &in) const -{ - out[0] = in.X * M[0] + in.Y * M[4] + in.Z * M[8]; - out[1] = in.X * M[1] + in.Y * M[5] + in.Z * M[9]; - out[2] = in.X * M[2] + in.Y * M[6] + in.Z * M[10]; + return { + v.X * M[0] + v.Y * M[4] + v.Z * M[8], + v.X * M[1] + v.Y * M[5] + v.Z * M[9], + v.X * M[2] + v.Y * M[6] + v.Z * M[10] + }; } template -inline void CMatrix4::inverseRotateVect(vector3df &vect) const +inline vector3d CMatrix4::scaleThenInvRotVect(const vector3d &v) const { - vector3d tmp(static_cast(vect.X), static_cast(vect.Y), static_cast(vect.Z)); - vect.X = static_cast(tmp.X * M[0] + tmp.Y * M[1] + tmp.Z * M[2]); - vect.Y = static_cast(tmp.X * M[4] + tmp.Y * M[5] + tmp.Z * M[6]); - vect.Z = static_cast(tmp.X * M[8] + tmp.Y * M[9] + tmp.Z * M[10]); + return { + v.X * M[0] + v.Y * M[1] + v.Z * M[2], + v.X * M[4] + v.Y * M[5] + v.Z * M[6], + v.X * M[8] + v.Y * M[9] + v.Z * M[10] + }; } template @@ -1247,8 +1230,7 @@ inline void CMatrix4::transformPlane(core::plane3d &plane) const // Transform the normal by the transposed inverse of the matrix CMatrix4 transposedInverse(*this, EM4CONST_INVERSE_TRANSPOSED); - vector3df normal = plane.Normal; - transposedInverse.rotateVect(normal); + vector3df normal = transposedInverse.rotateAndScaleVect(plane.Normal); plane.setPlane(member, normal.normalize()); } diff --git a/irr/include/vector2d.h b/irr/include/vector2d.h index 4c41389f4..182965295 100644 --- a/irr/include/vector2d.h +++ b/irr/include/vector2d.h @@ -8,6 +8,7 @@ #include "dimension2d.h" #include +#include namespace irr { @@ -34,6 +35,15 @@ public: constexpr vector2d(const dimension2d &other) : X(other.Width), Y(other.Height) {} + explicit constexpr vector2d(const std::array &arr) : + X(arr[0]), Y(arr[1]) {} + + template + constexpr static vector2d from(const vector2d &other) + { + return {static_cast(other.X), static_cast(other.Y)}; + } + // operators vector2d operator-() const { return vector2d(-X, -Y); } diff --git a/irr/media/coolguy_opt.x b/irr/media/coolguy_opt.x deleted file mode 100755 index e806d8315..000000000 --- a/irr/media/coolguy_opt.x +++ /dev/null @@ -1,2 +0,0 @@ -xof 0303txt 0032 -AnimationSet{Animation{{Armature}AnimationKey{0;2;0;4;1,0,0,0;;,29;4;1,0,0,0;;;}AnimationKey{2;2;0;3;0,0,0;;,29;3;0,0,0;;;}}Animation{{Armature_knee_r}AnimationKey{0;16;0;4;0.864183,0.503177,0,0;;,1;4;0.829812,0.558043,0,0;;,3;4;0.708698,0.705512,0,0;;,5;4;0.589108,0.808054,0,0;;,7;4;0.593659,0.804717,0,0;;,9;4;0.748627,0.662991,0,0;;,11;4;0.910305,0.413938,0,0;;,13;4;0.975925,0.218107,0,0;;,15;4;0.981302,0.192476,0,0;;,17;4;0.975476,0.220108,0,0;;,19;4;0.963662,0.267124,0,0;;,21;4;0.945893,0.324478,0,0;;,23;4;0.923816,0.382838,0,0;;,25;4;0.901205,0.433394,0,0;;,27;4;0.883429,0.468566,0,0;;,29;4;0.876305,0.481757,0,0;;;}AnimationKey{2;2;0;3;0,0,1.10139;;,29;3;0,0,1.10139;;;}}Animation{{Armature_elbow_r}AnimationKey{0;16;0;4;0.756295,0.004619,-0.619265,0.210967;;,1;4;0.771977,0.005599,-0.60257,0.202311;;,3;4;0.825501,0.009164,-0.538259,0.169533;;,5;4;0.891859,0.014253,-0.436142,0.119019;;,7;4;0.949154,0.019821,-0.308768,0.058108;;,9;4;0.983251,0.024703,-0.18057,-0.001258;;,11;4;0.995416,0.028143,-0.07812,-0.047458;;,13;4;0.996672,0.02991,-0.020368,-0.073041;;,15;4;0.996672,0.02991,-0.020368,-0.073041;;,17;4;0.995416,0.028143,-0.07812,-0.047458;;,19;4;0.983251,0.024703,-0.18057,-0.001258;;,21;4;0.949154,0.019821,-0.308768,0.058108;;,23;4;0.891859,0.014253,-0.436142,0.119019;;,25;4;0.825501,0.009164,-0.538259,0.169533;;,27;4;0.771977,0.005599,-0.60257,0.202311;;,29;4;0.750682,0.004275,-0.625038,0.213976;;;}AnimationKey{2;2;0;3;0,0,0.754892;;,29;3;0,0,0.754892;;;}}Animation{{Armature_arm_r}AnimationKey{0;16;0;4;0.28219,0.629905,0.723388,-0.017285;;,1;4;0.277641,0.632543,0.722699,-0.022614;;,3;4;0.261375,0.641615,0.719924,-0.041507;;,5;4;0.238321,0.653533,0.715186,-0.067874;;,7;4;0.212026,0.665838,0.708676,-0.097381;;,9;4;0.186345,0.676585,0.701229,-0.125643;;,11;4;0.165298,0.684491,0.694351,-0.14841;;,13;4;0.152894,0.688778,0.68998,-0.161665;;,15;4;0.152894,0.688779,0.68998,-0.161665;;,17;4;0.165298,0.684491,0.694351,-0.14841;;,19;4;0.186345,0.676585,0.701229,-0.125643;;,21;4;0.212026,0.665838,0.708676,-0.097381;;,23;4;0.238321,0.653533,0.715186,-0.067874;;,25;4;0.261375,0.641615,0.719924,-0.041507;;,27;4;0.277641,0.632543,0.722699,-0.022614;;,29;4;0.283802,0.628959,0.723623,-0.015394;;;}AnimationKey{2;2;0;3;-0.545315,0,1;;,29;3;-0.545315,0,1;;;}}Animation{{Armature_knee_l}AnimationKey{0;16;0;4;0.981896,0.189423,0,0;;,1;4;0.9814,0.191974,0,0;;,3;4;0.979127,0.203251,0,0;;,5;4;0.974526,0.224276,0,0;;,7;4;0.96645,0.256853,0,0;;,9;4;0.953088,0.302692,0,0;;,11;4;0.931731,0.36315,0,0;;,13;4;0.898645,0.438676,0,0;;,15;4;0.848226,0.529634,0,0;;,17;4;0.773692,0.633562,0,0;;,19;4;0.689831,0.72397,0,0;;,21;4;0.629304,0.777159,0,0;;,23;4;0.648685,0.761057,0,0;;,25;4;0.812268,0.583284,0,0;;,27;4;0.948066,0.318074,0,0;;,29;4;0.982049,0.188624,0,0;;;}AnimationKey{2;2;0;3;0,0,1.10139;;,29;3;0,0,1.10139;;;}}Animation{{Armature_Bone_007}AnimationKey{0;16;0;4;0.993671,-0.112331,0,0;;,1;4;0.994784,-0.102002,0,0;;,3;4;0.997507,-0.070564,0,0;;,5;4;0.999237,-0.039056,0,0;;,7;4;0.999694,-0.024737,0,0;;,9;4;0.999079,-0.042907,0,0;;,11;4;0.99677,-0.080308,0,0;;,13;4;0.993798,-0.111199,0,0;;,15;4;0.993599,-0.112965,0,0;;,17;4;0.995813,-0.091409,0,0;;,19;4;0.998181,-0.060285,0,0;;,21;4;0.999479,-0.032286,0,0;;,23;4;0.999797,-0.020142,0,0;;,25;4;0.998983,-0.045097,0,0;;,27;4;0.995813,-0.091409,0,0;;,29;4;0.993221,-0.116243,0,0;;;}AnimationKey{2;2;0;3;0,0,1.221802;;,29;3;0,0,1.221802;;;}}Animation{{Armature_elbow_l}AnimationKey{0;16;0;4;0.995195,-0.034868,-0.015799,-0.090119;;,1;4;0.993465,-0.046368,-0.030155,-0.099838;;,3;4;0.983557,-0.0879,-0.082099,-0.134715;;,5;4;0.959324,-0.146904,-0.156177,-0.183648;;,7;4;0.917546,-0.212233,-0.238611,-0.236921;;,9;4;0.864109,-0.271657,-0.314022,-0.284443;;,11;4;0.813172,-0.315829,-0.370387,-0.319087;;,13;4;0.781004,-0.339668,-0.400938,-0.337501;;,15;4;0.781004,-0.339668,-0.400938,-0.337501;;,17;4;0.813172,-0.315829,-0.370387,-0.319087;;,19;4;0.864109,-0.271657,-0.314022,-0.284443;;,21;4;0.917546,-0.212233,-0.238611,-0.236921;;,23;4;0.959324,-0.146904,-0.156177,-0.183648;;,25;4;0.983557,-0.0879,-0.082099,-0.134715;;,27;4;0.993465,-0.046368,-0.030155,-0.099838;;,29;4;0.995701,-0.030812,-0.010739,-0.086685;;;}AnimationKey{2;2;0;3;0,0,0.754892;;,29;3;0,0,0.754892;;;}}Animation{{Armature_body}AnimationKey{0;16;0;4;-0,0,0.601298,0.799025;;,1;4;-0,0,0.608144,0.793827;;,3;4;-0,0,0.627465,0.778645;;,5;4;-0,0,0.643183,0.765712;;,7;4;-0,0,0.643755,0.765231;;,9;4;-0,0,0.631076,0.775721;;,11;4;-0,0,0.613775,0.789481;;,13;4;-0,0,0.6007,0.799474;;,15;4;-0,0,0.601488,0.798882;;,17;4;-0,0,0.619499,0.784997;;,19;4;-0,0,0.643196,0.765702;;,21;4;-0,0,0.660441,0.750878;;,23;4;-0,0,0.659666,0.751559;;,25;4;-0,0,0.638264,0.769817;;,27;4;-0,0,0.611752,0.791049;;,29;4;-0,0,0.598631,0.801025;;;}AnimationKey{2;2;0;3;0,2.580534,0;;,29;3;0,2.571201,0;;;}}Animation{{Armature_leg_l}AnimationKey{0;16;0;4;0.390287,0.920693,0,0;;,1;4;0.362565,0.931959,0,0;;,3;4;0.266163,0.963928,0,0;;,5;4;0.138294,0.990391,0,0;;,7;4;0.012725,0.999919,0,0;;,9;4;-0.090194,0.995924,0,0;;,11;4;-0.162502,0.986708,0,0;;,13;4;-0.201466,0.979496,0,0;;,15;4;-0.185641,0.982618,0,0;;,17;4;-0.013697,0.999906,0,0;;,19;4;0.24238,0.970181,0,0;;,21;4;0.417271,0.908782,0,0;;,23;4;0.439308,0.898336,0,0;;,25;4;0.424255,0.905543,0,0;;,27;4;0.407664,0.913132,0,0;;,29;4;0.400263,0.9164,0,0;;;}AnimationKey{2;2;0;3;0.246294,0,-0.171352;;,29;3;0.246294,0,-0.171351;;;}}Animation{{Armature_leg_r}AnimationKey{0;16;0;4;0.174933,-0.98458,0,0;;,1;4;0.082829,-0.996564,0,0;;,3;4;-0.21147,-0.977384,0,0;;,5;4;-0.442802,-0.89662,0,0;;,7;4;-0.47604,-0.879424,0,0;;,9;4;-0.47279,-0.881175,0,0;;,11;4;-0.459567,-0.888143,0,0;;,13;4;-0.427425,-0.904051,0,0;;,15;4;-0.361724,-0.932285,0,0;;,17;4;-0.251362,-0.967893,0,0;;,19;4;-0.114531,-0.99342,0,0;;,21;4;0.021053,-0.999778,0,0;;,23;4;0.12473,-0.992191,0,0;;,25;4;0.181473,-0.983396,0,0;;,27;4;0.204037,-0.978963,0,0;;,29;4;0.208187,-0.978089,0,0;;;}AnimationKey{2;2;0;3;-0.246294,0,-0.171352;;,29;3;-0.246294,0,-0.171351;;;}}Animation{{Armature_arm_l}AnimationKey{0;16;0;4;0.200754,-0.659656,-0.716264,-0.107316;;,1;4;0.192268,-0.660735,-0.716526,-0.114246;;,3;4;0.161871,-0.663925,-0.716753,-0.138802;;,5;4;0.118745,-0.666682,-0.715211,-0.17294;;,7;4;0.069733,-0.667364,-0.710872,-0.210767;;,9;4;0.022313,-0.665594,-0.704111,-0.246404;;,11;4;-0.016046,-0.662426,-0.696821,-0.274543;;,13;4;-0.038374,-0.659874,-0.691824,-0.290643;;,15;4;-0.038373,-0.659874,-0.691824,-0.290643;;,17;4;-0.016044,-0.662427,-0.696822,-0.274543;;,19;4;0.022312,-0.665594,-0.70411,-0.246404;;,21;4;0.069733,-0.667365,-0.710872,-0.210767;;,23;4;0.118745,-0.666682,-0.715211,-0.17294;;,25;4;0.161871,-0.663925,-0.716753,-0.138802;;,27;4;0.192268,-0.660735,-0.716526,-0.114246;;,29;4;0.203757,-0.659255,-0.716151,-0.104856;;;}AnimationKey{2;2;0;3;0.545315,0,1;;,29;3;0.545315,0,1;;;}}}Frame Root{FrameTransformMatrix{1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1;;}Frame Armature{FrameTransformMatrix{1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1;;}Frame Armature_body{FrameTransformMatrix{-1,0,0,0,0,0,1,0,0,1,0,0,0,2.571201,0,1;;}Frame Armature_arm_r{FrameTransformMatrix{-0.047733,0.997488,-0.05233,0,0.901521,0.020464,-0.432251,0,-0.430095,-0.067809,-0.900233,0,-0.545315,0,1,1;;}Frame Armature_elbow_r{FrameTransformMatrix{0.987983,0.151721,-0.029519,0,-0.153228,0.986478,-0.058162,0,0.020295,0.061987,0.997871,0,0,0,0.754892,1;;}}}Frame Armature_arm_l{FrameTransformMatrix{-0.047732,0.994072,-0.097683,0,0.901521,0.084983,0.424309,0,0.430095,-0.067809,-0.900233,0,0.545315,0,1,1;;}Frame Armature_elbow_l{FrameTransformMatrix{0.984741,0.173286,-0.016044,0,-0.171963,0.983073,0.063221,0,0.026727,-0.059497,0.99787,0,0,0,0.754892,1;;}}}Frame Armature_leg_l{FrameTransformMatrix{1,0,0,0,0,-0.998426,-0.056453,0,0,0.056453,-0.998405,0,0.246294,0,-0.171351,1;;}Frame Armature_knee_l{FrameTransformMatrix{1,0,0,0,0,0.993861,-0.110639,0,0,0.110639,0.993861,0,0,0,1.10139,1;;}}}Frame Armature_leg_r{FrameTransformMatrix{1,0,0,0,0,-0.998426,-0.056453,0,0,0.056453,-0.998405,0,-0.246294,0,-0.171351,1;;}Frame Armature_knee_r{FrameTransformMatrix{1,0,0,0,0,0.993861,-0.110639,0,0,0.110639,0.993861,0,0,0,1.10139,1;;}}}Frame Armature_Bone_007{FrameTransformMatrix{1,0,0,0,0,1,0,0,0,0,1,0,0,0,1.221802,1;;}}}Frame cool_dude{FrameTransformMatrix{-1,0,0,0,0,1,0,0,0,0,-1,0,0,0,0,1;;}Mesh{272;0;2.440814;0.219926;,0;3.688199;0.219926;,0.466212;3.688199;0.219926;,0.466212;2.440814;0.219926;,0.466212;2.440814;0.219926;,0.466212;3.688199;0.219926;,0.466212;3.688199;-0.219926;,0.466212;2.440814;-0.219926;,0;2.440814;0.219926;,0.466212;2.440814;0.219926;,0.466212;2.440814;-0.219926;,0;2.440814;-0.219926;,0.055633;1.27575;-0.190081;,0.055633;2.35741;-0.190081;,0.055633;2.35741;0.190081;,0.055633;1.27575;0.190081;,0.055633;1.27575;0.190081;,0.055633;2.35741;0.190081;,0.43017;2.35741;0.190081;,0.43017;1.27575;0.190081;,0.43017;1.27575;0.190081;,0.43017;2.35741;0.190081;,0.43017;2.35741;-0.190081;,0.43017;1.27575;-0.190081;,0.43017;1.27575;-0.190081;,0.43017;2.35741;-0.190081;,0.055633;2.35741;-0.190081;,0.055633;1.27575;-0.190081;,0.055633;1.27575;0.190081;,0.43017;1.27575;0.190081;,0.43017;1.27575;-0.190081;,0.055633;1.27575;-0.190081;,0.43017;2.35741;0.190081;,0.055633;2.35741;0.190081;,0.055633;2.35741;-0.190081;,0.43017;2.35741;-0.190081;,0.466212;3.688199;0.219926;,0;3.688199;0.219926;,0;3.688199;-0.219926;,0.466212;3.688199;-0.219926;,0.466212;2.440814;-0.219926;,0.466212;3.688199;-0.219926;,0;3.688199;-0.219926;,0;2.440814;-0.219926;,0.769341;2.834949;-0.041122;,0.440953;3.555781;-0.041122;,0.440953;3.555781;0.207294;,0.769341;2.834949;0.207294;,0.769341;2.834949;0.207294;,0.440953;3.555781;0.207294;,0.616273;3.635651;0.207294;,0.944661;2.914819;0.207294;,0.944661;2.914819;0.207294;,0.616273;3.635651;0.207294;,0.616273;3.635651;-0.041122;,0.944661;2.914819;-0.041122;,0.944661;2.914819;-0.041122;,0.616273;3.635651;-0.041122;,0.440953;3.555781;-0.041122;,0.769341;2.834949;-0.041122;,0.769341;2.834949;0.207294;,0.944661;2.914819;0.207294;,0.944661;2.914819;-0.041122;,0.769341;2.834949;-0.041122;,0.616273;3.635651;0.207294;,0.440953;3.555781;0.207294;,0.440953;3.555781;-0.041122;,0.616273;3.635651;-0.041122;,1.104504;2.080977;-0.086788;,0.776116;2.801809;-0.086788;,0.776116;2.801809;0.161627;,1.104504;2.080977;0.161627;,1.104504;2.080977;0.161627;,0.776116;2.801809;0.161627;,0.951436;2.881679;0.161627;,1.279824;2.160847;0.161627;,1.279824;2.160847;0.161627;,0.951436;2.881679;0.161627;,0.951436;2.881679;-0.086788;,1.279824;2.160847;-0.086788;,1.279824;2.160847;-0.086788;,0.951436;2.881679;-0.086788;,0.776116;2.801809;-0.086788;,1.104504;2.080977;-0.086788;,1.104504;2.080977;0.161627;,1.279824;2.160847;0.161627;,1.279824;2.160847;-0.086788;,1.104504;2.080977;-0.086788;,0.951436;2.881679;0.161627;,0.776116;2.801809;0.161627;,0.776116;2.801809;-0.086788;,0.951436;2.881679;-0.086788;,0.055633;0.093601;-0.190081;,0.055633;1.205294;-0.190081;,0.055633;1.205294;0.190081;,0.055633;0.093601;0.190081;,0.055633;0.093601;0.190081;,0.055633;1.205294;0.190081;,0.43017;1.205294;0.190081;,0.43017;0.093601;0.190081;,0.43017;0.093601;0.190081;,0.43017;1.205294;0.190081;,0.43017;1.205294;-0.190081;,0.43017;0.093601;-0.190081;,0.43017;0.093601;-0.190081;,0.43017;1.205294;-0.190081;,0.055633;1.205294;-0.190081;,0.055633;0.093601;-0.190081;,0.055633;0.093601;0.190081;,0.43017;0.093601;0.190081;,0.43017;0.093601;-0.190081;,0.055633;0.093601;-0.190081;,0.43017;1.205294;0.190081;,0.055633;1.205294;0.190081;,0.055633;1.205294;-0.190081;,0.43017;1.205294;-0.190081;,0;3.790919;0.428464;,0;4.579204;0.428464;,0.43344;4.560537;0.409797;,0.43344;3.809586;0.409797;,0.43344;3.809586;0.409797;,0.43344;4.560537;0.409797;,0.43344;4.560537;-0.284975;,0.43344;3.809586;-0.284975;,0;3.790919;0.428464;,0.43344;3.809586;0.409797;,0.43344;3.809586;-0.284975;,0;3.790919;-0.303642;,0.43344;4.560537;0.409797;,0;4.579204;0.428464;,0;4.579204;-0.303642;,0.43344;4.560537;-0.284975;,0.43344;3.809586;-0.284975;,0.43344;4.560537;-0.284975;,0;4.579204;-0.303642;,0;3.790919;-0.303642;,0;2.440814;0.219926;,-0.466212;2.440814;0.219926;,-0.466212;3.688199;0.219926;,0;3.688199;0.219926;,-0.466212;2.440814;0.219926;,-0.466212;2.440814;-0.219926;,-0.466212;3.688199;-0.219926;,-0.466212;3.688199;0.219926;,0;2.440814;0.219926;,0;2.440814;-0.219926;,-0.466212;2.440814;-0.219926;,-0.466212;2.440814;0.219926;,-0.055633;1.27575;-0.190081;,-0.055633;1.27575;0.190081;,-0.055633;2.35741;0.190081;,-0.055633;2.35741;-0.190081;,-0.055633;1.27575;0.190081;,-0.43017;1.27575;0.190081;,-0.43017;2.35741;0.190081;,-0.055633;2.35741;0.190081;,-0.43017;1.27575;0.190081;,-0.43017;1.27575;-0.190081;,-0.43017;2.35741;-0.190081;,-0.43017;2.35741;0.190081;,-0.43017;1.27575;-0.190081;,-0.055633;1.27575;-0.190081;,-0.055633;2.35741;-0.190081;,-0.43017;2.35741;-0.190081;,-0.055633;1.27575;0.190081;,-0.055633;1.27575;-0.190081;,-0.43017;1.27575;-0.190081;,-0.43017;1.27575;0.190081;,-0.43017;2.35741;0.190081;,-0.43017;2.35741;-0.190081;,-0.055633;2.35741;-0.190081;,-0.055633;2.35741;0.190081;,-0.466212;3.688199;0.219926;,-0.466212;3.688199;-0.219926;,0;3.688199;-0.219926;,0;3.688199;0.219926;,-0.466212;2.440814;-0.219926;,0;2.440814;-0.219926;,0;3.688199;-0.219926;,-0.466212;3.688199;-0.219926;,-0.769341;2.834949;-0.041122;,-0.769341;2.834949;0.207294;,-0.440953;3.555781;0.207294;,-0.440953;3.555781;-0.041122;,-0.769341;2.834949;0.207294;,-0.944661;2.914819;0.207294;,-0.616273;3.635651;0.207294;,-0.440953;3.555781;0.207294;,-0.944661;2.914819;0.207294;,-0.944661;2.914819;-0.041122;,-0.616273;3.635651;-0.041122;,-0.616273;3.635651;0.207294;,-0.944661;2.914819;-0.041122;,-0.769341;2.834949;-0.041122;,-0.440953;3.555781;-0.041122;,-0.616273;3.635651;-0.041122;,-0.769341;2.834949;0.207294;,-0.769341;2.834949;-0.041122;,-0.944661;2.914819;-0.041122;,-0.944661;2.914819;0.207294;,-0.616273;3.635651;0.207294;,-0.616273;3.635651;-0.041122;,-0.440953;3.555781;-0.041122;,-0.440953;3.555781;0.207294;,-1.104504;2.080977;-0.086788;,-1.104504;2.080977;0.161627;,-0.776116;2.801809;0.161627;,-0.776116;2.801809;-0.086788;,-1.104504;2.080977;0.161627;,-1.279824;2.160847;0.161627;,-0.951436;2.881679;0.161627;,-0.776116;2.801809;0.161627;,-1.279824;2.160847;0.161627;,-1.279824;2.160847;-0.086788;,-0.951436;2.881679;-0.086788;,-0.951436;2.881679;0.161627;,-1.279824;2.160847;-0.086788;,-1.104504;2.080977;-0.086788;,-0.776116;2.801809;-0.086788;,-0.951436;2.881679;-0.086788;,-1.104504;2.080977;0.161627;,-1.104504;2.080977;-0.086788;,-1.279824;2.160847;-0.086788;,-1.279824;2.160847;0.161627;,-0.951436;2.881679;0.161627;,-0.951436;2.881679;-0.086788;,-0.776116;2.801809;-0.086788;,-0.776116;2.801809;0.161627;,-0.055633;0.093601;-0.190081;,-0.055633;0.093601;0.190081;,-0.055633;1.205294;0.190081;,-0.055633;1.205294;-0.190081;,-0.055633;0.093601;0.190081;,-0.43017;0.093601;0.190081;,-0.43017;1.205294;0.190081;,-0.055633;1.205294;0.190081;,-0.43017;0.093601;0.190081;,-0.43017;0.093601;-0.190081;,-0.43017;1.205294;-0.190081;,-0.43017;1.205294;0.190081;,-0.43017;0.093601;-0.190081;,-0.055633;0.093601;-0.190081;,-0.055633;1.205294;-0.190081;,-0.43017;1.205294;-0.190081;,-0.055633;0.093601;0.190081;,-0.055633;0.093601;-0.190081;,-0.43017;0.093601;-0.190081;,-0.43017;0.093601;0.190081;,-0.43017;1.205294;0.190081;,-0.43017;1.205294;-0.190081;,-0.055633;1.205294;-0.190081;,-0.055633;1.205294;0.190081;,0;3.790919;0.428464;,-0.43344;3.809586;0.409797;,-0.43344;4.560537;0.409797;,0;4.579204;0.428464;,-0.43344;3.809586;0.409797;,-0.43344;3.809586;-0.284975;,-0.43344;4.560537;-0.284975;,-0.43344;4.560537;0.409797;,0;3.790919;0.428464;,0;3.790919;-0.303642;,-0.43344;3.809586;-0.284975;,-0.43344;3.809586;0.409797;,-0.43344;4.560537;0.409797;,-0.43344;4.560537;-0.284975;,0;4.579204;-0.303642;,0;4.579204;0.428464;,-0.43344;3.809586;-0.284975;,0;3.790919;-0.303642;,0;4.579204;-0.303642;,-0.43344;4.560537;-0.284975;;68;4;3,2,1,0;,4;7,6,5,4;,4;11,10,9,8;,4;15,14,13,12;,4;19,18,17,16;,4;23,22,21,20;,4;27,26,25,24;,4;31,30,29,28;,4;35,34,33,32;,4;39,38,37,36;,4;43,42,41,40;,4;47,46,45,44;,4;51,50,49,48;,4;55,54,53,52;,4;59,58,57,56;,4;63,62,61,60;,4;67,66,65,64;,4;71,70,69,68;,4;75,74,73,72;,4;79,78,77,76;,4;83,82,81,80;,4;87,86,85,84;,4;91,90,89,88;,4;95,94,93,92;,4;99,98,97,96;,4;103,102,101,100;,4;107,106,105,104;,4;111,110,109,108;,4;115,114,113,112;,4;119,118,117,116;,4;123,122,121,120;,4;127,126,125,124;,4;131,130,129,128;,4;135,134,133,132;,4;139,138,137,136;,4;143,142,141,140;,4;147,146,145,144;,4;151,150,149,148;,4;155,154,153,152;,4;159,158,157,156;,4;163,162,161,160;,4;167,166,165,164;,4;171,170,169,168;,4;175,174,173,172;,4;179,178,177,176;,4;183,182,181,180;,4;187,186,185,184;,4;191,190,189,188;,4;195,194,193,192;,4;199,198,197,196;,4;203,202,201,200;,4;207,206,205,204;,4;211,210,209,208;,4;215,214,213,212;,4;219,218,217,216;,4;223,222,221,220;,4;227,226,225,224;,4;231,230,229,228;,4;235,234,233,232;,4;239,238,237,236;,4;243,242,241,240;,4;247,246,245,244;,4;251,250,249,248;,4;255,254,253,252;,4;259,258,257,256;,4;263,262,261,260;,4;267,266,265,264;,4;271,270,269,268;;MeshNormals{272;0;-0.707083;0.707083;,0;0.707083;0.707083;,0.577349;0.577349;0.577349;,0.577349;-0.577349;0.577349;,0.577349;-0.577349;0.577349;,0.577349;0.577349;0.577349;,0.577349;0.577349;-0.577349;,0.577349;-0.577349;-0.577349;,0;-0.707083;0.707083;,0.577349;-0.577349;0.577349;,0.577349;-0.577349;-0.577349;,0;-0.707083;-0.707083;,-0.577349;-0.577349;-0.577349;,-0.577349;0.577349;-0.577349;,-0.577349;0.577349;0.577349;,-0.577349;-0.577349;0.577349;,-0.577349;-0.577349;0.577349;,-0.577349;0.577349;0.577349;,0.577349;0.577349;0.577349;,0.577349;-0.577349;0.577349;,0.577349;-0.577349;0.577349;,0.577349;0.577349;0.577349;,0.577349;0.577349;-0.577349;,0.577349;-0.577349;-0.577349;,0.577349;-0.577349;-0.577349;,0.577349;0.577349;-0.577349;,-0.577349;0.577349;-0.577349;,-0.577349;-0.577349;-0.577349;,-0.577349;-0.577349;0.577349;,0.577349;-0.577349;0.577349;,0.577349;-0.577349;-0.577349;,-0.577349;-0.577349;-0.577349;,0.577349;0.577349;0.577349;,-0.577349;0.577349;0.577349;,-0.577349;0.577349;-0.577349;,0.577349;0.577349;-0.577349;,0.577349;0.577349;0.577349;,0;0.707083;0.707083;,0;0.707083;-0.707083;,0.577349;0.577349;-0.577349;,0.577349;-0.577349;-0.577349;,0.577349;0.577349;-0.577349;,0;0.707083;-0.707083;,0;-0.707083;-0.707083;,-0.286019;-0.764733;-0.577349;,-0.764733;0.286019;-0.577349;,-0.764733;0.286019;0.577349;,-0.286019;-0.764733;0.577349;,-0.286019;-0.764733;0.577349;,-0.764733;0.286019;0.577349;,0.286019;0.764733;0.577349;,0.764733;-0.286019;0.577349;,0.764733;-0.286019;0.577349;,0.286019;0.764733;0.577349;,0.286019;0.764733;-0.577349;,0.764733;-0.286019;-0.577349;,0.764733;-0.286019;-0.577349;,0.286019;0.764733;-0.577349;,-0.764733;0.286019;-0.577349;,-0.286019;-0.764733;-0.577349;,-0.286019;-0.764733;0.577349;,0.764733;-0.286019;0.577349;,0.764733;-0.286019;-0.577349;,-0.286019;-0.764733;-0.577349;,0.286019;0.764733;0.577349;,-0.764733;0.286019;0.577349;,-0.764733;0.286019;-0.577349;,0.286019;0.764733;-0.577349;,-0.286019;-0.764733;-0.577349;,-0.764733;0.286019;-0.577349;,-0.764733;0.286019;0.577349;,-0.286019;-0.764733;0.577349;,-0.286019;-0.764733;0.577349;,-0.764733;0.286019;0.577349;,0.286019;0.764733;0.577349;,0.764733;-0.286019;0.577349;,0.764733;-0.286019;0.577349;,0.286019;0.764733;0.577349;,0.286019;0.764733;-0.577349;,0.764733;-0.286019;-0.577349;,0.764733;-0.286019;-0.577349;,0.286019;0.764733;-0.577349;,-0.764733;0.286019;-0.577349;,-0.286019;-0.764733;-0.577349;,-0.286019;-0.764733;0.577349;,0.764733;-0.286019;0.577349;,0.764733;-0.286019;-0.577349;,-0.286019;-0.764733;-0.577349;,0.286019;0.764733;0.577349;,-0.764733;0.286019;0.577349;,-0.764733;0.286019;-0.577349;,0.286019;0.764733;-0.577349;,-0.577349;-0.577349;-0.577349;,-0.577349;0.577349;-0.577349;,-0.577349;0.577349;0.577349;,-0.577349;-0.577349;0.577349;,-0.577349;-0.577349;0.577349;,-0.577349;0.577349;0.577349;,0.577349;0.577349;0.577349;,0.577349;-0.577349;0.577349;,0.577349;-0.577349;0.577349;,0.577349;0.577349;0.577349;,0.577349;0.577349;-0.577349;,0.577349;-0.577349;-0.577349;,0.577349;-0.577349;-0.577349;,0.577349;0.577349;-0.577349;,-0.577349;0.577349;-0.577349;,-0.577349;-0.577349;-0.577349;,-0.577349;-0.577349;0.577349;,0.577349;-0.577349;0.577349;,0.577349;-0.577349;-0.577349;,-0.577349;-0.577349;-0.577349;,0.577349;0.577349;0.577349;,-0.577349;0.577349;0.577349;,-0.577349;0.577349;-0.577349;,0.577349;0.577349;-0.577349;,0;-0.707083;0.707083;,0;0.707083;0.707083;,0.599902;0.565722;0.565722;,0.599902;-0.565722;0.565722;,0.599902;-0.565722;0.565722;,0.599902;0.565722;0.565722;,0.599902;0.565722;-0.565722;,0.599902;-0.565722;-0.565722;,0;-0.707083;0.707083;,0.599902;-0.565722;0.565722;,0.599902;-0.565722;-0.565722;,0;-0.707083;-0.707083;,0.599902;0.565722;0.565722;,0;0.707083;0.707083;,0;0.707083;-0.707083;,0.599902;0.565722;-0.565722;,0.599902;-0.565722;-0.565722;,0.599902;0.565722;-0.565722;,0;0.707083;-0.707083;,0;-0.707083;-0.707083;,0;-0.707083;0.707083;,-0.577349;-0.577349;0.577349;,-0.577349;0.577349;0.577349;,0;0.707083;0.707083;,-0.577349;-0.577349;0.577349;,-0.577349;-0.577349;-0.577349;,-0.577349;0.577349;-0.577349;,-0.577349;0.577349;0.577349;,0;-0.707083;0.707083;,0;-0.707083;-0.707083;,-0.577349;-0.577349;-0.577349;,-0.577349;-0.577349;0.577349;,0.577349;-0.577349;-0.577349;,0.577349;-0.577349;0.577349;,0.577349;0.577349;0.577349;,0.577349;0.577349;-0.577349;,0.577349;-0.577349;0.577349;,-0.577349;-0.577349;0.577349;,-0.577349;0.577349;0.577349;,0.577349;0.577349;0.577349;,-0.577349;-0.577349;0.577349;,-0.577349;-0.577349;-0.577349;,-0.577349;0.577349;-0.577349;,-0.577349;0.577349;0.577349;,-0.577349;-0.577349;-0.577349;,0.577349;-0.577349;-0.577349;,0.577349;0.577349;-0.577349;,-0.577349;0.577349;-0.577349;,0.577349;-0.577349;0.577349;,0.577349;-0.577349;-0.577349;,-0.577349;-0.577349;-0.577349;,-0.577349;-0.577349;0.577349;,-0.577349;0.577349;0.577349;,-0.577349;0.577349;-0.577349;,0.577349;0.577349;-0.577349;,0.577349;0.577349;0.577349;,-0.577349;0.577349;0.577349;,-0.577349;0.577349;-0.577349;,0;0.707083;-0.707083;,0;0.707083;0.707083;,-0.577349;-0.577349;-0.577349;,0;-0.707083;-0.707083;,0;0.707083;-0.707083;,-0.577349;0.577349;-0.577349;,0.286019;-0.764733;-0.577349;,0.286019;-0.764733;0.577349;,0.764733;0.286019;0.577349;,0.764733;0.286019;-0.577349;,0.286019;-0.764733;0.577349;,-0.764733;-0.286019;0.577349;,-0.286019;0.764733;0.577349;,0.764733;0.286019;0.577349;,-0.764733;-0.286019;0.577349;,-0.764733;-0.286019;-0.577349;,-0.286019;0.764733;-0.577349;,-0.286019;0.764733;0.577349;,-0.764733;-0.286019;-0.577349;,0.286019;-0.764733;-0.577349;,0.764733;0.286019;-0.577349;,-0.286019;0.764733;-0.577349;,0.286019;-0.764733;0.577349;,0.286019;-0.764733;-0.577349;,-0.764733;-0.286019;-0.577349;,-0.764733;-0.286019;0.577349;,-0.286019;0.764733;0.577349;,-0.286019;0.764733;-0.577349;,0.764733;0.286019;-0.577349;,0.764733;0.286019;0.577349;,0.286019;-0.764733;-0.577349;,0.286019;-0.764733;0.577349;,0.764733;0.286019;0.577349;,0.764733;0.286019;-0.577349;,0.286019;-0.764733;0.577349;,-0.764733;-0.286019;0.577349;,-0.286019;0.764733;0.577349;,0.764733;0.286019;0.577349;,-0.764733;-0.286019;0.577349;,-0.764733;-0.286019;-0.577349;,-0.286019;0.764733;-0.577349;,-0.286019;0.764733;0.577349;,-0.764733;-0.286019;-0.577349;,0.286019;-0.764733;-0.577349;,0.764733;0.286019;-0.577349;,-0.286019;0.764733;-0.577349;,0.286019;-0.764733;0.577349;,0.286019;-0.764733;-0.577349;,-0.764733;-0.286019;-0.577349;,-0.764733;-0.286019;0.577349;,-0.286019;0.764733;0.577349;,-0.286019;0.764733;-0.577349;,0.764733;0.286019;-0.577349;,0.764733;0.286019;0.577349;,0.577349;-0.577349;-0.577349;,0.577349;-0.577349;0.577349;,0.577349;0.577349;0.577349;,0.577349;0.577349;-0.577349;,0.577349;-0.577349;0.577349;,-0.577349;-0.577349;0.577349;,-0.577349;0.577349;0.577349;,0.577349;0.577349;0.577349;,-0.577349;-0.577349;0.577349;,-0.577349;-0.577349;-0.577349;,-0.577349;0.577349;-0.577349;,-0.577349;0.577349;0.577349;,-0.577349;-0.577349;-0.577349;,0.577349;-0.577349;-0.577349;,0.577349;0.577349;-0.577349;,-0.577349;0.577349;-0.577349;,0.577349;-0.577349;0.577349;,0.577349;-0.577349;-0.577349;,-0.577349;-0.577349;-0.577349;,-0.577349;-0.577349;0.577349;,-0.577349;0.577349;0.577349;,-0.577349;0.577349;-0.577349;,0.577349;0.577349;-0.577349;,0.577349;0.577349;0.577349;,0;-0.707083;0.707083;,-0.599902;-0.565722;0.565722;,-0.599872;0.565722;0.565722;,0;0.707083;0.707083;,-0.599902;-0.565722;0.565722;,-0.599902;-0.565722;-0.565722;,-0.599872;0.565722;-0.565722;,-0.599872;0.565722;0.565722;,0;-0.707083;0.707083;,0;-0.707083;-0.707083;,-0.599902;-0.565722;-0.565722;,-0.599902;-0.565722;0.565722;,-0.599872;0.565722;0.565722;,-0.599872;0.565722;-0.565722;,0;0.707083;-0.707083;,0;0.707083;0.707083;,-0.599902;-0.565722;-0.565722;,0;-0.707083;-0.707083;,0;0.707083;-0.707083;,-0.599872;0.565722;-0.565722;;68;4;3,2,1,0;,4;7,6,5,4;,4;11,10,9,8;,4;15,14,13,12;,4;19,18,17,16;,4;23,22,21,20;,4;27,26,25,24;,4;31,30,29,28;,4;35,34,33,32;,4;39,38,37,36;,4;43,42,41,40;,4;47,46,45,44;,4;51,50,49,48;,4;55,54,53,52;,4;59,58,57,56;,4;63,62,61,60;,4;67,66,65,64;,4;71,70,69,68;,4;75,74,73,72;,4;79,78,77,76;,4;83,82,81,80;,4;87,86,85,84;,4;91,90,89,88;,4;95,94,93,92;,4;99,98,97,96;,4;103,102,101,100;,4;107,106,105,104;,4;111,110,109,108;,4;115,114,113,112;,4;119,118,117,116;,4;123,122,121,120;,4;127,126,125,124;,4;131,130,129,128;,4;135,134,133,132;,4;139,138,137,136;,4;143,142,141,140;,4;147,146,145,144;,4;151,150,149,148;,4;155,154,153,152;,4;159,158,157,156;,4;163,162,161,160;,4;167,166,165,164;,4;171,170,169,168;,4;175,174,173,172;,4;179,178,177,176;,4;183,182,181,180;,4;187,186,185,184;,4;191,190,189,188;,4;195,194,193,192;,4;199,198,197,196;,4;203,202,201,200;,4;207,206,205,204;,4;211,210,209,208;,4;215,214,213,212;,4;219,218,217,216;,4;223,222,221,220;,4;227,226,225,224;,4;231,230,229,228;,4;235,234,233,232;,4;239,238,237,236;,4;243,242,241,240;,4;247,246,245,244;,4;251,250,249,248;,4;255,254,253,252;,4;259,258,257,256;,4;263,262,261,260;,4;267,266,265,264;,4;271,270,269,268;;}MeshTextureCoords{272;0.849264;0.899246;,0.849264;0.931916;,0.861547;0.931916;,0.861547;0.899246;,0.916988;0.931916;,0.916988;0.899246;,0.9054;0.899246;,0.9054;0.931916;,0.84857;0.844707;,0.84857;0.83254;,0.836981;0.83254;,0.836981;0.844707;,0.927004;0.903587;,0.927004;0.931916;,0.937019;0.931916;,0.937019;0.903587;,0.937019;0.903587;,0.937019;0.931916;,0.946887;0.931916;,0.946887;0.903587;,0.888533;0.856954;,0.888533;0.828625;,0.878517;0.828625;,0.878517;0.856954;,0.939292;0.870917;,0.939292;0.899246;,0.949159;0.899246;,0.949159;0.870917;,0.946887;0.91117;,0.956719;0.91117;,0.956719;0.901213;,0.946887;0.901213;,0.865118;0.813135;,0.855286;0.813135;,0.855286;0.823092;,0.865118;0.823092;,0.866874;0.847426;,0.866874;0.835259;,0.855286;0.835259;,0.855286;0.847426;,0.598002;0.973516;,0.598002;0.206739;,0.309722;0.206739;,0.309722;0.973516;,0.909393;0.822135;,0.909393;0.841014;,0.915938;0.841014;,0.915938;0.822135;,0.951962;0.931916;,0.951962;0.91117;,0.946887;0.91117;,0.946887;0.931916;,0.948762;0.841801;,0.948762;0.822921;,0.942217;0.822921;,0.942217;0.841801;,0.893608;0.838075;,0.893608;0.817329;,0.888533;0.817329;,0.888533;0.838075;,0.900724;0.909292;,0.90515;0.909292;,0.90515;0.902786;,0.900724;0.902786;,0.953585;0.871994;,0.949159;0.871994;,0.949159;0.8785;,0.953585;0.8785;,0.84857;0.837995;,0.84857;0.856874;,0.855114;0.856874;,0.855114;0.837995;,0.902881;0.83746;,0.902881;0.816714;,0.897805;0.816714;,0.897805;0.83746;,0.942217;0.841801;,0.942217;0.822921;,0.935673;0.822921;,0.935673;0.841801;,0.949159;0.8785;,0.949159;0.899246;,0.954235;0.899246;,0.954235;0.8785;,0.919226;0.822135;,0.923651;0.822135;,0.923651;0.815629;,0.919226;0.815629;,0.928077;0.815629;,0.923651;0.815629;,0.923651;0.822135;,0.928077;0.822135;,0.865301;0.847426;,0.865301;0.876542;,0.875317;0.876542;,0.875317;0.847426;,0.909393;0.841014;,0.909393;0.87013;,0.919261;0.87013;,0.919261;0.841014;,0.855286;0.847426;,0.855286;0.876542;,0.865301;0.876542;,0.865301;0.847426;,0.919261;0.841014;,0.919261;0.87013;,0.929128;0.87013;,0.929128;0.841014;,0.878517;0.828625;,0.888349;0.828625;,0.88835;0.818668;,0.878517;0.818668;,0.836981;0.83254;,0.846814;0.83254;,0.846814;0.822583;,0.836981;0.822583;,0.857749;0.887894;,0.836981;0.887894;,0.837473;0.899246;,0.857257;0.899246;,0.855286;0.876542;,0.855286;0.856874;,0.836981;0.856874;,0.836981;0.876542;,0.897805;0.887893;,0.897313;0.876622;,0.879009;0.876622;,0.878517;0.887893;,0.886604;0.909292;,0.886112;0.920645;,0.9054;0.920645;,0.904908;0.909292;,0.977665;0.442421;,0.977665;0.131438;,0.799225;0.123708;,0.799225;0.450151;,0.849264;0.899246;,0.836981;0.899246;,0.836981;0.931916;,0.849264;0.931916;,0.909393;0.866576;,0.897805;0.866576;,0.897805;0.899246;,0.909393;0.899246;,0.84857;0.844707;,0.836981;0.844707;,0.836981;0.856874;,0.84857;0.856874;,0.929276;0.899246;,0.939292;0.899246;,0.939292;0.870917;,0.929276;0.870917;,0.876741;0.819096;,0.866874;0.819096;,0.866874;0.847426;,0.876741;0.847426;,0.939144;0.841801;,0.929128;0.841801;,0.929128;0.87013;,0.939144;0.87013;,0.949011;0.841801;,0.939144;0.841801;,0.939144;0.87013;,0.949011;0.87013;,0.836981;0.812626;,0.836981;0.822583;,0.846814;0.822583;,0.846814;0.812626;,0.909393;0.812178;,0.909393;0.822135;,0.919226;0.822135;,0.919226;0.812178;,0.866874;0.823092;,0.855286;0.823092;,0.855286;0.835259;,0.866874;0.835259;,0.021442;0.973516;,0.309722;0.973516;,0.309722;0.206739;,0.021442;0.206739;,0.916039;0.841014;,0.922583;0.841014;,0.922583;0.822135;,0.916039;0.822135;,0.907956;0.816714;,0.902881;0.816714;,0.902881;0.83746;,0.907956;0.83746;,0.929128;0.822135;,0.922583;0.822135;,0.922583;0.841014;,0.929128;0.841014;,0.853645;0.817249;,0.84857;0.817249;,0.84857;0.837995;,0.853645;0.837995;,0.900724;0.909292;,0.900724;0.902786;,0.895944;0.902786;,0.895944;0.909292;,0.93896;0.816415;,0.93896;0.822921;,0.94374;0.822921;,0.94374;0.816415;,0.935673;0.822921;,0.929128;0.822921;,0.929128;0.841801;,0.935673;0.841801;,0.954087;0.849384;,0.949011;0.849384;,0.949011;0.87013;,0.954087;0.87013;,0.895077;0.838075;,0.888533;0.838075;,0.888533;0.856954;,0.895077;0.856954;,0.948762;0.841801;,0.953838;0.841801;,0.953838;0.821055;,0.948762;0.821055;,0.94374;0.816415;,0.94374;0.822921;,0.94852;0.822921;,0.94852;0.816415;,0.949011;0.842878;,0.949011;0.849384;,0.953791;0.849384;,0.953791;0.842878;,0.919409;0.87013;,0.909393;0.87013;,0.909393;0.899246;,0.919409;0.899246;,0.897805;0.866576;,0.907672;0.866576;,0.907672;0.83746;,0.897805;0.83746;,0.927004;0.9028;,0.916988;0.9028;,0.916988;0.931916;,0.927004;0.931916;,0.929276;0.87013;,0.919409;0.87013;,0.919409;0.899246;,0.929276;0.899246;,0.93896;0.822921;,0.93896;0.812965;,0.929128;0.812965;,0.929128;0.822921;,0.886112;0.899336;,0.886112;0.909292;,0.895944;0.909292;,0.895944;0.899336;,0.857749;0.887894;,0.857257;0.876542;,0.837473;0.876542;,0.836981;0.887894;,0.896821;0.856954;,0.878517;0.856954;,0.878517;0.876622;,0.896821;0.876622;,0.897805;0.887893;,0.878517;0.887893;,0.879009;0.899246;,0.897313;0.899246;,0.886604;0.931916;,0.904908;0.931916;,0.9054;0.920645;,0.886112;0.920645;,0.620785;0.44242;,0.799225;0.450151;,0.799225;0.123708;,0.620785;0.131438;;}XSkinMeshHeader{3;9;10;}SkinWeights{"Armature_arm_l";24;44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,67,66;1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1;-0.047733,0.901521,0.430095,0,-0.097683,0.424309,-0.900233,0,-0.994073,-0.084983,0.06781,0,0.374873,-2.006904,2.980378,1;;}SkinWeights{"Armature_elbow_r";24;216,219,218,213,212,215,214,209,224,208,227,211,226,210,206,221,207,220,204,223,205,222,225,217;1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1;0.102316,0.92166,-0.374266,0,-0.090709,-0.366028,-0.926173,0,-0.990608,0.128712,0.046152,0,0.402018,1.853661,2.350172,1;;}SkinWeights{"Armature_arm_r";24;186,187,184,185,182,183,180,194,195,203,202,192,193,201,200,199,190,198,191,197,188,196,189,181;1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1;-0.047733,0.901521,-0.430095,0,-0.05233,-0.432251,-0.900234,0,-0.997489,-0.020464,0.067809,0,0.160852,2.035269,2.980378,1;;}SkinWeights{"Armature_knee_l";24;105,99,114,106,98,115,107,101,93,108,100,92,109,103,95,110,102,94,111,97,112,104,113,96;1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1;1,0,0,0,0,0.054357,-0.998522,0,0,0.998501,0.054355,0,-0.246294,-0.008592,1.301673,1;;}SkinWeights{"Armature_Bone_007";40;132,133,134,135,124,125,126,252,253,254,255,121,122,264,265,123,267,268,269,270,116,256,258,259,260,261,262,263,271,266,120,119,117,128,129,127,130,118,131,257;1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1;1,0,0,0,0,0,1,0,0,-1,0,0,0,0,-3.793003,1;;}SkinWeights{"Armature_elbow_l";24;88,80,72,91,83,75,90,82,74,70,85,77,71,84,76,68,87,79,69,86,78,89,81,73;1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1;0.102316,0.92166,0.374266,0,-0.008222,0.377011,-0.926173,0,-0.994719,0.091686,0.046152,0,-0.014321,-1.896701,2.350171,1;;}SkinWeights{"Armature_knee_r";24;249,235,250,234,251,229,244,228,245,231,246,230,247,240,241,242,243,237,236,239,238,233,248,232;1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1;1,0,0,0,0,0.054357,-0.998522,0,0,0.998501,0.054355,0,0.246294,-0.008592,1.301673,1;;}SkinWeights{"Armature_leg_l";38;0,3,4,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,40,43,136,145,177,144;0.055873,0.852304,0.852304,0.82998,0.055873,0.852304,0.82998,0.054606,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.82998,0.054606,0.055873,0.054606,0.054606,0.055873;1,0,0,0,0,-0.056452,-0.998405,0,0,0.998385,-0.056452,0,-0.246294,0.135476,2.396023,1;;}SkinWeights{"Armature_leg_r";38;0,170,169,11,168,151,150,149,148,147,146,176,145,177,144,159,158,157,156,155,154,153,167,136,166,137,165,164,163,140,162,141,161,43,160,152,8,171;0.055873,1,1,0.054606,1,1,1,1,1,0.852304,0.82998,0.82998,0.054606,0.054606,0.055873,1,1,1,1,1,1,1,1,0.055873,1,0.852304,1,1,1,0.852304,1,0.82998,1,0.054606,1,1,0.055873,1;1,0,0,0,0,-0.056452,-0.998405,0,0,0.998385,-0.056452,0,0.246294,0.135476,2.396023,1;;}SkinWeights{"Armature_body";40;0,1,2,3,4,5,6,7,8,9,10,11,36,37,38,39,40,41,42,43,136,137,138,139,140,147,141,146,142,145,143,144,179,174,178,173,177,172,176,175;0.888255,1,1,0.147696,0.147696,1,1,0.17002,0.888255,0.147696,0.17002,0.890788,1,1,1,1,0.17002,1,1,0.890788,0.888255,0.147696,1,1,0.147696,0.147696,0.17002,0.17002,1,0.890788,1,0.888255,1,1,1,1,0.890788,1,0.17002,1;1,0,0,0,0,0,1,0,0,-1,0,0,0,0,-2.571201,1;;}}}}} \ No newline at end of file diff --git a/irr/media/cooltexture.png b/irr/media/cooltexture.png deleted file mode 100755 index fcc219ac7..000000000 Binary files a/irr/media/cooltexture.png and /dev/null differ diff --git a/irr/scripts/ci-build-android.sh b/irr/scripts/ci-build-android.sh deleted file mode 100755 index a865e56a4..000000000 --- a/irr/scripts/ci-build-android.sh +++ /dev/null @@ -1,123 +0,0 @@ -#!/bin/bash -e - -# NOTE: this code is mostly copied from minetest_android_deps -# - -png_ver=1.6.40 -jpeg_ver=3.0.1 - -download () { - get_tar_archive libpng "https://download.sourceforge.net/libpng/libpng-${png_ver}.tar.gz" - get_tar_archive libjpeg "https://download.sourceforge.net/libjpeg-turbo/libjpeg-turbo-${jpeg_ver}.tar.gz" -} - -build () { - # Build libjpg and libpng first because Irrlicht needs them - mkdir -p libpng - pushd libpng - $srcdir/libpng/configure --host=$CROSS_PREFIX - make && make DESTDIR=$PWD install - popd - - mkdir -p libjpeg - pushd libjpeg - cmake $srcdir/libjpeg "${CMAKE_FLAGS[@]}" -DENABLE_SHARED=OFF - make && make DESTDIR=$PWD install - popd - - local libpng=$PWD/libpng/usr/local/lib/libpng.a - local libjpeg=$(echo $PWD/libjpeg/opt/libjpeg-turbo/lib*/libjpeg.a) - cmake $srcdir/irrlicht "${CMAKE_FLAGS[@]}" \ - -DBUILD_SHARED_LIBS=OFF \ - -DPNG_LIBRARY=$libpng \ - -DPNG_PNG_INCLUDE_DIR=$(dirname "$libpng")/../include \ - -DJPEG_LIBRARY=$libjpeg \ - -DJPEG_INCLUDE_DIR=$(dirname "$libjpeg")/../include - make - - cp -p lib/Android/libIrrlichtMt.a $libpng $libjpeg $pkgdir/ - cp -a $srcdir/irrlicht/include $pkgdir/include - cp -a $srcdir/irrlicht/media/Shaders $pkgdir/Shaders -} - -get_tar_archive () { - # $1: folder to extract to, $2: URL - local filename="${2##*/}" - [ -d "$1" ] && return 0 - wget -c "$2" -O "$filename" - mkdir -p "$1" - tar -xaf "$filename" -C "$1" --strip-components=1 - rm "$filename" -} - -_setup_toolchain () { - local toolchain=$(echo "$ANDROID_NDK"/toolchains/llvm/prebuilt/*) - if [ ! -d "$toolchain" ]; then - echo "Android NDK path not specified or incorrect"; return 1 - fi - export PATH="$toolchain/bin:$ANDROID_NDK:$PATH" - - unset CFLAGS CPPFLAGS CXXFLAGS - - TARGET_ABI="$1" - API=21 - if [ "$TARGET_ABI" == armeabi-v7a ]; then - CROSS_PREFIX=armv7a-linux-androideabi - CFLAGS="-mthumb" - CXXFLAGS="-mthumb" - elif [ "$TARGET_ABI" == arm64-v8a ]; then - CROSS_PREFIX=aarch64-linux-android - elif [ "$TARGET_ABI" == x86 ]; then - CROSS_PREFIX=i686-linux-android - CFLAGS="-mssse3 -mfpmath=sse" - CXXFLAGS="-mssse3 -mfpmath=sse" - elif [ "$TARGET_ABI" == x86_64 ]; then - CROSS_PREFIX=x86_64-linux-android - else - echo "Invalid ABI given"; return 1 - fi - export CC=$CROSS_PREFIX$API-clang - export CXX=$CROSS_PREFIX$API-clang++ - export AR=llvm-ar - export RANLIB=llvm-ranlib - export CFLAGS="-fPIC ${CFLAGS}" - export CXXFLAGS="-fPIC ${CXXFLAGS}" - - CMAKE_FLAGS=( - "-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake" - "-DANDROID_ABI=$TARGET_ABI" "-DANDROID_NATIVE_API_LEVEL=$API" - "-DCMAKE_BUILD_TYPE=Release" - ) - - # make sure pkg-config doesn't interfere - export PKG_CONFIG=/bin/false - - export MAKEFLAGS="-j$(nproc)" -} - -_run_build () { - local abi=$1 - irrdir=$PWD - - mkdir -p $RUNNER_TEMP/src - cd $RUNNER_TEMP/src - srcdir=$PWD - [ -d irrlicht ] || ln -s $irrdir irrlicht - download - - builddir=$RUNNER_TEMP/build/irrlicht-$abi - pkgdir=$RUNNER_TEMP/pkg/$abi/Irrlicht - rm -rf "$pkgdir" - mkdir -p "$builddir" "$pkgdir" - - cd "$builddir" - build -} - -if [ $# -lt 1 ]; then - echo "Usage: ci-build-android.sh " - exit 1 -fi - -_setup_toolchain $1 -_run_build $1 diff --git a/irr/scripts/ci-build-mingw.sh b/irr/scripts/ci-build-mingw.sh deleted file mode 100755 index b1fdd7b99..000000000 --- a/irr/scripts/ci-build-mingw.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/bin/bash -e -topdir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -[[ -z "$CC" || -z "$CXX" ]] && exit 255 -variant=win32 -[[ "$(basename "$CXX")" == "x86_64-"* ]] && variant=win64 -with_sdl=0 -[[ "$extras" == *"-sdl"* ]] && with_sdl=1 -#with_gl3=0 -#[[ "$extras" == *"-gl3"* ]] && with_gl3=1 - -libjpeg_version=3.0.1 -libpng_version=1.6.40 -sdl2_version=2.28.5 -zlib_version=1.3.1 - -download () { - local url=$1 - local filename=${url##*/} - local foldername=${filename%%[.-]*} - - [ -d "./$foldername" ] && return 0 - [ -e "$filename" ] || wget "$url" -O "$filename" - sha256sum -w -c <(grep -F "$filename" "$topdir/sha256sums.txt") - unzip -o "$filename" -d "$foldername" -} - -libs=$PWD/libs -mkdir -p libs -pushd libs -libhost="http://minetest.kitsunemimi.pw" -download "$libhost/llvm/libjpeg-$libjpeg_version-$variant.zip" -download "$libhost/llvm/libpng-$libpng_version-$variant.zip" -[ $with_sdl -eq 1 ] && download "$libhost/llvm/sdl2-$sdl2_version-$variant.zip" -download "$libhost/llvm/zlib-$zlib_version-$variant.zip" -popd - -tmp=( - -DCMAKE_SYSTEM_NAME=Windows \ - -DPNG_LIBRARY=$libs/libpng/lib/libpng.dll.a \ - -DPNG_PNG_INCLUDE_DIR=$libs/libpng/include \ - -DJPEG_LIBRARY=$libs/libjpeg/lib/libjpeg.dll.a \ - -DJPEG_INCLUDE_DIR=$libs/libjpeg/include \ - -DZLIB_LIBRARY=$libs/zlib/lib/libz.dll.a \ - -DZLIB_INCLUDE_DIR=$libs/zlib/include -) -if [ $with_sdl -eq 1 ]; then - tmp+=( - -DUSE_SDL2=ON - -DCMAKE_PREFIX_PATH=$libs/sdl2/lib/cmake - ) -else - tmp+=(-DUSE_SDL2=OFF) -fi -#[ $with_gl3 -eq 1 ] && tmp+=(-DENABLE_OPENGL=OFF -DENABLE_OPENGL3=ON) - -cmake . "${tmp[@]}" -make -j$(nproc) - -if [ "$1" = "package" ]; then - make DESTDIR=$PWD/_install install - # strip library - "${CXX%-*}-strip" --strip-unneeded _install/usr/local/lib/*.dll - # bundle the DLLs that are specific to Irrlicht (kind of a hack) - shopt -s nullglob - cp -p $libs/*/bin/{libjpeg,libpng,SDL}*.dll _install/usr/local/lib/ - # create a ZIP - (cd _install/usr/local; zip -9r "$OLDPWD/irrlicht-$variant$extras.zip" -- *) -fi -exit 0 diff --git a/irr/scripts/ci-get-mingw.sh b/irr/scripts/ci-get-mingw.sh deleted file mode 100755 index 9cf933fea..000000000 --- a/irr/scripts/ci-get-mingw.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -e -topdir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -name=llvm-mingw-20231128-ucrt-ubuntu-20.04-x86_64.tar.xz -wget "https://github.com/mstorsjo/llvm-mingw/releases/download/20231128/$name" -O "$name" -sha256sum -w -c <(grep -F "$name" "$topdir/sha256sums.txt") -sudo tar -xaf "$name" -C /usr --strip-components=1 -rm -f "$name" diff --git a/irr/scripts/sha256sums.txt b/irr/scripts/sha256sums.txt deleted file mode 100644 index 0c080de3b..000000000 --- a/irr/scripts/sha256sums.txt +++ /dev/null @@ -1,9 +0,0 @@ -0f21ff3be90311092fe32e0e30878ef3ae9d9437b8d9ac25ef279e0d84e9bb8e llvm-mingw-20231128-ucrt-ubuntu-20.04-x86_64.tar.xz -53dfd31285f470fcf0dca88217c5cf9c557729af6d103afae5936e72ddc38d3c libjpeg-3.0.1-win32.zip -3d44e0740914e6878300e30653aad39e974821b5d7f6c2567e246b4eb04a5324 libjpeg-3.0.1-win64.zip -6baf4e819bfb3573760524b5dc9a04b5e479090d6d2046b86cf39a3107c0071f libpng-1.6.40-win32.zip -c02e029f01fce44baea7f4aecfd2564bd8a03507c0c6af8b03339ae0452c8b7d libpng-1.6.40-win64.zip -f9f890af960e92fd3f532f2e9ac00681c33bc67a722e000dfdaeb41b0064f1a0 sdl2-2.28.5-win32.zip -8dde2c6963544b7d8a2e87c128ebbdf51ad0e70c7e2df986ff4e963ce9996d9b sdl2-2.28.5-win64.zip -8af10515d57dbfee5d2106cd66cafa2adeb4270d4c6047ccbf7e8b5d2d50681c zlib-1.3.1-win32.zip -ad43f5d23052590c65633530743e5d622cc76b33c109072e6fd7b487aff56bca zlib-1.3.1-win64.zip diff --git a/irr/src/CAnimatedMeshSceneNode.cpp b/irr/src/CAnimatedMeshSceneNode.cpp index c3a71f943..ba8bc3b78 100644 --- a/irr/src/CAnimatedMeshSceneNode.cpp +++ b/irr/src/CAnimatedMeshSceneNode.cpp @@ -16,6 +16,7 @@ #include "IAnimatedMesh.h" #include "IFileSystem.h" #include "quaternion.h" +#include namespace irr { @@ -80,7 +81,7 @@ void CAnimatedMeshSceneNode::buildFrameNr(u32 timeMs) } if (StartFrame == EndFrame) { - CurrentFrameNr = (f32)StartFrame; // Support for non animated meshes + CurrentFrameNr = StartFrame; // Support for non animated meshes } else if (Looping) { // play animation looped CurrentFrameNr += timeMs * FramesPerSecond; @@ -89,26 +90,26 @@ void CAnimatedMeshSceneNode::buildFrameNr(u32 timeMs) // the last frame must be identical to first one with our current solution. if (FramesPerSecond > 0.f) { // forwards... if (CurrentFrameNr > EndFrame) - CurrentFrameNr = StartFrame + fmodf(CurrentFrameNr - StartFrame, (f32)(EndFrame - StartFrame)); + CurrentFrameNr = StartFrame + fmodf(CurrentFrameNr - StartFrame, EndFrame - StartFrame); } else // backwards... { if (CurrentFrameNr < StartFrame) - CurrentFrameNr = EndFrame - fmodf(EndFrame - CurrentFrameNr, (f32)(EndFrame - StartFrame)); + CurrentFrameNr = EndFrame - fmodf(EndFrame - CurrentFrameNr, EndFrame - StartFrame); } } else { // play animation non looped CurrentFrameNr += timeMs * FramesPerSecond; if (FramesPerSecond > 0.f) { // forwards... - if (CurrentFrameNr > (f32)EndFrame) { - CurrentFrameNr = (f32)EndFrame; + if (CurrentFrameNr > EndFrame) { + CurrentFrameNr = EndFrame; if (LoopCallBack) LoopCallBack->OnAnimationEnd(this); } } else // backwards... { - if (CurrentFrameNr < (f32)StartFrame) { - CurrentFrameNr = (f32)StartFrame; + if (CurrentFrameNr < StartFrame) { + CurrentFrameNr = StartFrame; if (LoopCallBack) LoopCallBack->OnAnimationEnd(this); } @@ -159,9 +160,7 @@ void CAnimatedMeshSceneNode::OnRegisterSceneNode() IMesh *CAnimatedMeshSceneNode::getMeshForCurrentFrame() { if (Mesh->getMeshType() != EAMT_SKINNED) { - s32 frameNr = (s32)getFrameNr(); - s32 frameBlend = (s32)(core::fract(getFrameNr()) * 1000.f); - return Mesh->getMesh(frameNr, frameBlend, StartFrame, EndFrame); + return Mesh->getMesh(getFrameNr()); } else { // As multiple scene nodes may be sharing the same skinned mesh, we have to // re-animate it every frame to ensure that this node gets the mesh that it needs. @@ -258,7 +257,6 @@ void CAnimatedMeshSceneNode::render() // for debug purposes only: if (DebugDataVisible && PassCount == 1) { video::SMaterial debug_mat; - debug_mat.Lighting = false; debug_mat.AntiAliasing = 0; driver->setMaterial(debug_mat); // show normals @@ -280,7 +278,6 @@ void CAnimatedMeshSceneNode::render() } debug_mat.ZBuffer = video::ECFN_DISABLED; - debug_mat.Lighting = false; driver->setMaterial(debug_mat); if (DebugDataVisible & scene::EDS_BBOX) @@ -316,7 +313,6 @@ void CAnimatedMeshSceneNode::render() // show mesh if (DebugDataVisible & scene::EDS_MESH_WIRE_OVERLAY) { - debug_mat.Lighting = false; debug_mat.Wireframe = true; debug_mat.ZBuffer = video::ECFN_DISABLED; driver->setMaterial(debug_mat); @@ -334,33 +330,33 @@ void CAnimatedMeshSceneNode::render() } //! Returns the current start frame number. -s32 CAnimatedMeshSceneNode::getStartFrame() const +f32 CAnimatedMeshSceneNode::getStartFrame() const { return StartFrame; } //! Returns the current start frame number. -s32 CAnimatedMeshSceneNode::getEndFrame() const +f32 CAnimatedMeshSceneNode::getEndFrame() const { return EndFrame; } //! sets the frames between the animation is looped. //! the default is 0 - MaximalFrameCount of the mesh. -bool CAnimatedMeshSceneNode::setFrameLoop(s32 begin, s32 end) +bool CAnimatedMeshSceneNode::setFrameLoop(f32 begin, f32 end) { - const s32 maxFrameCount = Mesh->getFrameCount() - 1; + const f32 maxFrame = Mesh->getMaxFrameNumber(); if (end < begin) { - StartFrame = core::s32_clamp(end, 0, maxFrameCount); - EndFrame = core::s32_clamp(begin, StartFrame, maxFrameCount); + StartFrame = std::clamp(end, 0, maxFrame); + EndFrame = std::clamp(begin, StartFrame, maxFrame); } else { - StartFrame = core::s32_clamp(begin, 0, maxFrameCount); - EndFrame = core::s32_clamp(end, StartFrame, maxFrameCount); + StartFrame = std::clamp(begin, 0, maxFrame); + EndFrame = std::clamp(end, StartFrame, maxFrame); } if (FramesPerSecond < 0) - setCurrentFrame((f32)EndFrame); + setCurrentFrame(EndFrame); else - setCurrentFrame((f32)StartFrame); + setCurrentFrame(StartFrame); return true; } @@ -535,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()); @@ -557,7 +553,7 @@ void CAnimatedMeshSceneNode::setMesh(IAnimatedMesh *mesh) // get start and begin time setAnimationSpeed(Mesh->getAnimationSpeed()); // NOTE: This had been commented out (but not removed!) in r3526. Which caused meshloader-values for speed to be ignored unless users specified explicitly. Missing a test-case where this could go wrong so I put the code back in. - setFrameLoop(0, Mesh->getFrameCount() - 1); + setFrameLoop(0, Mesh->getMaxFrameNumber()); } //! updates the absolute position based on the relative and the parents position diff --git a/irr/src/CAnimatedMeshSceneNode.h b/irr/src/CAnimatedMeshSceneNode.h index 0364ab527..e45edca86 100644 --- a/irr/src/CAnimatedMeshSceneNode.h +++ b/irr/src/CAnimatedMeshSceneNode.h @@ -45,7 +45,7 @@ public: //! sets the frames between the animation is looped. //! the default is 0 - MaximalFrameCount of the mesh. //! NOTE: setMesh will also change this value and set it to the full range of animations of the mesh - bool setFrameLoop(s32 begin, s32 end) override; + bool setFrameLoop(f32 begin, f32 end) override; //! Sets looping mode which is on by default. If set to false, //! animations will not be looped. @@ -93,9 +93,9 @@ public: //! Returns the current displayed frame number. f32 getFrameNr() const override; //! Returns the current start frame number. - s32 getStartFrame() const override; + f32 getStartFrame() const override; //! Returns the current end frame number. - s32 getEndFrame() const override; + f32 getEndFrame() const override; //! Sets if the scene node should not copy the materials of the mesh but use them in a read only style. /* In this way it is possible to change the materials a mesh causing all mesh scene nodes @@ -148,8 +148,8 @@ private: core::aabbox3d Box; IAnimatedMesh *Mesh; - s32 StartFrame; - s32 EndFrame; + f32 StartFrame; + f32 EndFrame; f32 FramesPerSecond; f32 CurrentFrameNr; diff --git a/irr/src/CAttributeImpl.h b/irr/src/CAttributeImpl.h index 9065a7342..e9436a407 100644 --- a/irr/src/CAttributeImpl.h +++ b/irr/src/CAttributeImpl.h @@ -3,9 +3,6 @@ // For conditions of distribution and use, see copyright notice in irrlicht.h #include "CAttributes.h" -#include "fast_atof.h" -#include "ITexture.h" -#include "IVideoDriver.h" namespace irr { diff --git a/irr/src/CAttributes.cpp b/irr/src/CAttributes.cpp index 2696d6bcf..b1509e455 100644 --- a/irr/src/CAttributes.cpp +++ b/irr/src/CAttributes.cpp @@ -12,61 +12,34 @@ namespace irr namespace io { -CAttributes::CAttributes(video::IVideoDriver *driver) : - Driver(driver) +CAttributes::CAttributes() { #ifdef _DEBUG setDebugName("CAttributes"); #endif - - if (Driver) - Driver->grab(); } CAttributes::~CAttributes() { clear(); - - if (Driver) - Driver->drop(); } //! Removes all attributes void CAttributes::clear() { - for (u32 i = 0; i < Attributes.size(); ++i) - Attributes[i]->drop(); - + for (auto it : Attributes) + delete it.second; Attributes.clear(); } -//! Returns attribute index from name, -1 if not found -s32 CAttributes::findAttribute(const c8 *attributeName) const -{ - for (u32 i = 0; i < Attributes.size(); ++i) - if (Attributes[i]->Name == attributeName) - return i; - - return -1; -} - -IAttribute *CAttributes::getAttributeP(const c8 *attributeName) const -{ - for (u32 i = 0; i < Attributes.size(); ++i) - if (Attributes[i]->Name == attributeName) - return Attributes[i]; - - return 0; -} - //! Sets a attribute as boolean value void CAttributes::setAttribute(const c8 *attributeName, bool value) { - IAttribute *att = getAttributeP(attributeName); - if (att) - att->setBool(value); - else { - Attributes.push_back(new CBoolAttribute(attributeName, value)); + auto it = Attributes.find(attributeName); + if (it != Attributes.end()) { + it->second->setBool(value); + } else { + Attributes[attributeName] = new CBoolAttribute(attributeName, value); } } @@ -76,9 +49,9 @@ void CAttributes::setAttribute(const c8 *attributeName, bool value) //! or 0 if attribute is not set. bool CAttributes::getAttributeAsBool(const c8 *attributeName, bool defaultNotFound) const { - const IAttribute *att = getAttributeP(attributeName); - if (att) - return att->getBool(); + auto it = Attributes.find(attributeName); + if (it != Attributes.end()) + return it->second->getBool(); else return defaultNotFound; } @@ -86,11 +59,11 @@ bool CAttributes::getAttributeAsBool(const c8 *attributeName, bool defaultNotFou //! Sets a attribute as integer value void CAttributes::setAttribute(const c8 *attributeName, s32 value) { - IAttribute *att = getAttributeP(attributeName); - if (att) - att->setInt(value); - else { - Attributes.push_back(new CIntAttribute(attributeName, value)); + auto it = Attributes.find(attributeName); + if (it != Attributes.end()) { + it->second->setInt(value); + } else { + Attributes[attributeName] = new CIntAttribute(attributeName, value); } } @@ -100,9 +73,9 @@ void CAttributes::setAttribute(const c8 *attributeName, s32 value) //! or 0 if attribute is not set. s32 CAttributes::getAttributeAsInt(const c8 *attributeName, irr::s32 defaultNotFound) const { - const IAttribute *att = getAttributeP(attributeName); - if (att) - return att->getInt(); + auto it = Attributes.find(attributeName); + if (it != Attributes.end()) + return it->second->getInt(); else return defaultNotFound; } @@ -110,11 +83,12 @@ s32 CAttributes::getAttributeAsInt(const c8 *attributeName, irr::s32 defaultNotF //! Sets a attribute as float value void CAttributes::setAttribute(const c8 *attributeName, f32 value) { - IAttribute *att = getAttributeP(attributeName); - if (att) - att->setFloat(value); - else - Attributes.push_back(new CFloatAttribute(attributeName, value)); + auto it = Attributes.find(attributeName); + if (it != Attributes.end()) { + it->second->setFloat(value); + } else { + Attributes[attributeName] = new CFloatAttribute(attributeName, value); + } } //! Gets a attribute as integer value @@ -123,27 +97,11 @@ void CAttributes::setAttribute(const c8 *attributeName, f32 value) //! or 0 if attribute is not set. f32 CAttributes::getAttributeAsFloat(const c8 *attributeName, irr::f32 defaultNotFound) const { - const IAttribute *att = getAttributeP(attributeName); - if (att) - return att->getFloat(); - - return defaultNotFound; -} - -//! Returns amount of string attributes set in this scene manager. -u32 CAttributes::getAttributeCount() const -{ - return Attributes.size(); -} - -//! Returns string attribute name by index. -//! \param index: Index value, must be between 0 and getStringAttributeCount()-1. -const c8 *CAttributes::getAttributeName(s32 index) const -{ - if ((u32)index >= Attributes.size()) - return 0; - - return Attributes[index]->Name.c_str(); + auto it = Attributes.find(attributeName); + if (it != Attributes.end()) + return it->second->getFloat(); + else + return defaultNotFound; } //! Returns the type of an attribute @@ -151,98 +109,17 @@ E_ATTRIBUTE_TYPE CAttributes::getAttributeType(const c8 *attributeName) const { E_ATTRIBUTE_TYPE ret = EAT_UNKNOWN; - const IAttribute *att = getAttributeP(attributeName); - if (att) - ret = att->getType(); + auto it = Attributes.find(attributeName); + if (it != Attributes.end()) + ret = it->second->getType(); return ret; } -//! Returns attribute type by index. -//! \param index: Index value, must be between 0 and getAttributeCount()-1. -E_ATTRIBUTE_TYPE CAttributes::getAttributeType(s32 index) const -{ - if ((u32)index >= Attributes.size()) - return EAT_UNKNOWN; - - return Attributes[index]->getType(); -} - -//! Gets an attribute as integer value -//! \param index: Index value, must be between 0 and getAttributeCount()-1. -s32 CAttributes::getAttributeAsInt(s32 index) const -{ - if ((u32)index < Attributes.size()) - return Attributes[index]->getInt(); - else - return 0; -} - -//! Gets an attribute as float value -//! \param index: Index value, must be between 0 and getAttributeCount()-1. -f32 CAttributes::getAttributeAsFloat(s32 index) const -{ - if ((u32)index < Attributes.size()) - return Attributes[index]->getFloat(); - else - return 0.f; -} - -//! Gets an attribute as boolean value -//! \param index: Index value, must be between 0 and getAttributeCount()-1. -bool CAttributes::getAttributeAsBool(s32 index) const -{ - bool ret = false; - - if ((u32)index < Attributes.size()) - ret = Attributes[index]->getBool(); - - return ret; -} - -//! Adds an attribute as integer -void CAttributes::addInt(const c8 *attributeName, s32 value) -{ - Attributes.push_back(new CIntAttribute(attributeName, value)); -} - -//! Adds an attribute as float -void CAttributes::addFloat(const c8 *attributeName, f32 value) -{ - Attributes.push_back(new CFloatAttribute(attributeName, value)); -} - -//! Adds an attribute as bool -void CAttributes::addBool(const c8 *attributeName, bool value) -{ - Attributes.push_back(new CBoolAttribute(attributeName, value)); -} - //! Returns if an attribute with a name exists bool CAttributes::existsAttribute(const c8 *attributeName) const { - return getAttributeP(attributeName) != 0; -} - -//! Sets an attribute as boolean value -void CAttributes::setAttribute(s32 index, bool value) -{ - if ((u32)index < Attributes.size()) - Attributes[index]->setBool(value); -} - -//! Sets an attribute as integer value -void CAttributes::setAttribute(s32 index, s32 value) -{ - if ((u32)index < Attributes.size()) - Attributes[index]->setInt(value); -} - -//! Sets a attribute as float value -void CAttributes::setAttribute(s32 index, f32 value) -{ - if ((u32)index < Attributes.size()) - Attributes[index]->setFloat(value); + return Attributes.find(attributeName) != Attributes.end(); } } // end namespace io diff --git a/irr/src/CAttributes.h b/irr/src/CAttributes.h index 0fe2739b6..8ea3ecf19 100644 --- a/irr/src/CAttributes.h +++ b/irr/src/CAttributes.h @@ -4,16 +4,14 @@ #pragma once +#include +#include #include "IAttributes.h" #include "IAttribute.h" namespace irr { -namespace video -{ -class ITexture; -class IVideoDriver; -} + namespace io { @@ -21,30 +19,16 @@ namespace io class CAttributes : public IAttributes { public: - CAttributes(video::IVideoDriver *driver = 0); + CAttributes(); ~CAttributes(); - //! Returns amount of attributes in this collection of attributes. - u32 getAttributeCount() const override; - - //! Returns attribute name by index. - //! \param index: Index value, must be between 0 and getAttributeCount()-1. - const c8 *getAttributeName(s32 index) const override; - //! Returns the type of an attribute //! \param attributeName: Name for the attribute E_ATTRIBUTE_TYPE getAttributeType(const c8 *attributeName) const override; - //! Returns attribute type by index. - //! \param index: Index value, must be between 0 and getAttributeCount()-1. - E_ATTRIBUTE_TYPE getAttributeType(s32 index) const override; - //! Returns if an attribute with a name exists bool existsAttribute(const c8 *attributeName) const override; - //! Returns attribute index from name, -1 if not found - s32 findAttribute(const c8 *attributeName) const override; - //! Removes all attributes void clear() override; @@ -55,7 +39,9 @@ public: */ //! Adds an attribute as integer - void addInt(const c8 *attributeName, s32 value) override; + void addInt(const c8 *attributeName, s32 value) override { + setAttribute(attributeName, value); + } //! Sets an attribute as integer value void setAttribute(const c8 *attributeName, s32 value) override; @@ -66,13 +52,6 @@ public: //! \return Returns value of the attribute previously set by setAttribute() s32 getAttributeAsInt(const c8 *attributeName, irr::s32 defaultNotFound = 0) const override; - //! Gets an attribute as integer value - //! \param index: Index value, must be between 0 and getAttributeCount()-1. - s32 getAttributeAsInt(s32 index) const override; - - //! Sets an attribute as integer value - void setAttribute(s32 index, s32 value) override; - /* Float Attribute @@ -80,7 +59,9 @@ public: */ //! Adds an attribute as float - void addFloat(const c8 *attributeName, f32 value) override; + void addFloat(const c8 *attributeName, f32 value) override { + setAttribute(attributeName, value); + } //! Sets a attribute as float value void setAttribute(const c8 *attributeName, f32 value) override; @@ -91,19 +72,14 @@ public: //! \return Returns value of the attribute previously set by setAttribute() f32 getAttributeAsFloat(const c8 *attributeName, irr::f32 defaultNotFound = 0.f) const override; - //! Gets an attribute as float value - //! \param index: Index value, must be between 0 and getAttributeCount()-1. - f32 getAttributeAsFloat(s32 index) const override; - - //! Sets an attribute as float value - void setAttribute(s32 index, f32 value) override; - /* Bool Attribute */ //! Adds an attribute as bool - void addBool(const c8 *attributeName, bool value) override; + void addBool(const c8 *attributeName, bool value) override { + setAttribute(attributeName, value); + } //! Sets an attribute as boolean value void setAttribute(const c8 *attributeName, bool value) override; @@ -114,19 +90,12 @@ public: //! \return Returns value of the attribute previously set by setAttribute() bool getAttributeAsBool(const c8 *attributeName, bool defaultNotFound = false) const override; - //! Gets an attribute as boolean value - //! \param index: Index value, must be between 0 and getAttributeCount()-1. - bool getAttributeAsBool(s32 index) const override; - - //! Sets an attribute as boolean value - void setAttribute(s32 index, bool value) override; - protected: - core::array Attributes; + typedef std::basic_string string; - IAttribute *getAttributeP(const c8 *attributeName) const; - - video::IVideoDriver *Driver; + // specify a comparator so we can directly look up in the map with const c8* + // (works since C++14) + std::map> Attributes; }; } // end namespace io diff --git a/irr/src/CB3DMeshFileLoader.cpp b/irr/src/CB3DMeshFileLoader.cpp index 6ee24af0e..cf6a980d8 100644 --- a/irr/src/CB3DMeshFileLoader.cpp +++ b/irr/src/CB3DMeshFileLoader.cpp @@ -259,14 +259,15 @@ bool CB3DMeshFileLoader::readChunkMESH(CSkinnedMesh::SJoint *inJoint) if (!NormalsInFile) { s32 i; - for (i = 0; i < (s32)meshBuffer->Indices.size(); i += 3) { - core::plane3df p(meshBuffer->getVertex(meshBuffer->Indices[i + 0])->Pos, - meshBuffer->getVertex(meshBuffer->Indices[i + 1])->Pos, - meshBuffer->getVertex(meshBuffer->Indices[i + 2])->Pos); + auto &indices = meshBuffer->Indices->Data; + for (i = 0; i < (s32)indices.size(); i += 3) { + core::plane3df p(meshBuffer->getVertex(indices[i + 0])->Pos, + meshBuffer->getVertex(indices[i + 1])->Pos, + meshBuffer->getVertex(indices[i + 2])->Pos); - meshBuffer->getVertex(meshBuffer->Indices[i + 0])->Normal += p.Normal; - meshBuffer->getVertex(meshBuffer->Indices[i + 1])->Normal += p.Normal; - meshBuffer->getVertex(meshBuffer->Indices[i + 2])->Normal += p.Normal; + meshBuffer->getVertex(indices[i + 0])->Normal += p.Normal; + meshBuffer->getVertex(indices[i + 1])->Normal += p.Normal; + meshBuffer->getVertex(indices[i + 2])->Normal += p.Normal; } for (i = 0; i < (s32)meshBuffer->getVertexCount(); ++i) { @@ -388,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); @@ -433,7 +435,7 @@ bool CB3DMeshFileLoader::readChunkTRIS(scene::SSkinMeshBuffer *meshBuffer, u32 m } const s32 memoryNeeded = B3dStack.getLast().length / sizeof(s32); - meshBuffer->Indices.reallocate(memoryNeeded + meshBuffer->Indices.size() + 1); + meshBuffer->Indices->Data.reserve(memoryNeeded + meshBuffer->Indices->Data.size() + 1); while ((B3dStack.getLast().startposition + B3dStack.getLast().length) > B3DFile->getPos()) // this chunk repeats { @@ -471,9 +473,9 @@ bool CB3DMeshFileLoader::readChunkTRIS(scene::SSkinMeshBuffer *meshBuffer, u32 m // Add the vertex to the meshbuffer: if (meshBuffer->VertexType == video::EVT_STANDARD) - meshBuffer->Vertices_Standard.push_back(BaseVertices[vertex_id[i]]); + meshBuffer->Vertices_Standard->Data.push_back(BaseVertices[vertex_id[i]]); else - meshBuffer->Vertices_2TCoords.push_back(BaseVertices[vertex_id[i]]); + meshBuffer->Vertices_2TCoords->Data.push_back(BaseVertices[vertex_id[i]]); // create vertex id to meshbuffer index link: AnimatedVertices_VertexID[vertex_id[i]] = meshBuffer->getVertexCount() - 1; @@ -484,7 +486,8 @@ bool CB3DMeshFileLoader::readChunkTRIS(scene::SSkinMeshBuffer *meshBuffer, u32 m video::S3DVertex *Vertex = meshBuffer->getVertex(meshBuffer->getVertexCount() - 1); if (!HasVertexColors) - Vertex->Color = B3dMaterial->Material.DiffuseColor; + Vertex->Color = video::SColorf(B3dMaterial->red, B3dMaterial->green, + B3dMaterial->blue, B3dMaterial->alpha).toSColor(); else if (Vertex->Color.getAlpha() == 255) Vertex->Color.setAlpha((s32)(B3dMaterial->alpha * 255.0f)); @@ -504,9 +507,9 @@ bool CB3DMeshFileLoader::readChunkTRIS(scene::SSkinMeshBuffer *meshBuffer, u32 m } } - meshBuffer->Indices.push_back(AnimatedVertices_VertexID[vertex_id[0]]); - meshBuffer->Indices.push_back(AnimatedVertices_VertexID[vertex_id[1]]); - meshBuffer->Indices.push_back(AnimatedVertices_VertexID[vertex_id[2]]); + meshBuffer->Indices->Data.push_back(AnimatedVertices_VertexID[vertex_id[0]]); + meshBuffer->Indices->Data.push_back(AnimatedVertices_VertexID[vertex_id[1]]); + meshBuffer->Indices->Data.push_back(AnimatedVertices_VertexID[vertex_id[2]]); } B3dStack.erase(B3dStack.size() - 1); @@ -889,23 +892,8 @@ bool CB3DMeshFileLoader::readChunkBRUS() } } - B3dMaterial.Material.DiffuseColor = video::SColorf(B3dMaterial.red, B3dMaterial.green, B3dMaterial.blue, B3dMaterial.alpha).toSColor(); - B3dMaterial.Material.ColorMaterial = video::ECM_NONE; - //------ Material fx ------ - if (B3dMaterial.fx & 1) { // full-bright - B3dMaterial.Material.AmbientColor = video::SColor(255, 255, 255, 255); - B3dMaterial.Material.Lighting = false; - } else - B3dMaterial.Material.AmbientColor = B3dMaterial.Material.DiffuseColor; - - if (B3dMaterial.fx & 2) // use vertex colors instead of brush color - B3dMaterial.Material.ColorMaterial = video::ECM_DIFFUSE_AND_AMBIENT; - - if (B3dMaterial.fx & 4) // flatshaded - B3dMaterial.Material.GouraudShading = false; - if (B3dMaterial.fx & 16) // disable backface culling B3dMaterial.Material.BackfaceCulling = false; @@ -913,8 +901,6 @@ bool CB3DMeshFileLoader::readChunkBRUS() B3dMaterial.Material.MaterialType = video::EMT_TRANSPARENT_VERTEX_ALPHA; B3dMaterial.Material.ZWriteEnable = video::EZW_OFF; } - - B3dMaterial.Material.Shininess = B3dMaterial.shininess; } B3dStack.erase(B3dStack.size() - 1); diff --git a/irr/src/CBillboardSceneNode.cpp b/irr/src/CBillboardSceneNode.cpp index ddb9d465a..1c6d88dae 100644 --- a/irr/src/CBillboardSceneNode.cpp +++ b/irr/src/CBillboardSceneNode.cpp @@ -26,27 +26,30 @@ CBillboardSceneNode::CBillboardSceneNode(ISceneNode *parent, ISceneManager *mgr, setSize(size); - Buffer->Vertices.set_used(4); - Buffer->Indices.set_used(6); + auto &Vertices = Buffer->Vertices->Data; + auto &Indices = Buffer->Indices->Data; - Buffer->Indices[0] = 0; - Buffer->Indices[1] = 2; - Buffer->Indices[2] = 1; - Buffer->Indices[3] = 0; - Buffer->Indices[4] = 3; - Buffer->Indices[5] = 2; + Vertices.resize(4); + Indices.resize(6); - Buffer->Vertices[0].TCoords.set(1.0f, 1.0f); - Buffer->Vertices[0].Color = colorBottom; + Indices[0] = 0; + Indices[1] = 2; + Indices[2] = 1; + Indices[3] = 0; + Indices[4] = 3; + Indices[5] = 2; - Buffer->Vertices[1].TCoords.set(1.0f, 0.0f); - Buffer->Vertices[1].Color = colorTop; + Vertices[0].TCoords.set(1.0f, 1.0f); + Vertices[0].Color = colorBottom; - Buffer->Vertices[2].TCoords.set(0.0f, 0.0f); - Buffer->Vertices[2].Color = colorTop; + Vertices[1].TCoords.set(1.0f, 0.0f); + Vertices[1].Color = colorTop; - Buffer->Vertices[3].TCoords.set(0.0f, 1.0f); - Buffer->Vertices[3].Color = colorBottom; + Vertices[2].TCoords.set(0.0f, 0.0f); + Vertices[2].Color = colorTop; + + Vertices[3].TCoords.set(0.0f, 1.0f); + Vertices[3].Color = colorBottom; } CBillboardSceneNode::~CBillboardSceneNode() @@ -82,7 +85,6 @@ void CBillboardSceneNode::render() if (DebugDataVisible & scene::EDS_BBOX) { driver->setTransform(video::ETS_WORLD, AbsoluteTransformation); video::SMaterial m; - m.Lighting = false; driver->setMaterial(m); driver->draw3DBox(BBoxSafe, video::SColor(0, 208, 195, 152)); } @@ -114,7 +116,7 @@ void CBillboardSceneNode::updateMesh(const irr::scene::ICameraSceneNode *camera) view *= -1.0f; - core::array &vertices = Buffer->Vertices; + auto &vertices = Buffer->Vertices->Data; for (s32 i = 0; i < 4; ++i) vertices[i].Normal = view; @@ -211,8 +213,9 @@ void CBillboardSceneNode::getSize(f32 &height, f32 &bottomEdgeWidth, //! \param overallColor: the color to set void CBillboardSceneNode::setColor(const video::SColor &overallColor) { + auto &vertices = Buffer->Vertices->Data; for (u32 vertex = 0; vertex < 4; ++vertex) - Buffer->Vertices[vertex].Color = overallColor; + vertices[vertex].Color = overallColor; } //! Set the color of the top and bottom vertices of the billboard @@ -221,10 +224,11 @@ void CBillboardSceneNode::setColor(const video::SColor &overallColor) void CBillboardSceneNode::setColor(const video::SColor &topColor, const video::SColor &bottomColor) { - Buffer->Vertices[0].Color = bottomColor; - Buffer->Vertices[1].Color = topColor; - Buffer->Vertices[2].Color = topColor; - Buffer->Vertices[3].Color = bottomColor; + auto &vertices = Buffer->Vertices->Data; + vertices[0].Color = bottomColor; + vertices[1].Color = topColor; + vertices[2].Color = topColor; + vertices[3].Color = bottomColor; } //! Gets the color of the top and bottom vertices of the billboard @@ -233,8 +237,9 @@ void CBillboardSceneNode::setColor(const video::SColor &topColor, void CBillboardSceneNode::getColor(video::SColor &topColor, video::SColor &bottomColor) const { - bottomColor = Buffer->Vertices[0].Color; - topColor = Buffer->Vertices[1].Color; + auto &vertices = Buffer->Vertices->Data; + bottomColor = vertices[0].Color; + topColor = vertices[1].Color; } //! Creates a clone of this scene node and its children. diff --git a/irr/src/CEGLManager.cpp b/irr/src/CEGLManager.cpp index ffe7a44cf..daaa64fdf 100644 --- a/irr/src/CEGLManager.cpp +++ b/irr/src/CEGLManager.cpp @@ -152,9 +152,6 @@ EGLConfig CEGLManager::chooseConfig(EConfigStyle confStyle) // Find proper OpenGL BIT. EGLint eglOpenGLBIT = 0; switch (Params.DriverType) { - case EDT_OGLES1: - eglOpenGLBIT = EGL_OPENGL_ES_BIT; - break; case EDT_OGLES2: case EDT_WEBGL1: eglOpenGLBIT = EGL_OPENGL_ES2_BIT; @@ -459,9 +456,6 @@ bool CEGLManager::generateContext() EGLint OpenGLESVersion = 0; switch (Params.DriverType) { - case EDT_OGLES1: - OpenGLESVersion = 1; - break; case EDT_OGLES2: case EDT_WEBGL1: OpenGLESVersion = 2; diff --git a/irr/src/CFPSCounter.cpp b/irr/src/CFPSCounter.cpp index 6717df5c9..0542fc5cb 100644 --- a/irr/src/CFPSCounter.cpp +++ b/irr/src/CFPSCounter.cpp @@ -11,53 +11,22 @@ namespace video { CFPSCounter::CFPSCounter() : - FPS(60), Primitive(0), StartTime(0), FramesCounted(0), - PrimitivesCounted(0), PrimitiveAverage(0), PrimitiveTotal(0) + FPS(0), StartTime(0), FramesCounted(0) { } -//! returns current fps -s32 CFPSCounter::getFPS() const -{ - return FPS; -} - -//! returns current primitive count -u32 CFPSCounter::getPrimitive() const -{ - return Primitive; -} - -//! returns average primitive count of last period -u32 CFPSCounter::getPrimitiveAverage() const -{ - return PrimitiveAverage; -} - -//! returns accumulated primitive count since start -u32 CFPSCounter::getPrimitiveTotal() const -{ - return PrimitiveTotal; -} - //! to be called every frame -void CFPSCounter::registerFrame(u32 now, u32 primitivesDrawn) +void CFPSCounter::registerFrame(u32 now) { ++FramesCounted; - PrimitiveTotal += primitivesDrawn; - PrimitivesCounted += primitivesDrawn; - Primitive = primitivesDrawn; const u32 milliseconds = now - StartTime; - if (milliseconds >= 1500) { const f32 invMilli = core::reciprocal((f32)milliseconds); FPS = core::ceil32((1000 * FramesCounted) * invMilli); - PrimitiveAverage = core::ceil32((1000 * PrimitivesCounted) * invMilli); FramesCounted = 0; - PrimitivesCounted = 0; StartTime = now; } } diff --git a/irr/src/CFPSCounter.h b/irr/src/CFPSCounter.h index e73afb049..692aef6bd 100644 --- a/irr/src/CFPSCounter.h +++ b/irr/src/CFPSCounter.h @@ -17,29 +17,15 @@ public: CFPSCounter(); //! returns current fps - s32 getFPS() const; - - //! returns primitive count - u32 getPrimitive() const; - - //! returns average primitive count of last period - u32 getPrimitiveAverage() const; - - //! returns accumulated primitive count since start - u32 getPrimitiveTotal() const; + s32 getFPS() const { return FPS; } //! to be called every frame - void registerFrame(u32 now, u32 primitive); + void registerFrame(u32 now); private: s32 FPS; - u32 Primitive; u32 StartTime; - u32 FramesCounted; - u32 PrimitivesCounted; - u32 PrimitiveAverage; - u32 PrimitiveTotal; }; } // end namespace video diff --git a/irr/src/CFileList.cpp b/irr/src/CFileList.cpp index dde8e75ac..cd6c85df4 100644 --- a/irr/src/CFileList.cpp +++ b/irr/src/CFileList.cpp @@ -80,7 +80,7 @@ u32 CFileList::addItem(const io::path &fullPath, u32 offset, u32 size, bool isDi entry.FullName = entry.Name; - core::deletePathFromFilename(entry.Name); + entry.Name = core::deletePathFromFilename(entry.Name); if (IgnorePaths) entry.FullName = entry.Name; @@ -140,7 +140,7 @@ s32 CFileList::findFile(const io::path &filename, bool isDirectory = false) cons entry.FullName.make_lower(); if (IgnorePaths) - core::deletePathFromFilename(entry.FullName); + entry.FullName = core::deletePathFromFilename(entry.FullName); return Files.binary_search(entry); } diff --git a/irr/src/CFileSystem.cpp b/irr/src/CFileSystem.cpp index bffb703dd..386bb389c 100644 --- a/irr/src/CFileSystem.cpp +++ b/irr/src/CFileSystem.cpp @@ -58,14 +58,8 @@ CFileSystem::CFileSystem() //! destructor CFileSystem::~CFileSystem() { - u32 i; - - for (i = 0; i < FileArchives.size(); ++i) { - FileArchives[i]->drop(); - } - - for (i = 0; i < ArchiveLoader.size(); ++i) { - ArchiveLoader[i]->drop(); + for (auto *it : ArchiveLoader) { + it->drop(); } } @@ -75,15 +69,6 @@ IReadFile *CFileSystem::createAndOpenFile(const io::path &filename) if (filename.empty()) return 0; - IReadFile *file = 0; - u32 i; - - for (i = 0; i < FileArchives.size(); ++i) { - file = FileArchives[i]->createAndOpenFile(filename); - if (file) - return file; - } - // Create the file using an absolute path so that it matches // the scheme used by CNullDriver::getTexture(). return CReadFile::createReadFile(getAbsolutePath(filename)); @@ -150,241 +135,6 @@ IArchiveLoader *CFileSystem::getArchiveLoader(u32 index) const return 0; } -//! move the hirarchy of the filesystem. moves sourceIndex relative up or down -bool CFileSystem::moveFileArchive(u32 sourceIndex, s32 relative) -{ - bool r = false; - const s32 dest = (s32)sourceIndex + relative; - const s32 dir = relative < 0 ? -1 : 1; - const s32 sourceEnd = ((s32)FileArchives.size()) - 1; - IFileArchive *t; - - for (s32 s = (s32)sourceIndex; s != dest; s += dir) { - if (s < 0 || s > sourceEnd || s + dir < 0 || s + dir > sourceEnd) - continue; - - t = FileArchives[s + dir]; - FileArchives[s + dir] = FileArchives[s]; - FileArchives[s] = t; - r = true; - } - return r; -} - -//! Adds an archive to the file system. -bool CFileSystem::addFileArchive(const io::path &filename, bool ignoreCase, - bool ignorePaths, E_FILE_ARCHIVE_TYPE archiveType, - const core::stringc &password, - IFileArchive **retArchive) -{ - IFileArchive *archive = 0; - bool ret = false; - - // see if archive is already added - - s32 i; - - // do we know what type it should be? - if (archiveType == EFAT_UNKNOWN) { - // try to load archive based on file name - for (i = ArchiveLoader.size() - 1; i >= 0; --i) { - if (ArchiveLoader[i]->isALoadableFileFormat(filename)) { - archive = ArchiveLoader[i]->createArchive(filename, ignoreCase, ignorePaths); - if (archive) - break; - } - } - - // try to load archive based on content - if (!archive) { - io::IReadFile *file = createAndOpenFile(filename); - if (file) { - for (i = ArchiveLoader.size() - 1; i >= 0; --i) { - file->seek(0); - if (ArchiveLoader[i]->isALoadableFileFormat(file)) { - file->seek(0); - archive = ArchiveLoader[i]->createArchive(file, ignoreCase, ignorePaths); - if (archive) - break; - } - } - file->drop(); - } - } - } else { - // try to open archive based on archive loader type - - io::IReadFile *file = 0; - - for (i = ArchiveLoader.size() - 1; i >= 0; --i) { - if (ArchiveLoader[i]->isALoadableFileFormat(archiveType)) { - // attempt to open file - if (!file) - file = createAndOpenFile(filename); - - // is the file open? - if (file) { - // attempt to open archive - file->seek(0); - if (ArchiveLoader[i]->isALoadableFileFormat(file)) { - file->seek(0); - archive = ArchiveLoader[i]->createArchive(file, ignoreCase, ignorePaths); - if (archive) - break; - } - } else { - // couldn't open file - break; - } - } - } - - // if open, close the file - if (file) - file->drop(); - } - - if (archive) { - FileArchives.push_back(archive); - if (password.size()) - archive->Password = password; - if (retArchive) - *retArchive = archive; - ret = true; - } else { - os::Printer::log("Could not create archive for", filename, ELL_ERROR); - } - - return ret; -} - -bool CFileSystem::addFileArchive(IReadFile *file, bool ignoreCase, - bool ignorePaths, E_FILE_ARCHIVE_TYPE archiveType, - const core::stringc &password, IFileArchive **retArchive) -{ - if (!file) - return false; - - if (file) { - IFileArchive *archive = 0; - s32 i; - - if (archiveType == EFAT_UNKNOWN) { - // try to load archive based on file name - for (i = ArchiveLoader.size() - 1; i >= 0; --i) { - if (ArchiveLoader[i]->isALoadableFileFormat(file->getFileName())) { - archive = ArchiveLoader[i]->createArchive(file, ignoreCase, ignorePaths); - if (archive) - break; - } - } - - // try to load archive based on content - if (!archive) { - for (i = ArchiveLoader.size() - 1; i >= 0; --i) { - file->seek(0); - if (ArchiveLoader[i]->isALoadableFileFormat(file)) { - file->seek(0); - archive = ArchiveLoader[i]->createArchive(file, ignoreCase, ignorePaths); - if (archive) - break; - } - } - } - } else { - // try to open archive based on archive loader type - for (i = ArchiveLoader.size() - 1; i >= 0; --i) { - if (ArchiveLoader[i]->isALoadableFileFormat(archiveType)) { - // attempt to open archive - file->seek(0); - if (ArchiveLoader[i]->isALoadableFileFormat(file)) { - file->seek(0); - archive = ArchiveLoader[i]->createArchive(file, ignoreCase, ignorePaths); - if (archive) - break; - } - } - } - } - - if (archive) { - FileArchives.push_back(archive); - if (password.size()) - archive->Password = password; - if (retArchive) - *retArchive = archive; - return true; - } else { - os::Printer::log("Could not create archive for", file->getFileName(), ELL_ERROR); - } - } - - return false; -} - -//! Adds an archive to the file system. -bool CFileSystem::addFileArchive(IFileArchive *archive) -{ - if (archive) { - for (u32 i = 0; i < FileArchives.size(); ++i) { - if (archive == FileArchives[i]) { - return false; - } - } - FileArchives.push_back(archive); - archive->grab(); - - return true; - } - - return false; -} - -//! removes an archive from the file system. -bool CFileSystem::removeFileArchive(u32 index) -{ - bool ret = false; - if (index < FileArchives.size()) { - FileArchives[index]->drop(); - FileArchives.erase(index); - ret = true; - } - return ret; -} - -//! removes an archive from the file system. -bool CFileSystem::removeFileArchive(const io::path &filename) -{ - const path absPath = getAbsolutePath(filename); - for (u32 i = 0; i < FileArchives.size(); ++i) { - if (absPath == FileArchives[i]->getFileList()->getPath()) - return removeFileArchive(i); - } - return false; -} - -//! Removes an archive from the file system. -bool CFileSystem::removeFileArchive(const IFileArchive *archive) -{ - for (u32 i = 0; i < FileArchives.size(); ++i) { - if (archive == FileArchives[i]) { - return removeFileArchive(i); - } - } - return false; -} - -//! gets an archive -u32 CFileSystem::getFileArchiveCount() const -{ - return FileArchives.size(); -} - -IFileArchive *CFileSystem::getFileArchive(u32 index) -{ - return index < getFileArchiveCount() ? FileArchives[index] : 0; -} - //! Returns the string of the current working directory const io::path &CFileSystem::getWorkingDirectory() { @@ -711,17 +461,6 @@ IFileList *CFileSystem::createFileList() //! parent r->addItem(Path + _IRR_TEXT(".."), 0, 0, true, 0); - - //! merge archives - for (u32 i = 0; i < FileArchives.size(); ++i) { - const IFileList *merge = FileArchives[i]->getFileList(); - - for (u32 j = 0; j < merge->getFileCount(); ++j) { - if (core::isInSameDirectory(Path, merge->getFullFileName(j)) == 0) { - r->addItem(merge->getFullFileName(j), merge->getFileOffset(j), merge->getFileSize(j), merge->isDirectory(j), 0); - } - } - } } if (r) @@ -738,10 +477,6 @@ IFileList *CFileSystem::createEmptyFileList(const io::path &path, bool ignoreCas //! determines if a file exists and would be able to be opened. bool CFileSystem::existFile(const io::path &filename) const { - for (u32 i = 0; i < FileArchives.size(); ++i) - if (FileArchives[i]->getFileList()->findFile(filename) != -1) - return true; - #if defined(_MSC_VER) return (_access(filename.c_str(), 0) != -1); #elif defined(F_OK) diff --git a/irr/src/CFileSystem.h b/irr/src/CFileSystem.h index 18bc7ff3f..9400d85a3 100644 --- a/irr/src/CFileSystem.h +++ b/irr/src/CFileSystem.h @@ -4,8 +4,8 @@ #pragma once +#include #include "IFileSystem.h" -#include "irrArray.h" namespace irr { @@ -17,7 +17,7 @@ class CZipReader; /*! FileSystem which uses normal files and one zipfile */ -class CFileSystem : public IFileSystem +class CFileSystem final : public IFileSystem { public: //! constructor @@ -41,25 +41,6 @@ public: //! Opens a file for write access. IWriteFile *createAndWriteFile(const io::path &filename, bool append = false) override; - //! Adds an archive to the file system. - virtual bool addFileArchive(const io::path &filename, - bool ignoreCase = true, bool ignorePaths = true, - E_FILE_ARCHIVE_TYPE archiveType = EFAT_UNKNOWN, - const core::stringc &password = "", - IFileArchive **retArchive = 0) override; - - //! Adds an archive to the file system. - virtual bool addFileArchive(IReadFile *file, bool ignoreCase = true, - bool ignorePaths = true, - E_FILE_ARCHIVE_TYPE archiveType = EFAT_UNKNOWN, - const core::stringc &password = "", - IFileArchive **retArchive = 0) override; - - //! Adds an archive to the file system. - bool addFileArchive(IFileArchive *archive) override; - - //! move the hirarchy of the filesystem. moves sourceIndex relative up or down - bool moveFileArchive(u32 sourceIndex, s32 relative) override; //! Adds an external archive loader to the engine. void addArchiveLoader(IArchiveLoader *loader) override; @@ -70,21 +51,6 @@ public: //! Gets the archive loader by index. IArchiveLoader *getArchiveLoader(u32 index) const override; - //! gets the file archive count - u32 getFileArchiveCount() const override; - - //! gets an archive - IFileArchive *getFileArchive(u32 index) override; - - //! removes an archive from the file system. - bool removeFileArchive(u32 index) override; - - //! removes an archive from the file system. - bool removeFileArchive(const io::path &filename) override; - - //! Removes an archive from the file system. - bool removeFileArchive(const IFileArchive *archive) override; - //! Returns the string of the current working directory const io::path &getWorkingDirectory() override; @@ -129,9 +95,7 @@ private: //! WorkingDirectory for Native and Virtual filesystems io::path WorkingDirectory[2]; //! currently attached ArchiveLoaders - core::array ArchiveLoader; - //! currently attached Archives - core::array FileArchives; + std::vector ArchiveLoader; }; } // end namespace irr diff --git a/irr/src/CGLTFMeshFileLoader.cpp b/irr/src/CGLTFMeshFileLoader.cpp new file mode 100644 index 000000000..54d207e5f --- /dev/null +++ b/irr/src/CGLTFMeshFileLoader.cpp @@ -0,0 +1,935 @@ +// Minetest +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "CGLTFMeshFileLoader.h" + +#include "SMaterialLayer.h" +#include "coreutil.h" +#include "CSkinnedMesh.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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace irr { + +/* Notes on the coordinate system. + * + * glTF uses a right-handed coordinate system where +Z is the + * front-facing axis, and Irrlicht uses a left-handed coordinate + * system where -Z is the front-facing axis. + * We convert between them by mirroring the mesh across the X axis. + * Doing this correctly requires negating the Z coordinate on + * vertex positions and normals, and reversing the winding order + * of the vertex indices. + */ + +// Right-to-left handedness conversions + +template +static inline T convertHandedness(const T &t); + +template <> +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; + +template +SelfType::Accessor +SelfType::Accessor::sparseIndices(const tiniergltf::GlTF &model, + const tiniergltf::AccessorSparseIndices &indices, + const std::size_t count) +{ + const auto &view = model.bufferViews->at(indices.bufferView); + const auto byteStride = view.byteStride.value_or(indices.elementSize()); + + const auto &buffer = model.buffers->at(view.buffer); + const auto source = buffer.data.data() + view.byteOffset + indices.byteOffset; + + return SelfType::Accessor(source, byteStride, count); +} + +template +SelfType::Accessor +SelfType::Accessor::sparseValues(const tiniergltf::GlTF &model, + const tiniergltf::AccessorSparseValues &values, + const std::size_t count, + const std::size_t defaultByteStride) +{ + const auto &view = model.bufferViews->at(values.bufferView); + const auto byteStride = view.byteStride.value_or(defaultByteStride); + + const auto &buffer = model.buffers->at(view.buffer); + const auto source = buffer.data.data() + view.byteOffset + values.byteOffset; + + return SelfType::Accessor(source, byteStride, count); +} + +template +SelfType::Accessor +SelfType::Accessor::base(const tiniergltf::GlTF &model, std::size_t accessorIdx) +{ + const auto &accessor = model.accessors->at(accessorIdx); + + if (!accessor.bufferView.has_value()) { + return Accessor(accessor.count); + } + + const auto &view = model.bufferViews->at(accessor.bufferView.value()); + const auto byteStride = view.byteStride.value_or(accessor.elementSize()); + + const auto &buffer = model.buffers->at(view.buffer); + const auto source = buffer.data.data() + view.byteOffset + accessor.byteOffset; + + return Accessor(source, byteStride, accessor.count); +} + +template +SelfType::Accessor +SelfType::Accessor::make(const tiniergltf::GlTF &model, std::size_t accessorIdx) +{ + const auto &accessor = model.accessors->at(accessorIdx); + if (accessor.componentType != getComponentType() || accessor.type != getType()) + throw std::runtime_error("invalid accessor"); + + const auto base = Accessor::base(model, accessorIdx); + + if (accessor.sparse.has_value()) { + std::vector vec(accessor.count); + for (std::size_t i = 0; i < accessor.count; ++i) { + vec[i] = base.get(i); + } + const auto overriddenCount = accessor.sparse->count; + const auto indicesAccessor = ([&]() -> AccessorVariant { + switch (accessor.sparse->indices.componentType) { + case tiniergltf::AccessorSparseIndices::ComponentType::UNSIGNED_BYTE: + return Accessor::sparseIndices(model, accessor.sparse->indices, overriddenCount); + case tiniergltf::AccessorSparseIndices::ComponentType::UNSIGNED_SHORT: + return Accessor::sparseIndices(model, accessor.sparse->indices, overriddenCount); + case tiniergltf::AccessorSparseIndices::ComponentType::UNSIGNED_INT: + return Accessor::sparseIndices(model, accessor.sparse->indices, overriddenCount); + } + throw std::logic_error("invalid enum value"); + })(); + + const auto valuesAccessor = Accessor::sparseValues(model, + accessor.sparse->values, overriddenCount, + accessor.bufferView.has_value() + ? model.bufferViews->at(*accessor.bufferView).byteStride.value_or(accessor.elementSize()) + : accessor.elementSize()); + + for (std::size_t i = 0; i < overriddenCount; ++i) { + u32 index; + std::visit([&](auto &&acc) { index = acc.get(i); }, indicesAccessor); + if (index >= accessor.count) + throw std::runtime_error("index out of bounds"); + vec[index] = valuesAccessor.get(i); + } + return Accessor(vec, accessor.count); + } + + return base; +} + +#define ACCESSOR_TYPES(T, U, V) \ + template <> \ + constexpr tiniergltf::Accessor::Type SelfType::Accessor::getType() \ + { \ + return tiniergltf::Accessor::Type::U; \ + } \ + template <> \ + constexpr tiniergltf::Accessor::ComponentType SelfType::Accessor::getComponentType() \ + { \ + return tiniergltf::Accessor::ComponentType::V; \ + } + +#define VEC_ACCESSOR_TYPES(T, U, N) \ + template <> \ + constexpr tiniergltf::Accessor::Type SelfType::Accessor>::getType() \ + { \ + return tiniergltf::Accessor::Type::VEC##N; \ + } \ + template <> \ + constexpr tiniergltf::Accessor::ComponentType SelfType::Accessor>::getComponentType() \ + { \ + return tiniergltf::Accessor::ComponentType::U; \ + } \ + template <> \ + std::array SelfType::rawget(const char *ptr) \ + { \ + std::array res; \ + for (int i = 0; i < N; ++i) \ + res[i] = rawget(ptr + sizeof(T) * i); \ + return res; \ + } + +#define ACCESSOR_PRIMITIVE(T, U) \ + ACCESSOR_TYPES(T, SCALAR, U) \ + VEC_ACCESSOR_TYPES(T, U, 2) \ + VEC_ACCESSOR_TYPES(T, U, 3) \ + VEC_ACCESSOR_TYPES(T, U, 4) + +ACCESSOR_PRIMITIVE(f32, FLOAT) +ACCESSOR_PRIMITIVE(u8, UNSIGNED_BYTE) +ACCESSOR_PRIMITIVE(u16, UNSIGNED_SHORT) +ACCESSOR_PRIMITIVE(u32, UNSIGNED_INT) + +ACCESSOR_TYPES(core::vector3df, VEC3, FLOAT) +ACCESSOR_TYPES(core::quaternion, VEC4, FLOAT) +ACCESSOR_TYPES(core::matrix4, MAT4, FLOAT) + +template +T SelfType::Accessor::get(std::size_t i) const +{ + // Buffer-based accessor: Read directly from the buffer. + if (std::holds_alternative(source)) { + const auto bufsrc = std::get(source); + return rawget(bufsrc.ptr + i * bufsrc.byteStride); + } + // Array-based accessor (used for sparse accessors): Read from array. + if (std::holds_alternative>(source)) { + return std::get>(source)[i]; + } + // Default-initialized accessor. + // We differ slightly from glTF here in that + // we default-initialize quaternions and matrices properly, + // but this does not cause any discrepancies for valid glTF models. + std::get>(source); + return T(); +} + +template +T SelfType::rawget(const char *ptr) +{ + T dest; + std::memcpy(&dest, ptr, sizeof(dest)); +#ifdef __BIG_ENDIAN__ + return os::Byteswap::byteswap(dest); +#else + return dest; +#endif +} + +// Note that these "more specialized templates" should win. + +template <> +core::matrix4 SelfType::rawget(const char *ptr) +{ + core::matrix4 mat; + for (u8 i = 0; i < 16; ++i) { + mat[i] = rawget(ptr + i * sizeof(f32)); + } + return mat; +} + +template <> +core::vector3df SelfType::rawget(const char *ptr) +{ + return core::vector3df( + rawget(ptr), + rawget(ptr + sizeof(f32)), + rawget(ptr + 2 * sizeof(f32))); +} + +template <> +core::quaternion SelfType::rawget(const char *ptr) +{ + return core::quaternion( + rawget(ptr), + rawget(ptr + sizeof(f32)), + rawget(ptr + 2 * sizeof(f32)), + rawget(ptr + 3 * sizeof(f32))); +} + +template +SelfType::NormalizedValuesAccessor +SelfType::createNormalizedValuesAccessor( + const tiniergltf::GlTF &model, + const std::size_t accessorIdx) +{ + const auto &acc = model.accessors->at(accessorIdx); + switch (acc.componentType) { + case tiniergltf::Accessor::ComponentType::UNSIGNED_BYTE: + return Accessor>::make(model, accessorIdx); + case tiniergltf::Accessor::ComponentType::UNSIGNED_SHORT: + return Accessor>::make(model, accessorIdx); + case tiniergltf::Accessor::ComponentType::FLOAT: + return Accessor>::make(model, accessorIdx); + default: + throw std::runtime_error("invalid component type"); + } +} + +template +std::array SelfType::getNormalizedValues( + const NormalizedValuesAccessor &accessor, + const std::size_t i) +{ + std::array values; + if (std::holds_alternative>>(accessor)) { + const auto u8s = std::get>>(accessor).get(i); + for (u8 i = 0; i < N; ++i) + values[i] = static_cast(u8s[i]) / std::numeric_limits::max(); + } else if (std::holds_alternative>>(accessor)) { + const auto u16s = std::get>>(accessor).get(i); + for (u8 i = 0; i < N; ++i) + values[i] = static_cast(u16s[i]) / std::numeric_limits::max(); + } else { + values = std::get>>(accessor).get(i); + for (u8 i = 0; i < N; ++i) { + if (values[i] < 0 || values[i] > 1) + throw std::runtime_error("invalid normalized value"); + } + } + return values; +} + +bool SelfType::isALoadableFileExtension( + const io::path& filename) const +{ + return core::hasFileExtension(filename, "gltf") || + core::hasFileExtension(filename, "glb"); +} + +/** + * Entry point into loading a GLTF model. +*/ +IAnimatedMesh* SelfType::createMesh(io::IReadFile* file) +{ + if (file->getSize() <= 0) { + return nullptr; + } + std::optional model = tryParseGLTF(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() + && model->accessors.has_value() + && model->meshes.has_value() + && model->nodes.has_value())) { + os::Printer::log("glTF loader", "missing required fields", ELL_ERROR); + return nullptr; + } + + auto *mesh = new CSkinnedMesh(); + MeshExtractor parser(std::move(model.value()), mesh); + try { + parser.load(); + } catch (std::runtime_error &e) { + os::Printer::log("glTF loader", e.what(), ELL_ERROR); + mesh->drop(); + return nullptr; + } + if (model->images.has_value()) + os::Printer::log("glTF loader", "embedded images are not supported", ELL_WARNING); + return mesh; +} + +static void transformVertices(std::vector &vertices, const core::matrix4 &transform) +{ + for (auto &vertex : vertices) { + // 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. + vertex.Normal = transform.rotateAndScaleVect(vertex.Normal); + // Renormalize (length might have been affected by scaling). + vertex.Normal.normalize(); + } +} + +static void checkIndices(const std::vector &indices, const std::size_t nVerts) +{ + for (u16 index : indices) { + if (index >= nVerts) + throw std::runtime_error("index out of bounds"); + } +} + +static std::vector generateIndices(const std::size_t nVerts) +{ + std::vector indices(nVerts); + for (std::size_t i = 0; i < nVerts; i += 3) { + // Reverse winding order per triangle + indices[i] = i + 2; + indices[i + 1] = i + 1; + indices[i + 2] = i; + } + return indices; +} + +using Wrap = tiniergltf::Sampler::Wrap; +static video::E_TEXTURE_CLAMP convertTextureWrap(const Wrap wrap) { + switch (wrap) { + case Wrap::REPEAT: + return video::ETC_REPEAT; + case Wrap::CLAMP_TO_EDGE: + return video::ETC_CLAMP_TO_EDGE; + case Wrap::MIRRORED_REPEAT: + return video::ETC_MIRROR; + default: + throw std::runtime_error("invalid sampler wrapping mode"); + } +} + +void SelfType::MeshExtractor::addPrimitive( + const tiniergltf::MeshPrimitive &primitive, + const std::optional skinIdx, + CSkinnedMesh::SJoint *parent) +{ + auto vertices = getVertices(primitive); + if (!vertices.has_value()) + 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 (n_vertices >= std::numeric_limits::max()) + throw std::runtime_error("too many vertices"); + + // Apply the global transform along the parent chain. + transformVertices(*vertices, parent->GlobalMatrix); + + auto maybeIndices = getIndices(primitive); + std::vector indices; + if (maybeIndices.has_value()) { + indices = std::move(*maybeIndices); + checkIndices(indices, vertices->size()); + } else { + // Non-indexed geometry + indices = generateIndices(vertices->size()); + } + + m_irr_model->addMeshBuffer( + new SSkinMeshBuffer(std::move(*vertices), std::move(indices))); + const auto meshbufNr = m_irr_model->getMeshBufferCount() - 1; + auto *meshbuf = m_irr_model->getMeshBuffer(meshbufNr); + + if (primitive.material.has_value()) { + const auto &material = m_gltf_model.materials->at(*primitive.material); + if (material.pbrMetallicRoughness.has_value()) { + const auto &texture = material.pbrMetallicRoughness->baseColorTexture; + if (texture.has_value()) { + m_irr_model->setTextureSlot(meshbufNr, static_cast(texture->index)); + const auto samplerIdx = m_gltf_model.textures->at(texture->index).sampler; + if (samplerIdx.has_value()) { + auto &sampler = m_gltf_model.samplers->at(*samplerIdx); + auto &layer = meshbuf->getMaterial().TextureLayers[0]; + layer.TextureWrapU = convertTextureWrap(sampler.wrapS); + layer.TextureWrapV = convertTextureWrap(sampler.wrapT); + } + } + } + } + + if (!skinIdx.has_value()) { + // No skin => all vertices belong entirely to their parent + for (std::size_t v = 0; v < n_vertices; ++v) { + auto *weight = m_irr_model->addWeight(parent); + weight->buffer_id = meshbufNr; + weight->vertex_id = v; + weight->strength = 1.0f; + } + return; + } + + const auto &skin = m_gltf_model.skins->at(*skinIdx); + + const auto &attrs = primitive.attributes; + const auto &joints = attrs.joints; + if (!joints.has_value()) + return; + + const auto &weights = attrs.weights; + for (std::size_t set = 0; set < joints->size(); ++set) { + const auto jointAccessor = ([&]() -> ArrayAccessorVariant<4, u8, u16> { + const auto idx = joints->at(set); + const auto &acc = m_gltf_model.accessors->at(idx); + + switch (acc.componentType) { + case tiniergltf::Accessor::ComponentType::UNSIGNED_BYTE: + return Accessor>::make(m_gltf_model, idx); + case tiniergltf::Accessor::ComponentType::UNSIGNED_SHORT: + return Accessor>::make(m_gltf_model, idx); + default: + throw std::runtime_error("invalid component type"); + } + })(); + + const auto weightAccessor = createNormalizedValuesAccessor<4>(m_gltf_model, weights->at(set)); + + for (std::size_t v = 0; v < n_vertices; ++v) { + std::array jointIdxs; + if (std::holds_alternative>>(jointAccessor)) { + const auto jointIdxsU8 = std::get>>(jointAccessor).get(v); + jointIdxs = {jointIdxsU8[0], jointIdxsU8[1], jointIdxsU8[2], jointIdxsU8[3]}; + } else if (std::holds_alternative>>(jointAccessor)) { + jointIdxs = std::get>>(jointAccessor).get(v); + } + std::array strengths = getNormalizedValues(weightAccessor, v); + + // 4 joints per set + for (std::size_t in_set = 0; in_set < 4; ++in_set) { + u16 jointIdx = jointIdxs[in_set]; + f32 strength = strengths[in_set]; + if (strength == 0) + continue; + + CSkinnedMesh::SWeight *weight = m_irr_model->addWeight(m_loaded_nodes.at(skin.joints.at(jointIdx))); + weight->buffer_id = meshbufNr; + weight->vertex_id = v; + weight->strength = strength; + } + } + } +} + +/** + * Load up the rawest form of the model. The vertex positions and indices. + * Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes + * If material is undefined, then a default material MUST be used. + */ +void SelfType::MeshExtractor::deferAddMesh( + const std::size_t meshIdx, + const std::optional skinIdx, + CSkinnedMesh::SJoint *parent) +{ + m_mesh_loaders.emplace_back([=] { + for (std::size_t pi = 0; pi < getPrimitiveCount(meshIdx); ++pi) { + const auto &primitive = m_gltf_model.meshes->at(meshIdx).primitives.at(pi); + addPrimitive(primitive, skinIdx, parent); + } + }); +} + +// Base transformation between left & right handed coordinate systems. +// This just inverts the Z axis. +static const core::matrix4 leftToRight = core::matrix4( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, -1, 0, + 0, 0, 0, 1 +); +static const core::matrix4 rightToLeft = leftToRight; + +static core::matrix4 loadTransform(const tiniergltf::Node::Matrix &m, CSkinnedMesh::SJoint *joint) +{ + // Note: Under the hood, this casts these doubles to floats. + 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])); + + // 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, CSkinnedMesh::SJoint *joint) +{ + const auto &trans = trs.translation; + const auto &rot = trs.rotation; + const auto &scale = trs.scale; + core::matrix4 transMat; + 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(joint->Animatedscale); + return transMat * rotMat * scaleMat; +} + +static core::matrix4 loadTransform(std::optional> transform, + CSkinnedMesh::SJoint *joint) { + if (!transform.has_value()) { + return core::matrix4(); + } + return std::visit([joint](const auto &t) { return loadTransform(t, joint); }, *transform); +} + +void SelfType::MeshExtractor::loadNode( + const std::size_t nodeIdx, + 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, 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()) { + deferAddMesh(*node.mesh, node.skin, joint); + } + if (node.children.has_value()) { + for (const auto &child : *node.children) { + loadNode(child, joint); + } + } +} + +void SelfType::MeshExtractor::loadNodes() +{ + m_loaded_nodes = std::vector(m_gltf_model.nodes->size()); + + std::vector isChild(m_gltf_model.nodes->size()); + for (const auto &node : *m_gltf_model.nodes) { + if (!node.children.has_value()) + continue; + for (const auto &child : *node.children) { + isChild[child] = true; + } + } + // Load all nodes that aren't children. + // Children will be loaded by their parent nodes. + for (std::size_t i = 0; i < m_gltf_model.nodes->size(); ++i) { + if (!isChild[i]) { + loadNode(i, nullptr); + } + } +} + +void SelfType::MeshExtractor::loadSkins() +{ + if (!m_gltf_model.skins.has_value()) + return; + + for (const auto &skin : *m_gltf_model.skins) { + if (!skin.inverseBindMatrices.has_value()) + continue; + const auto accessor = Accessor::make(m_gltf_model, *skin.inverseBindMatrices); + if (accessor.getCount() < skin.joints.size()) + throw std::runtime_error("accessor contains too few matrices"); + for (std::size_t i = 0; i < skin.joints.size(); ++i) { + m_loaded_nodes.at(skin.joints[i])->GlobalInversedMatrix = convertHandedness(accessor.get(i)); + } + } +} + +void SelfType::MeshExtractor::loadAnimation(const std::size_t animIdx) +{ + const auto &anim = m_gltf_model.animations->at(animIdx); + for (const auto &channel : anim.channels) { + + const auto &sampler = anim.samplers.at(channel.sampler); + if (sampler.interpolation != tiniergltf::AnimationSampler::Interpolation::LINEAR) + throw std::runtime_error("unsupported interpolation"); + + const auto inputAccessor = Accessor::make(m_gltf_model, sampler.input); + const auto n_frames = inputAccessor.getCount(); + + if (!channel.target.node.has_value()) + throw std::runtime_error("no animated node"); + + const auto &joint = m_loaded_nodes.at(*channel.target.node); + switch (channel.target.path) { + case tiniergltf::AnimationChannelTarget::Path::TRANSLATION: { + const auto outputAccessor = Accessor::make(m_gltf_model, sampler.output); + for (std::size_t i = 0; i < n_frames; ++i) { + auto *key = m_irr_model->addPositionKey(joint); + key->frame = inputAccessor.get(i); + key->position = convertHandedness(outputAccessor.get(i)); + } + break; + } + case tiniergltf::AnimationChannelTarget::Path::ROTATION: { + const auto outputAccessor = Accessor::make(m_gltf_model, sampler.output); + for (std::size_t i = 0; i < n_frames; ++i) { + auto *key = m_irr_model->addRotationKey(joint); + key->frame = inputAccessor.get(i); + key->rotation = convertHandedness(outputAccessor.get(i)); + } + break; + } + case tiniergltf::AnimationChannelTarget::Path::SCALE: { + const auto outputAccessor = Accessor::make(m_gltf_model, sampler.output); + for (std::size_t i = 0; i < n_frames; ++i) { + auto *key = m_irr_model->addScaleKey(joint); + key->frame = inputAccessor.get(i); + key->scale = outputAccessor.get(i); + } + break; + } + case tiniergltf::AnimationChannelTarget::Path::WEIGHTS: + throw std::runtime_error("no support for morph animations"); + } + } +} + +void SelfType::MeshExtractor::load() +{ + loadNodes(); + for (const auto &load_mesh : m_mesh_loaders) { + load_mesh(); + } + loadSkins(); + // Load the first animation, if there is one. + if (m_gltf_model.animations.has_value()) { + if (m_gltf_model.animations->size() > 1) { + os::Printer::log("glTF loader", + "multiple animations are not supported", ELL_WARNING); + } + loadAnimation(0); + m_irr_model->setAnimationSpeed(1); + } + m_irr_model->finalize(); +} + +/** + * Extracts GLTF mesh indices. + */ +std::optional> SelfType::MeshExtractor::getIndices( + const tiniergltf::MeshPrimitive &primitive) const +{ + const auto accessorIdx = primitive.indices; + if (!accessorIdx.has_value()) + return std::nullopt; // non-indexed geometry + + const auto accessor = ([&]() -> AccessorVariant { + const auto &acc = m_gltf_model.accessors->at(*accessorIdx); + switch (acc.componentType) { + case tiniergltf::Accessor::ComponentType::UNSIGNED_BYTE: + return Accessor::make(m_gltf_model, *accessorIdx); + case tiniergltf::Accessor::ComponentType::UNSIGNED_SHORT: + return Accessor::make(m_gltf_model, *accessorIdx); + case tiniergltf::Accessor::ComponentType::UNSIGNED_INT: + return Accessor::make(m_gltf_model, *accessorIdx); + default: + throw std::runtime_error("invalid component type"); + } + })(); + const auto count = std::visit([](auto &&a) { return a.getCount(); }, accessor); + + std::vector indices; + for (std::size_t i = 0; i < count; ++i) { + // TODO (low-priority, maybe never) also reverse winding order based on determinant of global transform + // FIXME this hack also reverses triangle draw order + std::size_t elemIdx = count - i - 1; // reverse index order + u16 index; + // Note: glTF forbids the max value for each component type. + if (std::holds_alternative>(accessor)) { + index = std::get>(accessor).get(elemIdx); + if (index == std::numeric_limits::max()) + throw std::runtime_error("invalid index"); + } else if (std::holds_alternative>(accessor)) { + index = std::get>(accessor).get(elemIdx); + if (index == std::numeric_limits::max()) + throw std::runtime_error("invalid index"); + } else if (std::holds_alternative>(accessor)) { + u32 indexWide = std::get>(accessor).get(elemIdx); + // Use >= here for consistency. + if (indexWide >= std::numeric_limits::max()) + throw std::runtime_error("index too large (>= 65536)"); + index = static_cast(indexWide); + } + indices.push_back(index); + } + + return indices; +} + +/** + * Create a vector of video::S3DVertex (model data) from a mesh & primitive index. + */ +std::optional> SelfType::MeshExtractor::getVertices( + const tiniergltf::MeshPrimitive &primitive) const +{ + const auto &attributes = primitive.attributes; + const auto positionAccessorIdx = attributes.position; + if (!positionAccessorIdx.has_value()) { + // "When positions are not specified, client implementations SHOULD skip primitive's rendering" + return std::nullopt; + } + + std::vector vertices; + const auto vertexCount = m_gltf_model.accessors->at(*positionAccessorIdx).count; + vertices.resize(vertexCount); + copyPositions(*positionAccessorIdx, vertices); + + const auto normalAccessorIdx = attributes.normal; + if (normalAccessorIdx.has_value()) { + copyNormals(normalAccessorIdx.value(), vertices); + } + // TODO verify that the automatic normal recalculation done in Minetest indeed works correctly + + const auto &texcoords = attributes.texcoord; + if (texcoords.has_value()) { + const auto tCoordAccessorIdx = texcoords->at(0); + copyTCoords(tCoordAccessorIdx, vertices); + } + + return vertices; +} + +/** + * Get the amount of meshes that a model contains. +*/ +std::size_t SelfType::MeshExtractor::getMeshCount() const +{ + return m_gltf_model.meshes->size(); +} + +/** + * Get the amount of primitives that a mesh in a model contains. +*/ +std::size_t SelfType::MeshExtractor::getPrimitiveCount( + const std::size_t meshIdx) const +{ + return m_gltf_model.meshes->at(meshIdx).primitives.size(); +} + +/** + * Streams vertex positions raw data into usable buffer via reference. + * Buffer: ref Vector +*/ +void SelfType::MeshExtractor::copyPositions( + const std::size_t accessorIdx, + std::vector& vertices) const +{ + const auto accessor = Accessor::make(m_gltf_model, accessorIdx); + for (std::size_t i = 0; i < accessor.getCount(); i++) { + vertices[i].Pos = convertHandedness(accessor.get(i)); + } +} + +/** + * Streams normals raw data into usable buffer via reference. + * Buffer: ref Vector +*/ +void SelfType::MeshExtractor::copyNormals( + const std::size_t accessorIdx, + std::vector& vertices) const +{ + const auto accessor = Accessor::make(m_gltf_model, accessorIdx); + for (std::size_t i = 0; i < accessor.getCount(); ++i) { + vertices[i].Normal = convertHandedness(accessor.get(i)); + } +} + +/** + * Streams texture coordinate raw data into usable buffer via reference. + * Buffer: ref Vector +*/ +void SelfType::MeshExtractor::copyTCoords( + const std::size_t accessorIdx, + std::vector& 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>::make(m_gltf_model, accessorIdx); + for (std::size_t i = 0; i < accessor.getCount(); ++i) { + vertices[i].TCoords = core::vector2d(accessor.get(i)); + } + } else { + const auto accessor = createNormalizedValuesAccessor<2>(m_gltf_model, accessorIdx); + const auto count = std::visit([](auto &&a) { return a.getCount(); }, accessor); + for (std::size_t i = 0; i < count; ++i) { + vertices[i].TCoords = core::vector2d(getNormalizedValues(accessor, i)); + } + } +} + +/** + * This is where the actual model's GLTF file is loaded and parsed by tiniergltf. +*/ +std::optional SelfType::tryParseGLTF(io::IReadFile* file) +{ + const bool isGlb = core::hasFileExtension(file->getFileName(), "glb"); + auto size = file->getSize(); + if (size < 0) // this can happen if `ftell` fails + return std::nullopt; + std::unique_ptr buf(new char[size + 1]); + if (file->read(buf.get(), size) != static_cast(size)) + return std::nullopt; + // We probably don't need this, but add it just to be sure. + buf[size] = '\0'; + try { + 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; + } catch (const std::out_of_range &e) { + os::Printer::log("glTF loader", e.what(), ELL_ERROR); + return std::nullopt; + } +} + +} // namespace scene + +} // namespace irr diff --git a/irr/src/CGLTFMeshFileLoader.h b/irr/src/CGLTFMeshFileLoader.h new file mode 100644 index 000000000..7674fd46a --- /dev/null +++ b/irr/src/CGLTFMeshFileLoader.h @@ -0,0 +1,161 @@ +// Minetest +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "CSkinnedMesh.h" +#include "IMeshLoader.h" +#include "IReadFile.h" +#include "irrTypes.h" +#include "path.h" +#include "S3DVertex.h" + +#include "tiniergltf.hpp" + +#include +#include +#include +#include + +namespace irr +{ + +namespace scene +{ + +class CGLTFMeshFileLoader : public IMeshLoader +{ +public: + CGLTFMeshFileLoader() noexcept {}; + + bool isALoadableFileExtension(const io::path &filename) const override; + + IAnimatedMesh *createMesh(io::IReadFile *file) override; + +private: + template + static T rawget(const char *ptr); + + template + class Accessor + { + struct BufferSource + { + const char *ptr; + std::size_t byteStride; + }; + using Source = std::variant, std::tuple<>>; + + public: + static Accessor sparseIndices( + const tiniergltf::GlTF &model, + const tiniergltf::AccessorSparseIndices &indices, + const std::size_t count); + static Accessor sparseValues( + const tiniergltf::GlTF &model, + const tiniergltf::AccessorSparseValues &values, + const std::size_t count, + const std::size_t defaultByteStride); + static Accessor base( + const tiniergltf::GlTF &model, + std::size_t accessorIdx); + static Accessor make(const tiniergltf::GlTF &model, std::size_t accessorIdx); + static constexpr tiniergltf::Accessor::Type getType(); + static constexpr tiniergltf::Accessor::ComponentType getComponentType(); + std::size_t getCount() const { return count; } + T get(std::size_t i) const; + + private: + Accessor(const char *ptr, std::size_t byteStride, std::size_t count) : + source(BufferSource{ptr, byteStride}), count(count) {} + Accessor(std::vector vec, std::size_t count) : + source(vec), count(count) {} + Accessor(std::size_t count) : + source(std::make_tuple()), count(count) {} + // Directly from buffer, sparse, or default-initialized + const Source source; + const std::size_t count; + }; + + template + using AccessorVariant = std::variant...>; + + template + using ArrayAccessorVariant = std::variant>...>; + + template + using NormalizedValuesAccessor = ArrayAccessorVariant; + + template + static NormalizedValuesAccessor createNormalizedValuesAccessor( + const tiniergltf::GlTF &model, + const std::size_t accessorIdx); + + template + static std::array getNormalizedValues( + const NormalizedValuesAccessor &accessor, + const std::size_t i); + + class MeshExtractor + { + public: + MeshExtractor(tiniergltf::GlTF &&model, + CSkinnedMesh *mesh) noexcept + : m_gltf_model(std::move(model)), m_irr_model(mesh) {}; + + /* Gets indices for the given mesh/primitive. + * + * Values are return in Irrlicht winding order. + */ + std::optional> getIndices( + const tiniergltf::MeshPrimitive &primitive) const; + + std::optional> getVertices( + const tiniergltf::MeshPrimitive &primitive) const; + + std::size_t getMeshCount() const; + + std::size_t getPrimitiveCount(const std::size_t meshIdx) const; + + void load(); + + private: + const tiniergltf::GlTF m_gltf_model; + CSkinnedMesh *m_irr_model; + + std::vector> m_mesh_loaders; + std::vector m_loaded_nodes; + + void copyPositions(const std::size_t accessorIdx, + std::vector& vertices) const; + + void copyNormals(const std::size_t accessorIdx, + std::vector& vertices) const; + + void copyTCoords(const std::size_t accessorIdx, + std::vector& vertices) const; + + void addPrimitive(const tiniergltf::MeshPrimitive &primitive, + const std::optional skinIdx, + CSkinnedMesh::SJoint *parent); + + void deferAddMesh(const std::size_t meshIdx, + const std::optional skinIdx, + CSkinnedMesh::SJoint *parentJoint); + + void loadNode(const std::size_t nodeIdx, CSkinnedMesh::SJoint *parentJoint); + + void loadNodes(); + + void loadSkins(); + + void loadAnimation(const std::size_t animIdx); + }; + + std::optional tryParseGLTF(io::IReadFile *file); +}; + +} // namespace scene + +} // namespace irr + diff --git a/irr/src/CGUISkin.cpp b/irr/src/CGUISkin.cpp index 84ceaeabf..e9721a5fa 100644 --- a/irr/src/CGUISkin.cpp +++ b/irr/src/CGUISkin.cpp @@ -1,4 +1,6 @@ // Copyright (C) 2002-2012 Nikolaus Gebhardt +// Copyright (C) 2019 Irrlick +// // This file is part of the "Irrlicht Engine". // For conditions of distribution and use, see copyright notice in irrlicht.h @@ -15,39 +17,41 @@ namespace irr namespace gui { -CGUISkin::CGUISkin(EGUI_SKIN_TYPE type, video::IVideoDriver *driver) : - SpriteBank(0), Driver(driver), Type(type) +CGUISkin::CGUISkin(EGUI_SKIN_TYPE type, video::IVideoDriver* driver) +: SpriteBank(0), Driver(driver), Type(type) { -#ifdef _DEBUG + #ifdef _DEBUG setDebugName("CGUISkin"); -#endif + #endif + + if ((Type == EGST_WINDOWS_CLASSIC) || (Type == EGST_WINDOWS_METALLIC)) + { + Colors[EGDC_3D_DARK_SHADOW] = video::SColor(101,50,50,50); + Colors[EGDC_3D_SHADOW] = video::SColor(101,130,130,130); + Colors[EGDC_3D_FACE] = video::SColor(220,100,100,100); + Colors[EGDC_3D_HIGH_LIGHT] = video::SColor(101,255,255,255); + Colors[EGDC_3D_LIGHT] = video::SColor(101,210,210,210); + Colors[EGDC_ACTIVE_BORDER] = video::SColor(101,16,14,115); + Colors[EGDC_ACTIVE_CAPTION] = video::SColor(255,255,255,255); + Colors[EGDC_APP_WORKSPACE] = video::SColor(101,100,100,100); + Colors[EGDC_BUTTON_TEXT] = video::SColor(240,10,10,10); + Colors[EGDC_GRAY_TEXT] = video::SColor(240,130,130,130); + Colors[EGDC_HIGH_LIGHT] = video::SColor(101,8,36,107); + Colors[EGDC_HIGH_LIGHT_TEXT] = video::SColor(240,255,255,255); + Colors[EGDC_INACTIVE_BORDER] = video::SColor(101,165,165,165); + Colors[EGDC_INACTIVE_CAPTION] = video::SColor(255,30,30,30); + Colors[EGDC_TOOLTIP] = video::SColor(200,0,0,0); + Colors[EGDC_TOOLTIP_BACKGROUND] = video::SColor(200,255,255,225); + Colors[EGDC_SCROLLBAR] = video::SColor(101,230,230,230); + Colors[EGDC_WINDOW] = video::SColor(101,255,255,255); + Colors[EGDC_WINDOW_SYMBOL] = video::SColor(200,10,10,10); + Colors[EGDC_ICON] = video::SColor(200,255,255,255); + Colors[EGDC_ICON_HIGH_LIGHT] = video::SColor(200,8,36,107); + Colors[EGDC_GRAY_WINDOW_SYMBOL] = video::SColor(240,100,100,100); + Colors[EGDC_EDITABLE] = video::SColor(255,255,255,255); + Colors[EGDC_GRAY_EDITABLE] = video::SColor(255,120,120,120); + Colors[EGDC_FOCUSED_EDITABLE] = video::SColor(255,240,240,255); - if ((Type == EGST_WINDOWS_CLASSIC) || (Type == EGST_WINDOWS_METALLIC)) { - Colors[EGDC_3D_DARK_SHADOW] = video::SColor(101, 50, 50, 50); - Colors[EGDC_3D_SHADOW] = video::SColor(101, 130, 130, 130); - Colors[EGDC_3D_FACE] = video::SColor(101, 210, 210, 210); - Colors[EGDC_3D_HIGH_LIGHT] = video::SColor(101, 255, 255, 255); - Colors[EGDC_3D_LIGHT] = video::SColor(101, 210, 210, 210); - Colors[EGDC_ACTIVE_BORDER] = video::SColor(101, 16, 14, 115); - Colors[EGDC_ACTIVE_CAPTION] = video::SColor(255, 255, 255, 255); - Colors[EGDC_APP_WORKSPACE] = video::SColor(101, 100, 100, 100); - Colors[EGDC_BUTTON_TEXT] = video::SColor(240, 10, 10, 10); - Colors[EGDC_GRAY_TEXT] = video::SColor(240, 130, 130, 130); - Colors[EGDC_HIGH_LIGHT] = video::SColor(101, 8, 36, 107); - Colors[EGDC_HIGH_LIGHT_TEXT] = video::SColor(240, 255, 255, 255); - Colors[EGDC_INACTIVE_BORDER] = video::SColor(101, 165, 165, 165); - Colors[EGDC_INACTIVE_CAPTION] = video::SColor(255, 30, 30, 30); - Colors[EGDC_TOOLTIP] = video::SColor(200, 0, 0, 0); - Colors[EGDC_TOOLTIP_BACKGROUND] = video::SColor(200, 255, 255, 225); - Colors[EGDC_SCROLLBAR] = video::SColor(101, 230, 230, 230); - Colors[EGDC_WINDOW] = video::SColor(101, 255, 255, 255); - Colors[EGDC_WINDOW_SYMBOL] = video::SColor(200, 10, 10, 10); - Colors[EGDC_ICON] = video::SColor(200, 255, 255, 255); - Colors[EGDC_ICON_HIGH_LIGHT] = video::SColor(200, 8, 36, 107); - Colors[EGDC_GRAY_WINDOW_SYMBOL] = video::SColor(240, 100, 100, 100); - Colors[EGDC_EDITABLE] = video::SColor(255, 255, 255, 255); - Colors[EGDC_GRAY_EDITABLE] = video::SColor(255, 120, 120, 120); - Colors[EGDC_FOCUSED_EDITABLE] = video::SColor(255, 240, 240, 255); Sizes[EGDS_SCROLLBAR_SIZE] = 14; Sizes[EGDS_MENU_HEIGHT] = 30; @@ -63,34 +67,36 @@ CGUISkin::CGUISkin(EGUI_SKIN_TYPE type, video::IVideoDriver *driver) : Sizes[EGDS_TITLEBARTEXT_DISTANCE_X] = 2; Sizes[EGDS_TITLEBARTEXT_DISTANCE_Y] = 0; - } else { - // 0x80a6a8af - Colors[EGDC_3D_DARK_SHADOW] = 0x60767982; - // Colors[EGDC_3D_FACE] = 0xc0c9ccd4; // tab background - Colors[EGDC_3D_FACE] = 0xc0cbd2d9; // tab background - Colors[EGDC_3D_SHADOW] = 0x50e4e8f1; // tab background, and left-top highlight - Colors[EGDC_3D_HIGH_LIGHT] = 0x40c7ccdc; - Colors[EGDC_3D_LIGHT] = 0x802e313a; - Colors[EGDC_ACTIVE_BORDER] = 0x80404040; // window title - Colors[EGDC_ACTIVE_CAPTION] = 0xffd0d0d0; - Colors[EGDC_APP_WORKSPACE] = 0xc0646464; // unused - Colors[EGDC_BUTTON_TEXT] = 0xd0161616; - Colors[EGDC_GRAY_TEXT] = 0x3c141414; - Colors[EGDC_HIGH_LIGHT] = 0x6c606060; - Colors[EGDC_HIGH_LIGHT_TEXT] = 0xd0e0e0e0; - Colors[EGDC_INACTIVE_BORDER] = 0xf0a5a5a5; - Colors[EGDC_INACTIVE_CAPTION] = 0xffd2d2d2; - Colors[EGDC_TOOLTIP] = 0xf00f2033; - Colors[EGDC_TOOLTIP_BACKGROUND] = 0xc0cbd2d9; - Colors[EGDC_SCROLLBAR] = 0xf0e0e0e0; - Colors[EGDC_WINDOW] = 0xf0f0f0f0; - Colors[EGDC_WINDOW_SYMBOL] = 0xd0161616; - Colors[EGDC_ICON] = 0xd0161616; - Colors[EGDC_ICON_HIGH_LIGHT] = 0xd0606060; - Colors[EGDC_GRAY_WINDOW_SYMBOL] = 0x3c101010; - Colors[EGDC_EDITABLE] = 0xf0ffffff; - Colors[EGDC_GRAY_EDITABLE] = 0xf0cccccc; - Colors[EGDC_FOCUSED_EDITABLE] = 0xf0fffff0; + } + else + { + //0x80a6a8af + Colors[EGDC_3D_DARK_SHADOW] = 0x60767982; + //Colors[EGDC_3D_FACE] = 0xc0c9ccd4; // tab background + Colors[EGDC_3D_FACE] = 0xc0cbd2d9; // tab background + Colors[EGDC_3D_SHADOW] = 0x50e4e8f1; // tab background, and left-top highlight + Colors[EGDC_3D_HIGH_LIGHT] = 0x40c7ccdc; + Colors[EGDC_3D_LIGHT] = 0x802e313a; + Colors[EGDC_ACTIVE_BORDER] = 0x80404040; // window title + Colors[EGDC_ACTIVE_CAPTION] = 0xffd0d0d0; + Colors[EGDC_APP_WORKSPACE] = 0xc0646464; // unused + Colors[EGDC_BUTTON_TEXT] = 0xd0161616; + Colors[EGDC_GRAY_TEXT] = 0x3c141414; + Colors[EGDC_HIGH_LIGHT] = 0x6c606060; + Colors[EGDC_HIGH_LIGHT_TEXT] = 0xd0e0e0e0; + Colors[EGDC_INACTIVE_BORDER] = 0xf0a5a5a5; + Colors[EGDC_INACTIVE_CAPTION] = 0xffd2d2d2; + Colors[EGDC_TOOLTIP] = 0xf00f2033; + Colors[EGDC_TOOLTIP_BACKGROUND] = 0xc0cbd2d9; + Colors[EGDC_SCROLLBAR] = 0xf0e0e0e0; + Colors[EGDC_WINDOW] = 0xf0f0f0f0; + Colors[EGDC_WINDOW_SYMBOL] = 0xd0161616; + Colors[EGDC_ICON] = 0xd0161616; + Colors[EGDC_ICON_HIGH_LIGHT] = 0xd0606060; + Colors[EGDC_GRAY_WINDOW_SYMBOL] = 0x3c101010; + Colors[EGDC_EDITABLE] = 0xf0ffffff; + Colors[EGDC_GRAY_EDITABLE] = 0xf0cccccc; + Colors[EGDC_FOCUSED_EDITABLE] = 0xf0fffff0; Sizes[EGDS_SCROLLBAR_SIZE] = 14; Sizes[EGDS_MENU_HEIGHT] = 48; @@ -118,8 +124,6 @@ CGUISkin::CGUISkin(EGUI_SKIN_TYPE type, video::IVideoDriver *driver) : Sizes[EGDS_BUTTON_PRESSED_IMAGE_OFFSET_Y] = 1; Sizes[EGDS_BUTTON_PRESSED_TEXT_OFFSET_X] = 0; Sizes[EGDS_BUTTON_PRESSED_TEXT_OFFSET_Y] = 2; - Sizes[EGDS_BUTTON_PRESSED_SPRITE_OFFSET_X] = 0; - Sizes[EGDS_BUTTON_PRESSED_SPRITE_OFFSET_Y] = 0; Texts[EGDT_MSG_BOX_OK] = L"OK"; Texts[EGDT_MSG_BOX_CANCEL] = L"Cancel"; @@ -155,16 +159,18 @@ CGUISkin::CGUISkin(EGUI_SKIN_TYPE type, video::IVideoDriver *driver) : Icons[EGDI_FILE] = 245; Icons[EGDI_DIRECTORY] = 246; - for (u32 i = 0; i < EGDF_COUNT; ++i) + for (u32 i=0; idrop(); } @@ -173,6 +179,7 @@ CGUISkin::~CGUISkin() SpriteBank->drop(); } + //! returns default color video::SColor CGUISkin::getColor(EGUI_DEFAULT_COLOR color) const { @@ -182,6 +189,7 @@ video::SColor CGUISkin::getColor(EGUI_DEFAULT_COLOR color) const return video::SColor(); } + //! sets a default color void CGUISkin::setColor(EGUI_DEFAULT_COLOR which, video::SColor newColor) { @@ -189,6 +197,7 @@ void CGUISkin::setColor(EGUI_DEFAULT_COLOR which, video::SColor newColor) Colors[which] = newColor; } + //! returns size for the given size type s32 CGUISkin::getSize(EGUI_DEFAULT_SIZE size) const { @@ -198,6 +207,7 @@ s32 CGUISkin::getSize(EGUI_DEFAULT_SIZE size) const return 0; } + //! sets a default size void CGUISkin::setSize(EGUI_DEFAULT_SIZE which, s32 size) { @@ -205,8 +215,9 @@ void CGUISkin::setSize(EGUI_DEFAULT_SIZE which, s32 size) Sizes[which] = size; } + //! returns the default font -IGUIFont *CGUISkin::getFont(EGUI_DEFAULT_FONT which) const +IGUIFont* CGUISkin::getFont(EGUI_DEFAULT_FONT which) const { if (((u32)which < EGDF_COUNT) && Fonts[which]) return Fonts[which]; @@ -214,13 +225,15 @@ IGUIFont *CGUISkin::getFont(EGUI_DEFAULT_FONT which) const return Fonts[EGDF_DEFAULT]; } + //! sets a default font -void CGUISkin::setFont(IGUIFont *font, EGUI_DEFAULT_FONT which) +void CGUISkin::setFont(IGUIFont* font, EGUI_DEFAULT_FONT which) { if ((u32)which >= EGDF_COUNT) return; - if (font) { + if (font) + { font->grab(); if (Fonts[which]) Fonts[which]->drop(); @@ -229,14 +242,16 @@ void CGUISkin::setFont(IGUIFont *font, EGUI_DEFAULT_FONT which) } } + //! gets the sprite bank stored -IGUISpriteBank *CGUISkin::getSpriteBank() const +IGUISpriteBank* CGUISkin::getSpriteBank() const { return SpriteBank; } + //! set a new sprite bank or remove one by passing 0 -void CGUISkin::setSpriteBank(IGUISpriteBank *bank) +void CGUISkin::setSpriteBank(IGUISpriteBank* bank) { if (bank) bank->grab(); @@ -247,6 +262,7 @@ void CGUISkin::setSpriteBank(IGUISpriteBank *bank) SpriteBank = bank; } + //! Returns a default icon u32 CGUISkin::getIcon(EGUI_DEFAULT_ICON icon) const { @@ -256,6 +272,7 @@ u32 CGUISkin::getIcon(EGUI_DEFAULT_ICON icon) const return 0; } + //! Sets a default icon void CGUISkin::setIcon(EGUI_DEFAULT_ICON icon, u32 index) { @@ -263,9 +280,10 @@ void CGUISkin::setIcon(EGUI_DEFAULT_ICON icon, u32 index) Icons[icon] = index; } + //! Returns a default text. For example for Message box button captions: //! "OK", "Cancel", "Yes", "No" and so on. -const wchar_t *CGUISkin::getDefaultText(EGUI_DEFAULT_TEXT text) const +const wchar_t* CGUISkin::getDefaultText(EGUI_DEFAULT_TEXT text) const { if ((u32)text < EGDT_COUNT) return Texts[text].c_str(); @@ -273,14 +291,16 @@ const wchar_t *CGUISkin::getDefaultText(EGUI_DEFAULT_TEXT text) const return Texts[0].c_str(); } + //! Sets a default text. For example for Message box button captions: //! "OK", "Cancel", "Yes", "No" and so on. -void CGUISkin::setDefaultText(EGUI_DEFAULT_TEXT which, const wchar_t *newText) +void CGUISkin::setDefaultText(EGUI_DEFAULT_TEXT which, const wchar_t* newText) { if ((u32)which < EGDT_COUNT) Texts[which] = newText; } + //! draws a standard 3d button pane /** Used for drawing for example buttons in normal state. It uses the colors EGDC_3D_DARK_SHADOW, EGDC_3D_HIGH_LIGHT, EGDC_3D_SHADOW and @@ -290,46 +310,58 @@ EGDC_3D_FACE for this. See EGUI_DEFAULT_COLOR for details. \param element: Pointer to the element which wishes to draw this. This parameter is usually not used by ISkin, but can be used for example by more complex implementations to find out how to draw the part exactly. */ -void CGUISkin::draw3DButtonPaneStandard(IGUIElement *element, - const core::rect &r, - const core::rect *clip) +// PATCH +void CGUISkin::drawColored3DButtonPaneStandard(IGUIElement* element, + const core::rect& r, + const core::rect* clip, + const video::SColor* colors) { if (!Driver) return; + if (!colors) + colors = Colors; + core::rect rect = r; - if (Type == EGST_BURNING_SKIN) { + if ( Type == EGST_BURNING_SKIN ) + { rect.UpperLeftCorner.X -= 1; rect.UpperLeftCorner.Y -= 1; rect.LowerRightCorner.X += 1; rect.LowerRightCorner.Y += 1; draw3DSunkenPane(element, - getColor(EGDC_WINDOW).getInterpolated(0xFFFFFFFF, 0.9f), false, true, rect, clip); + colors[ EGDC_WINDOW ].getInterpolated( 0xFFFFFFFF, 0.9f ) + ,false, true, rect, clip); return; } - Driver->draw2DRectangle(getColor(EGDC_3D_DARK_SHADOW), rect, clip); + Driver->draw2DRectangle(colors[EGDC_3D_DARK_SHADOW], rect, clip); rect.LowerRightCorner.X -= 1; rect.LowerRightCorner.Y -= 1; - Driver->draw2DRectangle(getColor(EGDC_3D_HIGH_LIGHT), rect, clip); + Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], rect, clip); rect.UpperLeftCorner.X += 1; rect.UpperLeftCorner.Y += 1; - Driver->draw2DRectangle(getColor(EGDC_3D_SHADOW), rect, clip); + Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], rect, clip); rect.LowerRightCorner.X -= 1; rect.LowerRightCorner.Y -= 1; - if (!UseGradient) { - Driver->draw2DRectangle(getColor(EGDC_3D_FACE), rect, clip); - } else { - const video::SColor c1 = getColor(EGDC_3D_FACE); - const video::SColor c2 = c1.getInterpolated(getColor(EGDC_3D_DARK_SHADOW), 0.4f); + if (!UseGradient) + { + Driver->draw2DRectangle(colors[EGDC_3D_FACE], rect, clip); + } + else + { + const video::SColor c1 = colors[EGDC_3D_FACE]; + const video::SColor c2 = c1.getInterpolated(colors[EGDC_3D_DARK_SHADOW], 0.4f); Driver->draw2DRectangle(rect, c1, c1, c2, c2, clip); } } +// END PATCH + //! draws a pressed 3d button pane /** Used for drawing for example buttons in pressed state. @@ -340,35 +372,45 @@ EGDC_3D_FACE for this. See EGUI_DEFAULT_COLOR for details. \param element: Pointer to the element which wishes to draw this. This parameter is usually not used by ISkin, but can be used for example by more complex implementations to find out how to draw the part exactly. */ -void CGUISkin::draw3DButtonPanePressed(IGUIElement *element, - const core::rect &r, - const core::rect *clip) +// PATCH +void CGUISkin::drawColored3DButtonPanePressed(IGUIElement* element, + const core::rect& r, + const core::rect* clip, + const video::SColor* colors) { if (!Driver) return; + if (!colors) + colors = Colors; + core::rect rect = r; - Driver->draw2DRectangle(getColor(EGDC_3D_HIGH_LIGHT), rect, clip); + Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], rect, clip); rect.LowerRightCorner.X -= 1; rect.LowerRightCorner.Y -= 1; - Driver->draw2DRectangle(getColor(EGDC_3D_DARK_SHADOW), rect, clip); + Driver->draw2DRectangle(colors[EGDC_3D_DARK_SHADOW], rect, clip); rect.UpperLeftCorner.X += 1; rect.UpperLeftCorner.Y += 1; - Driver->draw2DRectangle(getColor(EGDC_3D_SHADOW), rect, clip); + Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], rect, clip); rect.UpperLeftCorner.X += 1; rect.UpperLeftCorner.Y += 1; - if (!UseGradient) { - Driver->draw2DRectangle(getColor(EGDC_3D_FACE), rect, clip); - } else { - const video::SColor c1 = getColor(EGDC_3D_FACE); - const video::SColor c2 = c1.getInterpolated(getColor(EGDC_3D_DARK_SHADOW), 0.4f); + if (!UseGradient) + { + Driver->draw2DRectangle(colors[EGDC_3D_FACE], rect, clip); + } + else + { + const video::SColor c1 = colors[EGDC_3D_FACE]; + const video::SColor c2 = c1.getInterpolated(colors[EGDC_3D_DARK_SHADOW], 0.4f); Driver->draw2DRectangle(rect, c1, c1, c2, c2, clip); } } +// END PATCH + //! draws a sunken 3d pane /** Used for drawing the background of edit, combo or check boxes. @@ -380,112 +422,130 @@ implementations to find out how to draw the part exactly. deep into the ground. \param rect: Defining area where to draw. \param clip: Clip area. */ -void CGUISkin::draw3DSunkenPane(IGUIElement *element, video::SColor bgcolor, - bool flat, bool fillBackGround, - const core::rect &r, - const core::rect *clip) +// PATCH +void CGUISkin::drawColored3DSunkenPane(IGUIElement* element, video::SColor bgcolor, + bool flat, bool fillBackGround, + const core::rect& r, + const core::rect* clip, + const video::SColor* colors) { if (!Driver) return; + if (!colors) + colors = Colors; + core::rect rect = r; if (fillBackGround) Driver->draw2DRectangle(bgcolor, rect, clip); - if (flat) { + if (flat) + { // draw flat sunken pane rect.LowerRightCorner.Y = rect.UpperLeftCorner.Y + 1; - Driver->draw2DRectangle(getColor(EGDC_3D_SHADOW), rect, clip); // top + Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], rect, clip); // top ++rect.UpperLeftCorner.Y; rect.LowerRightCorner.Y = r.LowerRightCorner.Y; rect.LowerRightCorner.X = rect.UpperLeftCorner.X + 1; - Driver->draw2DRectangle(getColor(EGDC_3D_SHADOW), rect, clip); // left + Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], rect, clip); // left rect = r; ++rect.UpperLeftCorner.Y; rect.UpperLeftCorner.X = rect.LowerRightCorner.X - 1; - Driver->draw2DRectangle(getColor(EGDC_3D_HIGH_LIGHT), rect, clip); // right + Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], rect, clip); // right rect = r; ++rect.UpperLeftCorner.X; rect.UpperLeftCorner.Y = r.LowerRightCorner.Y - 1; --rect.LowerRightCorner.X; - Driver->draw2DRectangle(getColor(EGDC_3D_HIGH_LIGHT), rect, clip); // bottom - } else { + Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], rect, clip); // bottom + } + else + { // draw deep sunken pane rect.LowerRightCorner.Y = rect.UpperLeftCorner.Y + 1; - Driver->draw2DRectangle(getColor(EGDC_3D_SHADOW), rect, clip); // top + Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], rect, clip); // top ++rect.UpperLeftCorner.X; ++rect.UpperLeftCorner.Y; --rect.LowerRightCorner.X; ++rect.LowerRightCorner.Y; - Driver->draw2DRectangle(getColor(EGDC_3D_DARK_SHADOW), rect, clip); + Driver->draw2DRectangle(colors[EGDC_3D_DARK_SHADOW], rect, clip); rect.UpperLeftCorner.X = r.UpperLeftCorner.X; - rect.UpperLeftCorner.Y = r.UpperLeftCorner.Y + 1; + rect.UpperLeftCorner.Y = r.UpperLeftCorner.Y+1; rect.LowerRightCorner.X = rect.UpperLeftCorner.X + 1; rect.LowerRightCorner.Y = r.LowerRightCorner.Y; - Driver->draw2DRectangle(getColor(EGDC_3D_SHADOW), rect, clip); // left + Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], rect, clip); // left ++rect.UpperLeftCorner.X; ++rect.UpperLeftCorner.Y; ++rect.LowerRightCorner.X; --rect.LowerRightCorner.Y; - Driver->draw2DRectangle(getColor(EGDC_3D_DARK_SHADOW), rect, clip); + Driver->draw2DRectangle(colors[EGDC_3D_DARK_SHADOW], rect, clip); rect = r; rect.UpperLeftCorner.X = rect.LowerRightCorner.X - 1; ++rect.UpperLeftCorner.Y; - Driver->draw2DRectangle(getColor(EGDC_3D_HIGH_LIGHT), rect, clip); // right + Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], rect, clip); // right --rect.UpperLeftCorner.X; ++rect.UpperLeftCorner.Y; --rect.LowerRightCorner.X; --rect.LowerRightCorner.Y; - Driver->draw2DRectangle(getColor(EGDC_3D_LIGHT), rect, clip); + Driver->draw2DRectangle(colors[EGDC_3D_LIGHT], rect, clip); rect = r; ++rect.UpperLeftCorner.X; rect.UpperLeftCorner.Y = r.LowerRightCorner.Y - 1; --rect.LowerRightCorner.X; - Driver->draw2DRectangle(getColor(EGDC_3D_HIGH_LIGHT), rect, clip); // bottom + Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], rect, clip); // bottom ++rect.UpperLeftCorner.X; --rect.UpperLeftCorner.Y; --rect.LowerRightCorner.X; --rect.LowerRightCorner.Y; - Driver->draw2DRectangle(getColor(EGDC_3D_LIGHT), rect, clip); + Driver->draw2DRectangle(colors[EGDC_3D_LIGHT], rect, clip); } } +// END PATCH //! draws a window background // return where to draw title bar text. -core::rect CGUISkin::draw3DWindowBackground(IGUIElement *element, - bool drawTitleBar, video::SColor titleBarColor, - const core::rect &r, - const core::rect *clip, - core::rect *checkClientArea) +// PATCH +core::rect CGUISkin::drawColored3DWindowBackground(IGUIElement* element, + bool drawTitleBar, video::SColor titleBarColor, + const core::rect& r, + const core::rect* clip, + core::rect* checkClientArea, + const video::SColor* colors) { - if (!Driver) { - if (checkClientArea) { + if (!Driver) + { + if ( checkClientArea ) + { *checkClientArea = r; } return r; } + if (!colors) + colors = Colors; + core::rect rect = r; // top border rect.LowerRightCorner.Y = rect.UpperLeftCorner.Y + 1; - if (!checkClientArea) { - Driver->draw2DRectangle(getColor(EGDC_3D_HIGH_LIGHT), rect, clip); + if ( !checkClientArea ) + { + Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], rect, clip); } // left border rect.LowerRightCorner.Y = r.LowerRightCorner.Y; rect.LowerRightCorner.X = rect.UpperLeftCorner.X + 1; - if (!checkClientArea) { - Driver->draw2DRectangle(getColor(EGDC_3D_HIGH_LIGHT), rect, clip); + if ( !checkClientArea ) + { + Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], rect, clip); } // right border dark outer line @@ -493,8 +553,9 @@ core::rect CGUISkin::draw3DWindowBackground(IGUIElement *element, rect.LowerRightCorner.X = r.LowerRightCorner.X; rect.UpperLeftCorner.Y = r.UpperLeftCorner.Y; rect.LowerRightCorner.Y = r.LowerRightCorner.Y; - if (!checkClientArea) { - Driver->draw2DRectangle(getColor(EGDC_3D_DARK_SHADOW), rect, clip); + if ( !checkClientArea ) + { + Driver->draw2DRectangle(colors[EGDC_3D_DARK_SHADOW], rect, clip); } // right border bright innner line @@ -502,8 +563,9 @@ core::rect CGUISkin::draw3DWindowBackground(IGUIElement *element, rect.LowerRightCorner.X -= 1; rect.UpperLeftCorner.Y += 1; rect.LowerRightCorner.Y -= 1; - if (!checkClientArea) { - Driver->draw2DRectangle(getColor(EGDC_3D_SHADOW), rect, clip); + if ( !checkClientArea ) + { + Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], rect, clip); } // bottom border dark outer line @@ -511,8 +573,9 @@ core::rect CGUISkin::draw3DWindowBackground(IGUIElement *element, rect.UpperLeftCorner.Y = r.LowerRightCorner.Y - 1; rect.LowerRightCorner.Y = r.LowerRightCorner.Y; rect.LowerRightCorner.X = r.LowerRightCorner.X; - if (!checkClientArea) { - Driver->draw2DRectangle(getColor(EGDC_3D_DARK_SHADOW), rect, clip); + if ( !checkClientArea ) + { + Driver->draw2DRectangle(colors[EGDC_3D_DARK_SHADOW], rect, clip); } // bottom border bright inner line @@ -520,31 +583,39 @@ core::rect CGUISkin::draw3DWindowBackground(IGUIElement *element, rect.LowerRightCorner.X -= 1; rect.UpperLeftCorner.Y -= 1; rect.LowerRightCorner.Y -= 1; - if (!checkClientArea) { - Driver->draw2DRectangle(getColor(EGDC_3D_SHADOW), rect, clip); + if ( !checkClientArea ) + { + Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], rect, clip); } // client area for background rect = r; - rect.UpperLeftCorner.X += 1; - rect.UpperLeftCorner.Y += 1; + rect.UpperLeftCorner.X +=1; + rect.UpperLeftCorner.Y +=1; rect.LowerRightCorner.X -= 2; rect.LowerRightCorner.Y -= 2; - if (checkClientArea) { + if (checkClientArea) + { *checkClientArea = rect; } - if (!checkClientArea) { - if (!UseGradient) { - Driver->draw2DRectangle(getColor(EGDC_3D_FACE), rect, clip); - } else if (Type == EGST_BURNING_SKIN) { - const video::SColor c1 = getColor(EGDC_WINDOW).getInterpolated(0xFFFFFFFF, 0.9f); - const video::SColor c2 = getColor(EGDC_WINDOW).getInterpolated(0xFFFFFFFF, 0.8f); + if ( !checkClientArea ) + { + if (!UseGradient) + { + Driver->draw2DRectangle(colors[EGDC_3D_FACE], rect, clip); + } + else if ( Type == EGST_BURNING_SKIN ) + { + const video::SColor c1 = colors[EGDC_WINDOW].getInterpolated ( 0xFFFFFFFF, 0.9f ); + const video::SColor c2 = colors[EGDC_WINDOW].getInterpolated ( 0xFFFFFFFF, 0.8f ); Driver->draw2DRectangle(rect, c1, c1, c2, c2, clip); - } else { - const video::SColor c2 = getColor(EGDC_3D_SHADOW); - const video::SColor c1 = getColor(EGDC_3D_FACE); + } + else + { + const video::SColor c2 = colors[EGDC_3D_SHADOW]; + const video::SColor c1 = colors[EGDC_3D_FACE]; Driver->draw2DRectangle(rect, c1, c1, c1, c2, clip); } } @@ -556,19 +627,26 @@ core::rect CGUISkin::draw3DWindowBackground(IGUIElement *element, rect.LowerRightCorner.X -= 2; rect.LowerRightCorner.Y = rect.UpperLeftCorner.Y + getSize(EGDS_WINDOW_BUTTON_WIDTH) + 2; - if (drawTitleBar) { - if (checkClientArea) { + if (drawTitleBar ) + { + if (checkClientArea) + { (*checkClientArea).UpperLeftCorner.Y = rect.LowerRightCorner.Y; - } else { + } + else + { // draw title bar - // if (!UseGradient) + //if (!UseGradient) // Driver->draw2DRectangle(titleBarColor, rect, clip); - // else - if (Type == EGST_BURNING_SKIN) { - const video::SColor c = titleBarColor.getInterpolated(video::SColor(titleBarColor.getAlpha(), 255, 255, 255), 0.8f); + //else + if ( Type == EGST_BURNING_SKIN ) + { + const video::SColor c = titleBarColor.getInterpolated( video::SColor(titleBarColor.getAlpha(),255,255,255), 0.8f); Driver->draw2DRectangle(rect, titleBarColor, titleBarColor, c, c, clip); - } else { - const video::SColor c = titleBarColor.getInterpolated(video::SColor(titleBarColor.getAlpha(), 0, 0, 0), 0.2f); + } + else + { + const video::SColor c = titleBarColor.getInterpolated(video::SColor(titleBarColor.getAlpha(),0,0,0), 0.2f); Driver->draw2DRectangle(rect, titleBarColor, c, titleBarColor, c, clip); } } @@ -576,6 +654,8 @@ core::rect CGUISkin::draw3DWindowBackground(IGUIElement *element, return rect; } +// END PATCH + //! draws a standard 3d menu pane /** Used for drawing for menus and context menus. @@ -586,15 +666,21 @@ is usually not used by ISkin, but can be used for example by more complex implementations to find out how to draw the part exactly. \param rect: Defining area where to draw. \param clip: Clip area. */ -void CGUISkin::draw3DMenuPane(IGUIElement *element, - const core::rect &r, const core::rect *clip) +// PATCH +void CGUISkin::drawColored3DMenuPane(IGUIElement* element, + const core::rect& r, const core::rect* clip, + const video::SColor* colors) { if (!Driver) return; + if (!colors) + colors = Colors; + core::rect rect = r; - if (Type == EGST_BURNING_SKIN) { + if ( Type == EGST_BURNING_SKIN ) + { rect.UpperLeftCorner.Y -= 3; draw3DButtonPaneStandard(element, rect, clip); return; @@ -609,50 +695,53 @@ void CGUISkin::draw3DMenuPane(IGUIElement *element, // but there aren't that much menus visible anyway. rect.LowerRightCorner.Y = rect.UpperLeftCorner.Y + 1; - Driver->draw2DRectangle(getColor(EGDC_3D_HIGH_LIGHT), rect, clip); + Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], rect, clip); rect.LowerRightCorner.Y = r.LowerRightCorner.Y; rect.LowerRightCorner.X = rect.UpperLeftCorner.X + 1; - Driver->draw2DRectangle(getColor(EGDC_3D_HIGH_LIGHT), rect, clip); + Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], rect, clip); rect.UpperLeftCorner.X = r.LowerRightCorner.X - 1; rect.LowerRightCorner.X = r.LowerRightCorner.X; rect.UpperLeftCorner.Y = r.UpperLeftCorner.Y; rect.LowerRightCorner.Y = r.LowerRightCorner.Y; - Driver->draw2DRectangle(getColor(EGDC_3D_DARK_SHADOW), rect, clip); + Driver->draw2DRectangle(colors[EGDC_3D_DARK_SHADOW], rect, clip); rect.UpperLeftCorner.X -= 1; rect.LowerRightCorner.X -= 1; rect.UpperLeftCorner.Y += 1; rect.LowerRightCorner.Y -= 1; - Driver->draw2DRectangle(getColor(EGDC_3D_SHADOW), rect, clip); + Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], rect, clip); rect.UpperLeftCorner.X = r.UpperLeftCorner.X; rect.UpperLeftCorner.Y = r.LowerRightCorner.Y - 1; rect.LowerRightCorner.Y = r.LowerRightCorner.Y; rect.LowerRightCorner.X = r.LowerRightCorner.X; - Driver->draw2DRectangle(getColor(EGDC_3D_DARK_SHADOW), rect, clip); + Driver->draw2DRectangle(colors[EGDC_3D_DARK_SHADOW], rect, clip); rect.UpperLeftCorner.X += 1; rect.LowerRightCorner.X -= 1; rect.UpperLeftCorner.Y -= 1; rect.LowerRightCorner.Y -= 1; - Driver->draw2DRectangle(getColor(EGDC_3D_SHADOW), rect, clip); + Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], rect, clip); rect = r; - rect.UpperLeftCorner.X += 1; - rect.UpperLeftCorner.Y += 1; + rect.UpperLeftCorner.X +=1; + rect.UpperLeftCorner.Y +=1; rect.LowerRightCorner.X -= 2; rect.LowerRightCorner.Y -= 2; if (!UseGradient) - Driver->draw2DRectangle(getColor(EGDC_3D_FACE), rect, clip); - else { - const video::SColor c1 = getColor(EGDC_3D_FACE); - const video::SColor c2 = getColor(EGDC_3D_SHADOW); + Driver->draw2DRectangle(colors[EGDC_3D_FACE], rect, clip); + else + { + const video::SColor c1 = colors[EGDC_3D_FACE]; + const video::SColor c2 = colors[EGDC_3D_SHADOW]; Driver->draw2DRectangle(rect, c1, c1, c2, c2, clip); } } +// END PATCH + //! draws a standard 3d tool bar /** Used for drawing for toolbars and menus. @@ -661,38 +750,50 @@ is usually not used by ISkin, but can be used for example by more complex implementations to find out how to draw the part exactly. \param rect: Defining area where to draw. \param clip: Clip area. */ -void CGUISkin::draw3DToolBar(IGUIElement *element, - const core::rect &r, - const core::rect *clip) +// PATCH +void CGUISkin::drawColored3DToolBar(IGUIElement* element, + const core::rect& r, + const core::rect* clip, + const video::SColor* colors) { if (!Driver) return; + if (!colors) + colors = Colors; + core::rect rect = r; rect.UpperLeftCorner.X = r.UpperLeftCorner.X; rect.UpperLeftCorner.Y = r.LowerRightCorner.Y - 1; rect.LowerRightCorner.Y = r.LowerRightCorner.Y; rect.LowerRightCorner.X = r.LowerRightCorner.X; - Driver->draw2DRectangle(getColor(EGDC_3D_SHADOW), rect, clip); + Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], rect, clip); rect = r; rect.LowerRightCorner.Y -= 1; - if (!UseGradient) { - Driver->draw2DRectangle(getColor(EGDC_3D_FACE), rect, clip); - } else if (Type == EGST_BURNING_SKIN) { - const video::SColor c1 = 0xF0000000 | getColor(EGDC_3D_FACE).color; - const video::SColor c2 = 0xF0000000 | getColor(EGDC_3D_SHADOW).color; + if (!UseGradient) + { + Driver->draw2DRectangle(colors[EGDC_3D_FACE], rect, clip); + } + else + if ( Type == EGST_BURNING_SKIN ) + { + const video::SColor c1 = 0xF0000000 | colors[EGDC_3D_FACE].color; + const video::SColor c2 = 0xF0000000 | colors[EGDC_3D_SHADOW].color; rect.LowerRightCorner.Y += 1; Driver->draw2DRectangle(rect, c1, c2, c1, c2, clip); - } else { - const video::SColor c1 = getColor(EGDC_3D_FACE); - const video::SColor c2 = getColor(EGDC_3D_SHADOW); + } + else + { + const video::SColor c1 = colors[EGDC_3D_FACE]; + const video::SColor c2 = colors[EGDC_3D_SHADOW]; Driver->draw2DRectangle(rect, c1, c1, c2, c2, clip); } } +// END PATCH //! draws a tab button /** Used for drawing for tab buttons on top of tabs. @@ -702,53 +803,61 @@ implementations to find out how to draw the part exactly. \param active: Specifies if the tab is currently active. \param rect: Defining area where to draw. \param clip: Clip area. */ -void CGUISkin::draw3DTabButton(IGUIElement *element, bool active, - const core::rect &frameRect, const core::rect *clip, EGUI_ALIGNMENT alignment) +// PATCH +void CGUISkin::drawColored3DTabButton(IGUIElement* element, bool active, + const core::rect& frameRect, const core::rect* clip, EGUI_ALIGNMENT alignment, + const video::SColor* colors) { if (!Driver) return; + if (!colors) + colors = Colors; + core::rect tr = frameRect; - if (alignment == EGUIA_UPPERLEFT) { + if ( alignment == EGUIA_UPPERLEFT ) + { tr.LowerRightCorner.X -= 2; tr.LowerRightCorner.Y = tr.UpperLeftCorner.Y + 1; tr.UpperLeftCorner.X += 1; - Driver->draw2DRectangle(getColor(EGDC_3D_HIGH_LIGHT), tr, clip); + Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], tr, clip); // draw left highlight tr = frameRect; tr.LowerRightCorner.X = tr.UpperLeftCorner.X + 1; tr.UpperLeftCorner.Y += 1; - Driver->draw2DRectangle(getColor(EGDC_3D_HIGH_LIGHT), tr, clip); + Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], tr, clip); // draw grey background tr = frameRect; tr.UpperLeftCorner.X += 1; tr.UpperLeftCorner.Y += 1; tr.LowerRightCorner.X -= 2; - Driver->draw2DRectangle(getColor(EGDC_3D_FACE), tr, clip); + Driver->draw2DRectangle(colors[EGDC_3D_FACE], tr, clip); // draw right middle gray shadow tr.LowerRightCorner.X += 1; tr.UpperLeftCorner.X = tr.LowerRightCorner.X - 1; - Driver->draw2DRectangle(getColor(EGDC_3D_SHADOW), tr, clip); + Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], tr, clip); tr.LowerRightCorner.X += 1; tr.UpperLeftCorner.X += 1; tr.UpperLeftCorner.Y += 1; - Driver->draw2DRectangle(getColor(EGDC_3D_DARK_SHADOW), tr, clip); - } else { + Driver->draw2DRectangle(colors[EGDC_3D_DARK_SHADOW], tr, clip); + } + else + { tr.LowerRightCorner.X -= 2; tr.UpperLeftCorner.Y = tr.LowerRightCorner.Y - 1; tr.UpperLeftCorner.X += 1; - Driver->draw2DRectangle(getColor(EGDC_3D_HIGH_LIGHT), tr, clip); + Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], tr, clip); // draw left highlight tr = frameRect; tr.LowerRightCorner.X = tr.UpperLeftCorner.X + 1; tr.LowerRightCorner.Y -= 1; - Driver->draw2DRectangle(getColor(EGDC_3D_HIGH_LIGHT), tr, clip); + Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], tr, clip); // draw grey background tr = frameRect; @@ -756,20 +865,22 @@ void CGUISkin::draw3DTabButton(IGUIElement *element, bool active, tr.UpperLeftCorner.Y -= 1; tr.LowerRightCorner.X -= 2; tr.LowerRightCorner.Y -= 1; - Driver->draw2DRectangle(getColor(EGDC_3D_FACE), tr, clip); + Driver->draw2DRectangle(colors[EGDC_3D_FACE], tr, clip); // draw right middle gray shadow tr.LowerRightCorner.X += 1; tr.UpperLeftCorner.X = tr.LowerRightCorner.X - 1; - // tr.LowerRightCorner.Y -= 1; - Driver->draw2DRectangle(getColor(EGDC_3D_SHADOW), tr, clip); + //tr.LowerRightCorner.Y -= 1; + Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], tr, clip); tr.LowerRightCorner.X += 1; tr.UpperLeftCorner.X += 1; tr.LowerRightCorner.Y -= 1; - Driver->draw2DRectangle(getColor(EGDC_3D_DARK_SHADOW), tr, clip); + Driver->draw2DRectangle(colors[EGDC_3D_DARK_SHADOW], tr, clip); } } +// END PATCH + //! draws a tab control body /** \param element: Pointer to the element which wishes to draw this. This parameter @@ -779,77 +890,93 @@ implementations to find out how to draw the part exactly. \param background: Specifies if the background should be drawn. \param rect: Defining area where to draw. \param clip: Clip area. */ -void CGUISkin::draw3DTabBody(IGUIElement *element, bool border, bool background, - const core::rect &rect, const core::rect *clip, s32 tabHeight, EGUI_ALIGNMENT alignment) +// PATCH +void CGUISkin::drawColored3DTabBody(IGUIElement* element, bool border, bool background, + const core::rect& rect, const core::rect* clip, s32 tabHeight, EGUI_ALIGNMENT alignment, + const video::SColor* colors) { if (!Driver) return; + if (!colors) + colors = Colors; + core::rect tr = rect; - if (tabHeight == -1) + if ( tabHeight == -1 ) tabHeight = getSize(gui::EGDS_BUTTON_HEIGHT); // draw border. - if (border) { - if (alignment == EGUIA_UPPERLEFT) { + if (border) + { + if ( alignment == EGUIA_UPPERLEFT ) + { // draw left hightlight tr.UpperLeftCorner.Y += tabHeight + 2; tr.LowerRightCorner.X = tr.UpperLeftCorner.X + 1; - Driver->draw2DRectangle(getColor(EGDC_3D_HIGH_LIGHT), tr, clip); + Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], tr, clip); // draw right shadow tr.UpperLeftCorner.X = rect.LowerRightCorner.X - 1; tr.LowerRightCorner.X = tr.UpperLeftCorner.X + 1; - Driver->draw2DRectangle(getColor(EGDC_3D_SHADOW), tr, clip); + Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], tr, clip); // draw lower shadow tr = rect; tr.UpperLeftCorner.Y = tr.LowerRightCorner.Y - 1; - Driver->draw2DRectangle(getColor(EGDC_3D_SHADOW), tr, clip); - } else { + Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], tr, clip); + } + else + { // draw left hightlight tr.LowerRightCorner.Y -= tabHeight + 2; tr.LowerRightCorner.X = tr.UpperLeftCorner.X + 1; - Driver->draw2DRectangle(getColor(EGDC_3D_HIGH_LIGHT), tr, clip); + Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], tr, clip); // draw right shadow tr.UpperLeftCorner.X = rect.LowerRightCorner.X - 1; tr.LowerRightCorner.X = tr.UpperLeftCorner.X + 1; - Driver->draw2DRectangle(getColor(EGDC_3D_SHADOW), tr, clip); + Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], tr, clip); // draw lower shadow tr = rect; tr.LowerRightCorner.Y = tr.UpperLeftCorner.Y + 1; - Driver->draw2DRectangle(getColor(EGDC_3D_HIGH_LIGHT), tr, clip); + Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], tr, clip); } } - if (background) { - if (alignment == EGUIA_UPPERLEFT) { + if (background) + { + if ( alignment == EGUIA_UPPERLEFT ) + { tr = rect; tr.UpperLeftCorner.Y += tabHeight + 2; tr.LowerRightCorner.X -= 1; tr.UpperLeftCorner.X += 1; tr.LowerRightCorner.Y -= 1; - } else { + } + else + { tr = rect; tr.UpperLeftCorner.X += 1; tr.UpperLeftCorner.Y -= 1; tr.LowerRightCorner.X -= 1; tr.LowerRightCorner.Y -= tabHeight + 2; - // tr.UpperLeftCorner.X += 1; + //tr.UpperLeftCorner.X += 1; } if (!UseGradient) - Driver->draw2DRectangle(getColor(EGDC_3D_FACE), tr, clip); - else { - video::SColor c1 = getColor(EGDC_3D_FACE); - video::SColor c2 = getColor(EGDC_3D_SHADOW); + Driver->draw2DRectangle(colors[EGDC_3D_FACE], tr, clip); + else + { + video::SColor c1 = colors[EGDC_3D_FACE]; + video::SColor c2 = colors[EGDC_3D_SHADOW]; Driver->draw2DRectangle(tr, c1, c1, c2, c2, clip); } } } +// END PATCH + //! draws an icon, usually from the skin's sprite bank /** \param parent: Pointer to the element which wishes to draw this icon. @@ -861,31 +988,50 @@ by more complex implementations to find out how to draw the part exactly. \param currenttime: The present time, used to calculate the frame number \param loop: Whether the animation should loop or not \param clip: Clip area. */ -void CGUISkin::drawIcon(IGUIElement *element, EGUI_DEFAULT_ICON icon, - const core::position2di position, - u32 starttime, u32 currenttime, - bool loop, const core::rect *clip) +// PATCH +void CGUISkin::drawColoredIcon(IGUIElement* element, EGUI_DEFAULT_ICON icon, + const core::position2di position, + u32 starttime, u32 currenttime, + bool loop, const core::rect* clip, + const video::SColor* colors) { if (!SpriteBank) return; + if (!colors) + colors = Colors; + bool gray = element && !element->isEnabled(); SpriteBank->draw2DSprite(Icons[icon], position, clip, - Colors[gray ? EGDC_GRAY_WINDOW_SYMBOL : EGDC_WINDOW_SYMBOL], starttime, currenttime, loop, true); + colors[gray? EGDC_GRAY_WINDOW_SYMBOL : EGDC_WINDOW_SYMBOL], starttime, currenttime, loop, true); } +// END PATCH + EGUI_SKIN_TYPE CGUISkin::getType() const { return Type; } + //! draws a 2d rectangle. -void CGUISkin::draw2DRectangle(IGUIElement *element, - const video::SColor &color, const core::rect &pos, - const core::rect *clip) +void CGUISkin::draw2DRectangle(IGUIElement* element, + const video::SColor &color, const core::rect& pos, + const core::rect* clip) { Driver->draw2DRectangle(color, pos, clip); } + +//! gets the colors +// PATCH +void CGUISkin::getColors(video::SColor* colors) +{ + u32 i; + for (i=0; i +#include "ITexture.h" namespace irr { namespace video { -class IVideoDriver; + class IVideoDriver; } namespace gui { + class CGUISkin : public IGUISkin + { + public: -class CGUISkin : public IGUISkin -{ -public: - CGUISkin(EGUI_SKIN_TYPE type, video::IVideoDriver *driver); + CGUISkin(EGUI_SKIN_TYPE type, video::IVideoDriver* driver); - //! destructor - virtual ~CGUISkin(); + //! destructor + virtual ~CGUISkin(); - //! returns display density scaling factor - virtual float getScale() const override { return Scale; } + //! returns display density scaling factor + virtual float getScale() const { return Scale; } - //! sets display density scaling factor - virtual void setScale(float scale) override { Scale = scale; } + //! sets display density scaling factor + virtual void setScale(float scale) { Scale = scale; } - //! returns default color - video::SColor getColor(EGUI_DEFAULT_COLOR color) const override; + //! returns default color + virtual video::SColor getColor(EGUI_DEFAULT_COLOR color) const; - //! sets a default color - void setColor(EGUI_DEFAULT_COLOR which, video::SColor newColor) override; + //! sets a default color + virtual void setColor(EGUI_DEFAULT_COLOR which, video::SColor newColor); - //! returns size for the given size type - s32 getSize(EGUI_DEFAULT_SIZE size) const override; + //! returns size for the given size type + virtual s32 getSize(EGUI_DEFAULT_SIZE size) const; - //! sets a default size - void setSize(EGUI_DEFAULT_SIZE which, s32 size) override; + //! sets a default size + virtual void setSize(EGUI_DEFAULT_SIZE which, s32 size); - //! returns the default font - IGUIFont *getFont(EGUI_DEFAULT_FONT which = EGDF_DEFAULT) const override; + //! returns the default font + virtual IGUIFont* getFont(EGUI_DEFAULT_FONT which=EGDF_DEFAULT) const; - //! sets a default font - void setFont(IGUIFont *font, EGUI_DEFAULT_FONT which = EGDF_DEFAULT) override; + //! sets a default font + virtual void setFont(IGUIFont* font, EGUI_DEFAULT_FONT which=EGDF_DEFAULT); - //! sets the sprite bank used for drawing icons - void setSpriteBank(IGUISpriteBank *bank) override; + //! sets the sprite bank used for drawing icons + virtual void setSpriteBank(IGUISpriteBank* bank); - //! gets the sprite bank used for drawing icons - IGUISpriteBank *getSpriteBank() const override; + //! gets the sprite bank used for drawing icons + virtual IGUISpriteBank* getSpriteBank() const; - //! Returns a default icon - /** Returns the sprite index within the sprite bank */ - u32 getIcon(EGUI_DEFAULT_ICON icon) const override; + //! Returns a default icon + /** Returns the sprite index within the sprite bank */ + virtual u32 getIcon(EGUI_DEFAULT_ICON icon) const; - //! Sets a default icon - /** Sets the sprite index used for drawing icons like arrows, - close buttons and ticks in checkboxes - \param icon: Enum specifying which icon to change - \param index: The sprite index used to draw this icon */ - void setIcon(EGUI_DEFAULT_ICON icon, u32 index) override; + //! Sets a default icon + /** Sets the sprite index used for drawing icons like arrows, + close buttons and ticks in checkboxes + \param icon: Enum specifying which icon to change + \param index: The sprite index used to draw this icon */ + virtual void setIcon(EGUI_DEFAULT_ICON icon, u32 index); - //! Returns a default text. - /** For example for Message box button captions: - "OK", "Cancel", "Yes", "No" and so on. */ - const wchar_t *getDefaultText(EGUI_DEFAULT_TEXT text) const override; + //! Returns a default text. + /** For example for Message box button captions: + "OK", "Cancel", "Yes", "No" and so on. */ + virtual const wchar_t* getDefaultText(EGUI_DEFAULT_TEXT text) const; - //! Sets a default text. - /** For example for Message box button captions: - "OK", "Cancel", "Yes", "No" and so on. */ - void setDefaultText(EGUI_DEFAULT_TEXT which, const wchar_t *newText) override; + //! Sets a default text. + /** For example for Message box button captions: + "OK", "Cancel", "Yes", "No" and so on. */ + virtual void setDefaultText(EGUI_DEFAULT_TEXT which, const wchar_t* newText); - //! draws a standard 3d button pane - /** Used for drawing for example buttons in normal state. - It uses the colors EGDC_3D_DARK_SHADOW, EGDC_3D_HIGH_LIGHT, EGDC_3D_SHADOW and - EGDC_3D_FACE for this. See EGUI_DEFAULT_COLOR for details. - \param rect: Defining area where to draw. - \param clip: Clip area. - \param element: Pointer to the element which wishes to draw this. This parameter - is usually not used by ISkin, but can be used for example by more complex - implementations to find out how to draw the part exactly. */ - virtual void draw3DButtonPaneStandard(IGUIElement *element, - const core::rect &rect, - const core::rect *clip = 0) override; + //! draws a standard 3d button pane + /** Used for drawing for example buttons in normal state. + It uses the colors EGDC_3D_DARK_SHADOW, EGDC_3D_HIGH_LIGHT, EGDC_3D_SHADOW and + EGDC_3D_FACE for this. See EGUI_DEFAULT_COLOR for details. + \param rect: Defining area where to draw. + \param clip: Clip area. + \param element: Pointer to the element which wishes to draw this. This parameter + is usually not used by ISkin, but can be used for example by more complex + implementations to find out how to draw the part exactly. */ + virtual void draw3DButtonPaneStandard(IGUIElement* element, + const core::rect& rect, + const core::rect* clip=0) + { + drawColored3DButtonPaneStandard(element, rect,clip); + } - //! draws a pressed 3d button pane - /** Used for drawing for example buttons in pressed state. - It uses the colors EGDC_3D_DARK_SHADOW, EGDC_3D_HIGH_LIGHT, EGDC_3D_SHADOW and - EGDC_3D_FACE for this. See EGUI_DEFAULT_COLOR for details. - \param rect: Defining area where to draw. - \param clip: Clip area. - \param element: Pointer to the element which wishes to draw this. This parameter - is usually not used by ISkin, but can be used for example by more complex - implementations to find out how to draw the part exactly. */ - virtual void draw3DButtonPanePressed(IGUIElement *element, - const core::rect &rect, - const core::rect *clip = 0) override; + virtual void drawColored3DButtonPaneStandard(IGUIElement* element, + const core::rect& rect, + const core::rect* clip=0, + const video::SColor* colors=0); - //! draws a sunken 3d pane - /** Used for drawing the background of edit, combo or check boxes. - \param element: Pointer to the element which wishes to draw this. This parameter - is usually not used by ISkin, but can be used for example by more complex - implementations to find out how to draw the part exactly. - \param bgcolor: Background color. - \param flat: Specifies if the sunken pane should be flat or displayed as sunken - deep into the ground. - \param rect: Defining area where to draw. - \param clip: Clip area. */ - virtual void draw3DSunkenPane(IGUIElement *element, - video::SColor bgcolor, bool flat, - bool fillBackGround, - const core::rect &rect, - const core::rect *clip = 0) override; + //! draws a pressed 3d button pane + /** Used for drawing for example buttons in pressed state. + It uses the colors EGDC_3D_DARK_SHADOW, EGDC_3D_HIGH_LIGHT, EGDC_3D_SHADOW and + EGDC_3D_FACE for this. See EGUI_DEFAULT_COLOR for details. + \param rect: Defining area where to draw. + \param clip: Clip area. + \param element: Pointer to the element which wishes to draw this. This parameter + is usually not used by ISkin, but can be used for example by more complex + implementations to find out how to draw the part exactly. */ + virtual void draw3DButtonPanePressed(IGUIElement* element, + const core::rect& rect, + const core::rect* clip=0) + { + drawColored3DButtonPanePressed(element, rect, clip); + } - //! draws a window background - /** Used for drawing the background of dialogs and windows. - \param element: Pointer to the element which wishes to draw this. This parameter - is usually not used by ISkin, but can be used for example by more complex - implementations to find out how to draw the part exactly. - \param titleBarColor: Title color. - \param drawTitleBar: True to enable title drawing. - \param rect: Defining area where to draw. - \param clip: Clip area. - \param checkClientArea: When set to non-null the function will not draw anything, - but will instead return the clientArea which can be used for drawing by the calling window. - That is the area without borders and without titlebar. - \return Returns rect where it would be good to draw title bar text. This will - work even when checkClientArea is set to a non-null value.*/ - virtual core::rect draw3DWindowBackground(IGUIElement *element, - bool drawTitleBar, video::SColor titleBarColor, - const core::rect &rect, - const core::rect *clip, - core::rect *checkClientArea) override; + virtual void drawColored3DButtonPanePressed(IGUIElement* element, + const core::rect& rect, + const core::rect* clip=0, + const video::SColor* colors=0); - //! draws a standard 3d menu pane - /** Used for drawing for menus and context menus. - It uses the colors EGDC_3D_DARK_SHADOW, EGDC_3D_HIGH_LIGHT, EGDC_3D_SHADOW and - EGDC_3D_FACE for this. See EGUI_DEFAULT_COLOR for details. - \param element: Pointer to the element which wishes to draw this. This parameter - is usually not used by ISkin, but can be used for example by more complex - implementations to find out how to draw the part exactly. - \param rect: Defining area where to draw. - \param clip: Clip area. */ - virtual void draw3DMenuPane(IGUIElement *element, - const core::rect &rect, - const core::rect *clip = 0) override; + //! draws a sunken 3d pane + /** Used for drawing the background of edit, combo or check boxes. + \param element: Pointer to the element which wishes to draw this. This parameter + is usually not used by ISkin, but can be used for example by more complex + implementations to find out how to draw the part exactly. + \param bgcolor: Background color. + \param flat: Specifies if the sunken pane should be flat or displayed as sunken + deep into the ground. + \param rect: Defining area where to draw. + \param clip: Clip area. */ + virtual void draw3DSunkenPane(IGUIElement* element, + video::SColor bgcolor, bool flat, + bool fillBackGround, + const core::rect& rect, + const core::rect* clip=0) + { + drawColored3DSunkenPane(element, bgcolor, flat, fillBackGround, rect, clip); + } - //! draws a standard 3d tool bar - /** Used for drawing for toolbars and menus. - \param element: Pointer to the element which wishes to draw this. This parameter - is usually not used by ISkin, but can be used for example by more complex - implementations to find out how to draw the part exactly. - \param rect: Defining area where to draw. - \param clip: Clip area. */ - virtual void draw3DToolBar(IGUIElement *element, - const core::rect &rect, - const core::rect *clip = 0) override; + virtual void drawColored3DSunkenPane(IGUIElement* element, + video::SColor bgcolor, bool flat, + bool fillBackGround, + const core::rect& rect, + const core::rect* clip=0, + const video::SColor* colors=0); - //! draws a tab button - /** Used for drawing for tab buttons on top of tabs. - \param element: Pointer to the element which wishes to draw this. This parameter - is usually not used by ISkin, but can be used for example by more complex - implementations to find out how to draw the part exactly. - \param active: Specifies if the tab is currently active. - \param rect: Defining area where to draw. - \param clip: Clip area. */ - virtual void draw3DTabButton(IGUIElement *element, bool active, - const core::rect &rect, const core::rect *clip = 0, - EGUI_ALIGNMENT alignment = EGUIA_UPPERLEFT) override; + //! draws a window background + /** Used for drawing the background of dialogs and windows. + \param element: Pointer to the element which wishes to draw this. This parameter + is usually not used by ISkin, but can be used for example by more complex + implementations to find out how to draw the part exactly. + \param titleBarColor: Title color. + \param drawTitleBar: True to enable title drawing. + \param rect: Defining area where to draw. + \param clip: Clip area. + \param checkClientArea: When set to non-null the function will not draw anything, + but will instead return the clientArea which can be used for drawing by the calling window. + That is the area without borders and without titlebar. + \return Returns rect where it would be good to draw title bar text. This will + work even when checkClientArea is set to a non-null value.*/ + virtual core::rect draw3DWindowBackground(IGUIElement* element, + bool drawTitleBar, video::SColor titleBarColor, + const core::rect& rect, + const core::rect* clip, + core::rect* checkClientArea) + { + return drawColored3DWindowBackground(element, drawTitleBar, titleBarColor, + rect, clip, checkClientArea); + } - //! draws a tab control body - /** \param element: Pointer to the element which wishes to draw this. This parameter - is usually not used by ISkin, but can be used for example by more complex - implementations to find out how to draw the part exactly. - \param border: Specifies if the border should be drawn. - \param background: Specifies if the background should be drawn. - \param rect: Defining area where to draw. - \param clip: Clip area. */ - virtual void draw3DTabBody(IGUIElement *element, bool border, bool background, - const core::rect &rect, const core::rect *clip = 0, s32 tabHeight = -1, - EGUI_ALIGNMENT alignment = EGUIA_UPPERLEFT) override; + virtual core::rect drawColored3DWindowBackground(IGUIElement* element, + bool drawTitleBar, video::SColor titleBarColor, + const core::rect& rect, + const core::rect* clip, + core::rect* checkClientArea, + const video::SColor* colors=0); - //! draws an icon, usually from the skin's sprite bank - /** \param element: Pointer to the element which wishes to draw this icon. - This parameter is usually not used by IGUISkin, but can be used for example - by more complex implementations to find out how to draw the part exactly. - \param icon: Specifies the icon to be drawn. - \param position: The position to draw the icon - \param starttime: The time at the start of the animation - \param currenttime: The present time, used to calculate the frame number - \param loop: Whether the animation should loop or not - \param clip: Clip area. */ - virtual void drawIcon(IGUIElement *element, EGUI_DEFAULT_ICON icon, - const core::position2di position, - u32 starttime = 0, u32 currenttime = 0, - bool loop = false, const core::rect *clip = 0) override; + //! draws a standard 3d menu pane + /** Used for drawing for menus and context menus. + It uses the colors EGDC_3D_DARK_SHADOW, EGDC_3D_HIGH_LIGHT, EGDC_3D_SHADOW and + EGDC_3D_FACE for this. See EGUI_DEFAULT_COLOR for details. + \param element: Pointer to the element which wishes to draw this. This parameter + is usually not used by ISkin, but can be used for example by more complex + implementations to find out how to draw the part exactly. + \param rect: Defining area where to draw. + \param clip: Clip area. */ + virtual void draw3DMenuPane(IGUIElement* element, + const core::rect& rect, + const core::rect* clip=0) + { + drawColored3DMenuPane(element, rect, clip); + } - //! draws a 2d rectangle. - /** \param element: Pointer to the element which wishes to draw this icon. - This parameter is usually not used by IGUISkin, but can be used for example - by more complex implementations to find out how to draw the part exactly. - \param color: Color of the rectangle to draw. The alpha component specifies how - transparent the rectangle will be. - \param pos: Position of the rectangle. - \param clip: Pointer to rectangle against which the rectangle will be clipped. - If the pointer is null, no clipping will be performed. */ - virtual void draw2DRectangle(IGUIElement *element, const video::SColor &color, - const core::rect &pos, const core::rect *clip = 0) override; + virtual void drawColored3DMenuPane(IGUIElement* element, + const core::rect& rect, + const core::rect* clip=0, + const video::SColor* colors=0); - //! get the type of this skin - EGUI_SKIN_TYPE getType() const override; + //! draws a standard 3d tool bar + /** Used for drawing for toolbars and menus. + \param element: Pointer to the element which wishes to draw this. This parameter + is usually not used by ISkin, but can be used for example by more complex + implementations to find out how to draw the part exactly. + \param rect: Defining area where to draw. + \param clip: Clip area. */ + virtual void draw3DToolBar(IGUIElement* element, + const core::rect& rect, + const core::rect* clip=0) + { + drawColored3DToolBar(element, rect, clip); + } -private: - float Scale = 1.0f; - video::SColor Colors[EGDC_COUNT]; - s32 Sizes[EGDS_COUNT]; - u32 Icons[EGDI_COUNT]; - IGUIFont *Fonts[EGDF_COUNT]; - IGUISpriteBank *SpriteBank; - core::stringw Texts[EGDT_COUNT]; - video::IVideoDriver *Driver; - bool UseGradient; + virtual void drawColored3DToolBar(IGUIElement* element, + const core::rect& rect, + const core::rect* clip=0, + const video::SColor* colors=0); - EGUI_SKIN_TYPE Type; -}; + //! draws a tab button + /** Used for drawing for tab buttons on top of tabs. + \param element: Pointer to the element which wishes to draw this. This parameter + is usually not used by ISkin, but can be used for example by more complex + implementations to find out how to draw the part exactly. + \param active: Specifies if the tab is currently active. + \param rect: Defining area where to draw. + \param clip: Clip area. */ + virtual void draw3DTabButton(IGUIElement* element, bool active, + const core::rect& rect, const core::rect* clip=0, EGUI_ALIGNMENT alignment=EGUIA_UPPERLEFT) + { + drawColored3DTabButton(element, active, rect, clip, alignment); + } + + virtual void drawColored3DTabButton(IGUIElement* element, bool active, + const core::rect& rect, const core::rect* clip=0, EGUI_ALIGNMENT alignment=EGUIA_UPPERLEFT, + const video::SColor* colors=0); + + //! draws a tab control body + /** \param element: Pointer to the element which wishes to draw this. This parameter + is usually not used by ISkin, but can be used for example by more complex + implementations to find out how to draw the part exactly. + \param border: Specifies if the border should be drawn. + \param background: Specifies if the background should be drawn. + \param rect: Defining area where to draw. + \param clip: Clip area. */ + virtual void draw3DTabBody(IGUIElement* element, bool border, bool background, + const core::rect& rect, const core::rect* clip=0, s32 tabHeight=-1, EGUI_ALIGNMENT alignment=EGUIA_UPPERLEFT) + { + drawColored3DTabBody(element, border, background, rect, clip, tabHeight, alignment); + } + + virtual void drawColored3DTabBody(IGUIElement* element, bool border, bool background, + const core::rect& rect, const core::rect* clip=0, s32 tabHeight=-1, EGUI_ALIGNMENT alignment=EGUIA_UPPERLEFT, + const video::SColor* colors=0); + + //! draws an icon, usually from the skin's sprite bank + /** \param element: Pointer to the element which wishes to draw this icon. + This parameter is usually not used by IGUISkin, but can be used for example + by more complex implementations to find out how to draw the part exactly. + \param icon: Specifies the icon to be drawn. + \param position: The position to draw the icon + \param starttime: The time at the start of the animation + \param currenttime: The present time, used to calculate the frame number + \param loop: Whether the animation should loop or not + \param clip: Clip area. */ + virtual void drawIcon(IGUIElement* element, EGUI_DEFAULT_ICON icon, + const core::position2di position, + u32 starttime=0, u32 currenttime=0, + bool loop=false, const core::rect* clip=0) + { + drawColoredIcon(element, icon, position, starttime, currenttime, loop, clip); + } + + virtual void drawColoredIcon(IGUIElement* element, EGUI_DEFAULT_ICON icon, + const core::position2di position, + u32 starttime=0, u32 currenttime=0, + bool loop=false, const core::rect* clip=0, + const video::SColor* colors=0); + + //! draws a 2d rectangle. + /** \param element: Pointer to the element which wishes to draw this icon. + This parameter is usually not used by IGUISkin, but can be used for example + by more complex implementations to find out how to draw the part exactly. + \param color: Color of the rectangle to draw. The alpha component specifies how + transparent the rectangle will be. + \param pos: Position of the rectangle. + \param clip: Pointer to rectangle against which the rectangle will be clipped. + If the pointer is null, no clipping will be performed. */ + virtual void draw2DRectangle(IGUIElement* element, const video::SColor &color, + const core::rect& pos, const core::rect* clip = 0); + + + //! get the type of this skin + virtual EGUI_SKIN_TYPE getType() const; + + //! gets the colors + virtual void getColors(video::SColor* colors); // ::PATCH: + + private: + + float Scale = 1.0f; + video::SColor Colors[EGDC_COUNT]; + s32 Sizes[EGDS_COUNT]; + u32 Icons[EGDI_COUNT]; + IGUIFont* Fonts[EGDF_COUNT]; + IGUISpriteBank* SpriteBank; + core::stringw Texts[EGDT_COUNT]; + video::IVideoDriver* Driver; + bool UseGradient; + + EGUI_SKIN_TYPE Type; + }; } // end namespace gui } // end namespace irr + diff --git a/irr/src/CImage.h b/irr/src/CImage.h index 955f85705..33a34386e 100644 --- a/irr/src/CImage.h +++ b/irr/src/CImage.h @@ -21,7 +21,7 @@ inline bool checkImageDimensions(u32 width, u32 height) //! IImage implementation with a lot of special image operations for //! 16 bit A1R5G5B5/32 Bit A8R8G8B8 images, which are used by the SoftwareDevice. -class CImage : public IImage +class CImage final : public IImage { public: //! constructor from raw image data diff --git a/irr/src/CIrrDeviceLinux.cpp b/irr/src/CIrrDeviceLinux.cpp index 5491d2037..6ecb499b2 100644 --- a/irr/src/CIrrDeviceLinux.cpp +++ b/irr/src/CIrrDeviceLinux.cpp @@ -26,6 +26,7 @@ #include "IGUISpriteBank.h" #include "IImageLoader.h" #include "IFileSystem.h" +#include "IVideoDriver.h" #include #include @@ -33,7 +34,7 @@ #include #endif -#if defined(_IRR_COMPILE_WITH_OGLES1_) || defined(_IRR_COMPILE_WITH_OGLES2_) +#if defined(_IRR_COMPILE_WITH_OGLES2_) #include "CEGLManager.h" #endif @@ -76,10 +77,6 @@ namespace video IVideoDriver *createOpenGLDriver(const irr::SIrrlichtCreationParameters ¶ms, io::IFileSystem *io, IContextManager *contextManager); #endif -#ifdef _IRR_COMPILE_WITH_OGLES1_ -IVideoDriver *createOGLES1Driver(const irr::SIrrlichtCreationParameters ¶ms, io::IFileSystem *io, IContextManager *contextManager); -#endif - #ifdef _IRR_COMPILE_WITH_OGLES2_ IVideoDriver *createOGLES2Driver(const irr::SIrrlichtCreationParameters ¶ms, io::IFileSystem *io, IContextManager *contextManager); #endif @@ -554,22 +551,6 @@ void CIrrDeviceLinux::createDriver() } #else os::Printer::log("No OpenGL support compiled in.", ELL_ERROR); -#endif - break; - case video::EDT_OGLES1: -#ifdef _IRR_COMPILE_WITH_OGLES1_ - { - video::SExposedVideoData data; - data.OpenGLLinux.X11Window = XWindow; - data.OpenGLLinux.X11Display = XDisplay; - - ContextManager = new video::CEGLManager(); - ContextManager->initialize(CreationParams, data); - - VideoDriver = video::createOGLES1Driver(CreationParams, FileSystem, ContextManager); - } -#else - os::Printer::log("No OpenGL-ES1 support compiled in.", ELL_ERROR); #endif break; case video::EDT_OGLES2: diff --git a/irr/src/CIrrDeviceOSX.mm b/irr/src/CIrrDeviceOSX.mm index 67c0ce05c..e335085e4 100644 --- a/irr/src/CIrrDeviceOSX.mm +++ b/irr/src/CIrrDeviceOSX.mm @@ -720,9 +720,8 @@ void CIrrDeviceMacOSX::createDriver() #endif break; - case video::EDT_OGLES1: case video::EDT_OGLES2: - os::Printer::log("This driver is not available in OSX. Try OpenGL or Software renderer.", ELL_ERROR); + os::Printer::log("This driver is not available on OSX.", ELL_ERROR); break; case video::EDT_NULL: diff --git a/irr/src/CIrrDeviceSDL.cpp b/irr/src/CIrrDeviceSDL.cpp index ffbd81950..6d1b45886 100644 --- a/irr/src/CIrrDeviceSDL.cpp +++ b/irr/src/CIrrDeviceSDL.cpp @@ -10,6 +10,7 @@ #include "IGUIEnvironment.h" #include "IImageLoader.h" #include "IFileSystem.h" +#include "IVideoDriver.h" #include "os.h" #include "CTimer.h" #include "irrString.h" @@ -594,18 +595,14 @@ bool CIrrDeviceSDL::createWindowWithContext() SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY); break; - case video::EDT_OGLES1: - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 1); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); - break; case video::EDT_OGLES2: case video::EDT_WEBGL1: SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); break; - default:; + default: + _IRR_DEBUG_BREAK_IF(1); } if (CreationParams.DriverDebug) { @@ -724,12 +721,19 @@ bool CIrrDeviceSDL::run() irrevent.EventType = irr::EET_MOUSE_INPUT_EVENT; irrevent.MouseInput.Event = irr::EMIE_MOUSE_MOVED; - MouseX = irrevent.MouseInput.X = - static_cast(SDL_event.motion.x * ScaleX); - MouseY = irrevent.MouseInput.Y = - static_cast(SDL_event.motion.y * ScaleY); + MouseXRel = static_cast(SDL_event.motion.xrel * ScaleX); MouseYRel = static_cast(SDL_event.motion.yrel * ScaleY); + if (!SDL_GetRelativeMouseMode()) { + MouseX = static_cast(SDL_event.motion.x * ScaleX); + MouseY = static_cast(SDL_event.motion.y * ScaleY); + } else { + MouseX += MouseXRel; + MouseY += MouseYRel; + } + irrevent.MouseInput.X = MouseX; + irrevent.MouseInput.Y = MouseY; + irrevent.MouseInput.ButtonStates = MouseButtonStates; irrevent.MouseInput.Shift = (keymod & KMOD_SHIFT) != 0; irrevent.MouseInput.Control = (keymod & KMOD_CTRL) != 0; diff --git a/irr/src/CIrrDeviceSDL.h b/irr/src/CIrrDeviceSDL.h index f881bba5c..7156c19b6 100644 --- a/irr/src/CIrrDeviceSDL.h +++ b/irr/src/CIrrDeviceSDL.h @@ -158,9 +158,13 @@ public: //! Sets the new position of the cursor. void setPosition(s32 x, s32 y) override { +#ifndef __ANDROID__ + // On Android, this somehow results in a camera jump when enabling + // relative mouse mode and it isn't supported anyway. SDL_WarpMouseInWindow(Device->Window, static_cast(x / Device->ScaleX), static_cast(y / Device->ScaleY)); +#endif if (SDL_GetRelativeMouseMode()) { // There won't be an event for this warp (details on libsdl-org/SDL/issues/6034) @@ -298,6 +302,7 @@ private: #endif s32 MouseX, MouseY; + // these two only continue to exist for some Emscripten stuff idk about s32 MouseXRel, MouseYRel; u32 MouseButtonStates; diff --git a/irr/src/CIrrDeviceStub.cpp b/irr/src/CIrrDeviceStub.cpp index 1ff120d10..fd8e458c8 100644 --- a/irr/src/CIrrDeviceStub.cpp +++ b/irr/src/CIrrDeviceStub.cpp @@ -8,6 +8,7 @@ #include "IFileSystem.h" #include "IGUIElement.h" #include "IGUIEnvironment.h" +#include "IVideoDriver.h" #include "os.h" #include "CTimer.h" #include "CLogger.h" diff --git a/irr/src/CIrrDeviceWin32.cpp b/irr/src/CIrrDeviceWin32.cpp index c2876fcce..366be8013 100644 --- a/irr/src/CIrrDeviceWin32.cpp +++ b/irr/src/CIrrDeviceWin32.cpp @@ -17,6 +17,7 @@ #include "COSOperator.h" #include "dimension2d.h" #include "IGUISpriteBank.h" +#include "IVideoDriver.h" #include #include "SExposedVideoData.h" @@ -29,7 +30,7 @@ #endif #endif -#if defined(_IRR_COMPILE_WITH_OGLES1_) || defined(_IRR_COMPILE_WITH_OGLES2_) +#if defined(_IRR_COMPILE_WITH_OGLES2_) #include "CEGLManager.h" #endif @@ -45,10 +46,6 @@ namespace video IVideoDriver *createOpenGLDriver(const irr::SIrrlichtCreationParameters ¶ms, io::IFileSystem *io, IContextManager *contextManager); #endif -#ifdef _IRR_COMPILE_WITH_OGLES1_ -IVideoDriver *createOGLES1Driver(const irr::SIrrlichtCreationParameters ¶ms, io::IFileSystem *io, IContextManager *contextManager); -#endif - #ifdef _IRR_COMPILE_WITH_OGLES2_ IVideoDriver *createOGLES2Driver(const irr::SIrrlichtCreationParameters ¶ms, io::IFileSystem *io, IContextManager *contextManager); #endif @@ -890,21 +887,6 @@ void CIrrDeviceWin32::createDriver() os::Printer::log("Could not create OpenGL driver.", ELL_ERROR); #else os::Printer::log("OpenGL driver was not compiled in.", ELL_ERROR); -#endif - break; - case video::EDT_OGLES1: -#ifdef _IRR_COMPILE_WITH_OGLES1_ - switchToFullScreen(); - - ContextManager = new video::CEGLManager(); - ContextManager->initialize(CreationParams, video::SExposedVideoData(HWnd)); - - VideoDriver = video::createOGLES1Driver(CreationParams, FileSystem, ContextManager); - - if (!VideoDriver) - os::Printer::log("Could not create OpenGL-ES1 driver.", ELL_ERROR); -#else - os::Printer::log("OpenGL-ES1 driver was not compiled in.", ELL_ERROR); #endif break; case video::EDT_OGLES2: diff --git a/irr/src/CLimitReadFile.h b/irr/src/CLimitReadFile.h index 1594135e6..6b02cfdc0 100644 --- a/irr/src/CLimitReadFile.h +++ b/irr/src/CLimitReadFile.h @@ -20,7 +20,7 @@ namespace io This can be useful, for example for reading uncompressed files in an archive (zip, tar). !*/ -class CLimitReadFile : public IReadFile +class CLimitReadFile final : public IReadFile { public: CLimitReadFile(IReadFile *alreadyOpenedFile, long pos, long areaSize, const io::path &name); diff --git a/irr/src/CMakeLists.txt b/irr/src/CMakeLists.txt index d5e9d47e7..6e38220be 100644 --- a/irr/src/CMakeLists.txt +++ b/irr/src/CMakeLists.txt @@ -17,7 +17,7 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$") set(CMAKE_CXX_FLAGS_RELEASE "-O3") set(CMAKE_CXX_FLAGS_DEBUG "-g") - add_compile_options(-Wall -pipe -fno-exceptions) + add_compile_options(-Wall -pipe) # Enable SSE for floating point math on 32-bit x86 by default # reasoning see minetest issue #11810 and https://gcc.gnu.org/wiki/FloatingPointMath @@ -83,6 +83,10 @@ if(LINUX_PLATFORM) add_definitions(-D_IRR_LINUX_PLATFORM_) endif() +if(MSVC) + add_definitions(-D_CRT_SECURE_NO_WARNINGS) +endif() + if(USE_SDL2) set(DEVICE "SDL") elseif(DEVICE STREQUAL "SDL") @@ -130,12 +134,6 @@ else() option(ENABLE_OPENGL "Enable OpenGL" TRUE) endif() -if(USE_SDL2 OR EMSCRIPTEN OR APPLE) - set(ENABLE_GLES1 FALSE) -else() - option(ENABLE_GLES1 "Enable OpenGL ES" FALSE) -endif() - if(APPLE) set(ENABLE_GLES2 FALSE) set(ENABLE_WEBGL1 FALSE) @@ -172,14 +170,6 @@ if(ENABLE_OPENGL3) endif() endif() -if(ENABLE_GLES1) - add_definitions(-D_IRR_COMPILE_WITH_OGLES1_) - set(OPENGLES_DIRECT_LINK TRUE) - if(DEVICE MATCHES "^(WINDOWS|X11)$") - add_definitions(-D_IRR_COMPILE_WITH_EGL_MANAGER_) - endif() -endif() - if(ENABLE_GLES2) add_definitions(-D_IRR_COMPILE_WITH_OGLES2_) if(DEVICE MATCHES "^(WINDOWS|X11)$" OR EMSCRIPTEN) @@ -204,7 +194,6 @@ endif() message(STATUS "Device: ${DEVICE}") message(STATUS "OpenGL: ${ENABLE_OPENGL}") message(STATUS "OpenGL 3: ${ENABLE_OPENGL3}") -message(STATUS "OpenGL ES: ${ENABLE_GLES1}") if (ENABLE_GLES2) message(STATUS "OpenGL ES 2: ON (unified)") else() @@ -220,13 +209,6 @@ find_package(ZLIB REQUIRED) find_package(JPEG REQUIRED) find_package(PNG REQUIRED) -if(ENABLE_GLES1) - # only tested on Android, probably works on Linux (is this needed anywhere else?) - find_library(OPENGLES_LIBRARY NAMES GLESv1_CM REQUIRED) - find_library(EGL_LIBRARY NAMES EGL REQUIRED) - - message(STATUS "Found OpenGLES: ${OPENGLES_LIBRARY}") -endif() if(ENABLE_GLES2) find_package(OpenGLES2 REQUIRED) endif() @@ -316,6 +298,7 @@ set(link_includes set(IRRMESHLOADER CB3DMeshFileLoader.cpp + CGLTFMeshFileLoader.cpp COBJMeshFileLoader.cpp CXMeshFileLoader.cpp ) @@ -328,6 +311,8 @@ add_library(IRRMESHOBJ OBJECT ${IRRMESHLOADER} ) +target_link_libraries(IRRMESHOBJ PUBLIC tiniergltf::tiniergltf) + add_library(IRROBJ OBJECT CBillboardSceneNode.cpp CCameraSceneNode.cpp @@ -339,6 +324,10 @@ add_library(IRROBJ OBJECT CMeshCache.cpp ) +# Make sure IRROBJ gets the transitive include directories for +# tiniergltf from IRRMESHOBJ. +target_link_libraries(IRROBJ PRIVATE IRRMESHOBJ) + set(IRRDRVROBJ CNullDriver.cpp CGLXManager.cpp @@ -360,14 +349,6 @@ if(ENABLE_OPENGL) ) endif() -if(ENABLE_GLES1) - set(IRRDRVROBJ - ${IRRDRVROBJ} - COGLESDriver.cpp - COGLESExtensionHandler.cpp - ) -endif() - # the unified drivers if(ENABLE_OPENGL3 OR ENABLE_GLES2) @@ -487,6 +468,10 @@ foreach(object_lib target_include_directories(${object_lib} PRIVATE ${link_includes}) # Add objects from object library to main library target_sources(IrrlichtMt PRIVATE $) + + if(BUILD_WITH_TRACY) + target_link_libraries(${object_lib} PRIVATE Tracy::TracyClient) + endif() endforeach() # Alias target provides add_submodule compatibility @@ -495,21 +480,21 @@ add_library(IrrlichtMt::IrrlichtMt ALIAS IrrlichtMt) target_include_directories(IrrlichtMt PUBLIC "$" - "$" PRIVATE + "$" ${link_includes} ) # this needs to be here and not in a variable (like link_includes) due to issues # with the generator expressions on at least CMake 3.22, but not 3.28 or later target_link_libraries(IrrlichtMt PRIVATE + tiniergltf::tiniergltf ${ZLIB_LIBRARY} ${JPEG_LIBRARY} ${PNG_LIBRARY} "$<$:SDL2::SDL2>" "$<$:${OPENGL_LIBRARIES}>" - "$<$:${OPENGLES_LIBRARY}>" ${EGL_LIBRARY} # incl. transitive SDL2 dependencies for static linking @@ -525,6 +510,9 @@ target_link_libraries(IrrlichtMt PRIVATE if(WIN32) target_compile_definitions(IrrlichtMt INTERFACE _IRR_WINDOWS_API_) # used in _IRR_DEBUG_BREAK_IF definition in a public header endif() +if(CMAKE_BUILD_TYPE STREQUAL "Debug") + target_compile_definitions(IrrlichtMt INTERFACE _DEBUG) # same +endif() if(APPLE OR ANDROID OR EMSCRIPTEN) target_compile_definitions(IrrlichtMt PUBLIC IRR_MOBILE_PATHS) endif() diff --git a/irr/src/CMemoryFile.h b/irr/src/CMemoryFile.h index 3ce0e8460..83d77cb77 100644 --- a/irr/src/CMemoryFile.h +++ b/irr/src/CMemoryFile.h @@ -17,7 +17,7 @@ namespace io /*! Class for reading from memory. */ -class CMemoryReadFile : public IMemoryReadFile +class CMemoryReadFile final : public IMemoryReadFile { public: //! Constructor diff --git a/irr/src/CMeshManipulator.cpp b/irr/src/CMeshManipulator.cpp index 3309fea3f..67b22a07e 100644 --- a/irr/src/CMeshManipulator.cpp +++ b/irr/src/CMeshManipulator.cpp @@ -67,7 +67,7 @@ void recalculateNormalsT(IMeshBuffer *buffer, bool smooth, bool angleWeighted) core::vector3df weight(1.f, 1.f, 1.f); if (angleWeighted) - weight = irr::scene::getAngleWeight(v1, v2, v3); // writing irr::scene:: necessary for borland + weight = getAngleWeight(v1, v2, v3); buffer->getNormal(idx[i + 0]) += weight.X * normal; buffer->getNormal(idx[i + 1]) += weight.Y * normal; @@ -115,6 +115,21 @@ void CMeshManipulator::recalculateNormals(scene::IMesh *mesh, bool smooth, bool } } +template +void copyVertices(const scene::IVertexBuffer *src, scene::CVertexBuffer *dst) +{ + _IRR_DEBUG_BREAK_IF(T::getType() != src->getType()); + auto *data = static_cast(src->getData()); + dst->Data.assign(data, data + src->getCount()); +} + +static void copyIndices(const scene::IIndexBuffer *src, scene::SIndexBuffer *dst) +{ + _IRR_DEBUG_BREAK_IF(src->getType() != video::EIT_16BIT); + auto *data = static_cast(src->getData()); + dst->Data.assign(data, data + src->getCount()); +} + //! Clones a static IMesh into a modifyable SMesh. // not yet 32bit SMesh *CMeshManipulator::createMeshCopy(scene::IMesh *mesh) const @@ -132,48 +147,24 @@ SMesh *CMeshManipulator::createMeshCopy(scene::IMesh *mesh) const case video::EVT_STANDARD: { SMeshBuffer *buffer = new SMeshBuffer(); buffer->Material = mb->getMaterial(); - const u32 vcount = mb->getVertexCount(); - buffer->Vertices.reallocate(vcount); - video::S3DVertex *vertices = (video::S3DVertex *)mb->getVertices(); - for (u32 i = 0; i < vcount; ++i) - buffer->Vertices.push_back(vertices[i]); - const u32 icount = mb->getIndexCount(); - buffer->Indices.reallocate(icount); - const u16 *indices = mb->getIndices(); - for (u32 i = 0; i < icount; ++i) - buffer->Indices.push_back(indices[i]); + copyVertices(mb->getVertexBuffer(), buffer->Vertices); + copyIndices(mb->getIndexBuffer(), buffer->Indices); clone->addMeshBuffer(buffer); buffer->drop(); } break; case video::EVT_2TCOORDS: { SMeshBufferLightMap *buffer = new SMeshBufferLightMap(); buffer->Material = mb->getMaterial(); - const u32 vcount = mb->getVertexCount(); - buffer->Vertices.reallocate(vcount); - video::S3DVertex2TCoords *vertices = (video::S3DVertex2TCoords *)mb->getVertices(); - for (u32 i = 0; i < vcount; ++i) - buffer->Vertices.push_back(vertices[i]); - const u32 icount = mb->getIndexCount(); - buffer->Indices.reallocate(icount); - const u16 *indices = mb->getIndices(); - for (u32 i = 0; i < icount; ++i) - buffer->Indices.push_back(indices[i]); + copyVertices(mb->getVertexBuffer(), buffer->Vertices); + copyIndices(mb->getIndexBuffer(), buffer->Indices); clone->addMeshBuffer(buffer); buffer->drop(); } break; case video::EVT_TANGENTS: { SMeshBufferTangents *buffer = new SMeshBufferTangents(); buffer->Material = mb->getMaterial(); - const u32 vcount = mb->getVertexCount(); - buffer->Vertices.reallocate(vcount); - video::S3DVertexTangents *vertices = (video::S3DVertexTangents *)mb->getVertices(); - for (u32 i = 0; i < vcount; ++i) - buffer->Vertices.push_back(vertices[i]); - const u32 icount = mb->getIndexCount(); - buffer->Indices.reallocate(icount); - const u16 *indices = mb->getIndices(); - for (u32 i = 0; i < icount; ++i) - buffer->Indices.push_back(indices[i]); + copyVertices(mb->getVertexBuffer(), buffer->Vertices); + copyIndices(mb->getIndexBuffer(), buffer->Indices); clone->addMeshBuffer(buffer); buffer->drop(); } break; @@ -202,7 +193,7 @@ s32 CMeshManipulator::getPolyCount(scene::IMesh *mesh) const //! Returns amount of polygons in mesh. s32 CMeshManipulator::getPolyCount(scene::IAnimatedMesh *mesh) const { - if (mesh && mesh->getFrameCount() != 0) + if (mesh && mesh->getMaxFrameNumber() != 0) return getPolyCount(mesh->getMesh(0)); return 0; diff --git a/irr/src/CMeshSceneNode.cpp b/irr/src/CMeshSceneNode.cpp index 7ff6cad55..030e1fd15 100644 --- a/irr/src/CMeshSceneNode.cpp +++ b/irr/src/CMeshSceneNode.cpp @@ -115,7 +115,6 @@ void CMeshSceneNode::render() // for debug purposes only: if (DebugDataVisible && PassCount == 1) { video::SMaterial m; - m.Lighting = false; m.AntiAliasing = 0; driver->setMaterial(m); diff --git a/irr/src/CNullDriver.cpp b/irr/src/CNullDriver.cpp index 92450e5d7..c7b296b57 100644 --- a/irr/src/CNullDriver.cpp +++ b/irr/src/CNullDriver.cpp @@ -53,7 +53,7 @@ public: //! constructor CNullDriver::CNullDriver(io::IFileSystem *io, const core::dimension2d &screenSize) : SharedRenderTarget(0), CurrentRenderTarget(0), CurrentRenderTargetSize(0, 0), FileSystem(io), MeshManipulator(0), - ViewPort(0, 0, 0, 0), ScreenSize(screenSize), PrimitivesDrawn(0), MinVertexCountForVBO(500), + ViewPort(0, 0, 0, 0), ScreenSize(screenSize), MinVertexCountForVBO(500), TextureCreationFlags(0), OverrideMaterial2DEnabled(false), AllowZWriteOnTransparent(false) { #ifdef _DEBUG @@ -64,7 +64,6 @@ CNullDriver::CNullDriver(io::IFileSystem *io, const core::dimension2d &scre DriverAttributes->addInt("MaxTextures", MATERIAL_MAX_TEXTURES); DriverAttributes->addInt("MaxSupportedTextures", MATERIAL_MAX_TEXTURES); DriverAttributes->addInt("MaxAnisotropy", 1); - // DriverAttributes->addInt("MaxUserClipPlanes", 0); // DriverAttributes->addInt("MaxAuxBuffers", 0); DriverAttributes->addInt("MaxMultipleRenderTargets", 1); DriverAttributes->addInt("MaxIndices", -1); @@ -105,7 +104,6 @@ CNullDriver::CNullDriver(io::IFileSystem *io, const core::dimension2d &scre FeatureEnabled[i] = true; InitMaterial2D.AntiAliasing = video::EAAM_OFF; - InitMaterial2D.Lighting = false; InitMaterial2D.ZWriteEnable = video::EZW_OFF; InitMaterial2D.ZBuffer = video::ECFN_DISABLED; InitMaterial2D.UseMipMaps = false; @@ -223,13 +221,13 @@ void CNullDriver::deleteAllTextures() bool CNullDriver::beginScene(u16 clearFlag, SColor clearColor, f32 clearDepth, u8 clearStencil, const SExposedVideoData &videoData, core::rect *sourceRect) { - PrimitivesDrawn = 0; + FrameStats = {}; return true; } bool CNullDriver::endScene() { - FPSCounter.registerFrame(os::Timer::getRealTime(), PrimitivesDrawn); + FPSCounter.registerFrame(os::Timer::getRealTime()); updateAllHardwareBuffers(); updateAllOcclusionQueries(); return true; @@ -361,7 +359,7 @@ ITexture *CNullDriver::addTextureCubemap(const io::path &name, IImage *imagePosX ITexture *t = 0; - core::array imageArray(6); + std::vector imageArray; imageArray.push_back(imagePosX); imageArray.push_back(imageNegX); imageArray.push_back(imagePosY); @@ -391,7 +389,7 @@ ITexture *CNullDriver::addTextureCubemap(const irr::u32 sideLen, const io::path return 0; } - core::array imageArray(6); + std::vector imageArray; for (int i = 0; i < 6; ++i) imageArray.push_back(new CImage(format, core::dimension2du(sideLen, sideLen))); @@ -548,7 +546,7 @@ ITexture *CNullDriver::createDeviceDependentTexture(const io::path &name, IImage return dummy; } -ITexture *CNullDriver::createDeviceDependentTextureCubemap(const io::path &name, const core::array &image) +ITexture *CNullDriver::createDeviceDependentTextureCubemap(const io::path &name, const std::vector &image) { return new SDummyTexture(name, ETT_CUBEMAP); } @@ -606,7 +604,8 @@ void CNullDriver::drawVertexPrimitiveList(const void *vertices, u32 vertexCount, { if ((iType == EIT_16BIT) && (vertexCount > 65536)) os::Printer::log("Too many vertices for 16bit index type, render artifacts may occur."); - PrimitivesDrawn += primitiveCount; + FrameStats.Drawcalls++; + FrameStats.PrimitivesDrawn += primitiveCount; } //! draws a vertex primitive list in 2d @@ -614,7 +613,8 @@ void CNullDriver::draw2DVertexPrimitiveList(const void *vertices, u32 vertexCoun { if ((iType == EIT_16BIT) && (vertexCount > 65536)) os::Printer::log("Too many vertices for 16bit index type, render artifacts may occur."); - PrimitivesDrawn += primitiveCount; + FrameStats.Drawcalls++; + FrameStats.PrimitivesDrawn += primitiveCount; } //! Draws a 3d line. @@ -744,12 +744,9 @@ s32 CNullDriver::getFPS() const return FPSCounter.getFPS(); } -//! returns amount of primitives (mostly triangles) were drawn in the last frame. -//! very useful method for statistics. -u32 CNullDriver::getPrimitiveCountDrawn(u32 param) const +SFrameStats CNullDriver::getFrameStats() const { - return (0 == param) ? FPSCounter.getPrimitive() : (1 == param) ? FPSCounter.getPrimitiveAverage() - : FPSCounter.getPrimitiveTotal(); + return FrameStats; } //! Sets the dynamic ambient light color. The default color is @@ -913,17 +910,17 @@ bool CNullDriver::checkImage(IImage *image) const return true; } -bool CNullDriver::checkImage(const core::array &image) const +bool CNullDriver::checkImage(const std::vector &image) const { - if (!image.size()) + if (image.empty()) return false; ECOLOR_FORMAT lastFormat = image[0]->getColorFormat(); - core::dimension2d lastSize = image[0]->getDimension(); + auto lastSize = image[0]->getDimension(); - for (u32 i = 0; i < image.size(); ++i) { + for (size_t i = 0; i < image.size(); ++i) { ECOLOR_FORMAT format = image[i]->getColorFormat(); - core::dimension2d size = image[i]->getDimension(); + auto size = image[i]->getDimension(); if (!checkImage(image[i])) return false; @@ -1113,48 +1110,57 @@ void CNullDriver::getFog(SColor &color, E_FOG_TYPE &fogType, f32 &start, f32 &en rangeFog = RangeFog; } -//! Draws a mesh buffer -void CNullDriver::drawMeshBuffer(const scene::IMeshBuffer *mb) +void CNullDriver::drawBuffers(const scene::IVertexBuffer *vb, + const scene::IIndexBuffer *ib, u32 primCount, + scene::E_PRIMITIVE_TYPE pType) { - if (!mb) + if (!vb || !ib) return; - // IVertexBuffer and IIndexBuffer later - SHWBufferLink *HWBuffer = getBufferLink(mb); + if (vb->getHWBuffer() || ib->getHWBuffer()) { + // subclass is supposed to override this if it supports hw buffers + _IRR_DEBUG_BREAK_IF(1); + } - if (HWBuffer) - drawHardwareBuffer(HWBuffer); - else - drawVertexPrimitiveList(mb->getVertices(), mb->getVertexCount(), mb->getIndices(), mb->getPrimitiveCount(), mb->getVertexType(), mb->getPrimitiveType(), mb->getIndexType()); + drawVertexPrimitiveList(vb->getData(), vb->getCount(), ib->getData(), + primCount, vb->getType(), pType, ib->getType()); } //! Draws the normals of a mesh buffer void CNullDriver::drawMeshBufferNormals(const scene::IMeshBuffer *mb, f32 length, SColor color) { const u32 count = mb->getVertexCount(); - const bool normalize = mb->getMaterial().NormalizeNormals; - for (u32 i = 0; i < count; ++i) { - core::vector3df normalizedNormal = mb->getNormal(i); - if (normalize) - normalizedNormal.normalize(); - + core::vector3df normal = mb->getNormal(i); const core::vector3df &pos = mb->getPosition(i); - draw3DLine(pos, pos + (normalizedNormal * length), color); + draw3DLine(pos, pos + (normal * length), color); } } -CNullDriver::SHWBufferLink *CNullDriver::getBufferLink(const scene::IMeshBuffer *mb) +CNullDriver::SHWBufferLink *CNullDriver::getBufferLink(const scene::IVertexBuffer *vb) { - if (!mb || !isHardwareBufferRecommend(mb)) + if (!vb || !isHardwareBufferRecommend(vb)) return 0; // search for hardware links - SHWBufferLink *HWBuffer = reinterpret_cast(mb->getHWBuffer()); + SHWBufferLink *HWBuffer = reinterpret_cast(vb->getHWBuffer()); if (HWBuffer) return HWBuffer; - return createHardwareBuffer(mb); // no hardware links, and mesh wants one, create it + return createHardwareBuffer(vb); // no hardware links, and mesh wants one, create it +} + +CNullDriver::SHWBufferLink *CNullDriver::getBufferLink(const scene::IIndexBuffer *ib) +{ + if (!ib || !isHardwareBufferRecommend(ib)) + return 0; + + // search for hardware links + SHWBufferLink *HWBuffer = reinterpret_cast(ib->getHWBuffer()); + if (HWBuffer) + return HWBuffer; + + return createHardwareBuffer(ib); // no hardware links, and mesh wants one, create it } //! Update all hardware buffers, remove unused ones @@ -1165,8 +1171,13 @@ void CNullDriver::updateAllHardwareBuffers() SHWBufferLink *Link = *it; ++it; - if (!Link->MeshBuffer || Link->MeshBuffer->getReferenceCount() == 1) - deleteHardwareBuffer(Link); + if (Link->IsVertex) { + if (!Link->VertexBuffer || Link->VertexBuffer->getReferenceCount() == 1) + deleteHardwareBuffer(Link); + } else { + if (!Link->IndexBuffer || Link->IndexBuffer->getReferenceCount() == 1) + deleteHardwareBuffer(Link); + } } } @@ -1178,12 +1189,20 @@ void CNullDriver::deleteHardwareBuffer(SHWBufferLink *HWBuffer) delete HWBuffer; } -//! Remove hardware buffer -void CNullDriver::removeHardwareBuffer(const scene::IMeshBuffer *mb) +void CNullDriver::removeHardwareBuffer(const scene::IVertexBuffer *vb) { - if (!mb) + if (!vb) return; - SHWBufferLink *HWBuffer = reinterpret_cast(mb->getHWBuffer()); + SHWBufferLink *HWBuffer = reinterpret_cast(vb->getHWBuffer()); + if (HWBuffer) + deleteHardwareBuffer(HWBuffer); +} + +void CNullDriver::removeHardwareBuffer(const scene::IIndexBuffer *ib) +{ + if (!ib) + return; + SHWBufferLink *HWBuffer = reinterpret_cast(ib->getHWBuffer()); if (HWBuffer) deleteHardwareBuffer(HWBuffer); } @@ -1195,12 +1214,24 @@ void CNullDriver::removeAllHardwareBuffers() deleteHardwareBuffer(HWBufferList.front()); } -bool CNullDriver::isHardwareBufferRecommend(const scene::IMeshBuffer *mb) +bool CNullDriver::isHardwareBufferRecommend(const scene::IVertexBuffer *vb) { - if (!mb || (mb->getHardwareMappingHint_Index() == scene::EHM_NEVER && mb->getHardwareMappingHint_Vertex() == scene::EHM_NEVER)) + if (!vb || vb->getHardwareMappingHint() == scene::EHM_NEVER) return false; - if (mb->getVertexCount() < MinVertexCountForVBO) + if (vb->getCount() < MinVertexCountForVBO) + return false; + + return true; +} + +bool CNullDriver::isHardwareBufferRecommend(const scene::IIndexBuffer *ib) +{ + if (!ib || ib->getHardwareMappingHint() == scene::EHM_NEVER) + return false; + + // This is a bit stupid + if (ib->getCount() < MinVertexCountForVBO * 3) return false; return true; @@ -1269,10 +1300,8 @@ void CNullDriver::runOcclusionQuery(scene::ISceneNode *node, bool visible) OcclusionQueries[index].Run = 0; if (!visible) { SMaterial mat; - mat.Lighting = false; mat.AntiAliasing = 0; mat.ColorMask = ECP_NONE; - mat.GouraudShading = false; mat.ZWriteEnable = EZW_OFF; setMaterial(mat); } @@ -1699,22 +1728,6 @@ IVideoDriver *createNullDriver(io::IFileSystem *io, const core::dimension2d return nullDriver; } -//! Set/unset a clipping plane. -//! There are at least 6 clipping planes available for the user to set at will. -//! \param index: The plane index. Must be between 0 and MaxUserClipPlanes. -//! \param plane: The plane itself. -//! \param enable: If true, enable the clipping plane else disable it. -bool CNullDriver::setClipPlane(u32 index, const core::plane3df &plane, bool enable) -{ - return false; -} - -//! Enable/disable a clipping plane. -void CNullDriver::enableClipPlane(u32 index, bool enable) -{ - // not necessary -} - void CNullDriver::setMinHardwareBufferVertexCount(u32 count) { MinVertexCountForVBO = count; diff --git a/irr/src/CNullDriver.h b/irr/src/CNullDriver.h index d47212c0f..b8d45118f 100644 --- a/irr/src/CNullDriver.h +++ b/irr/src/CNullDriver.h @@ -195,9 +195,7 @@ public: // get current frames per second value s32 getFPS() const override; - //! returns amount of primitives (mostly triangles) were drawn in the last frame. - //! very useful method for statistics. - u32 getPrimitiveCountDrawn(u32 param = 0) const override; + SFrameStats getFrameStats() const override; //! \return Returns the name of the video driver. Example: In case of the DIRECT3D8 //! driver, it would return "Direct3D8.1". @@ -271,8 +269,18 @@ public: const core::position2d &pos, const core::dimension2d &size) override; - //! Draws a mesh buffer - void drawMeshBuffer(const scene::IMeshBuffer *mb) override; + void drawMeshBuffer(const scene::IMeshBuffer *mb) override + { + if (!mb) + return; + drawBuffers(mb->getVertexBuffer(), mb->getIndexBuffer(), + mb->getPrimitiveCount(), mb->getPrimitiveType()); + } + + // Note: this should handle hw buffers + virtual void drawBuffers(const scene::IVertexBuffer *vb, + const scene::IIndexBuffer *ib, u32 primCount, + scene::E_PRIMITIVE_TYPE pType = scene::EPT_TRIANGLES) override; //! Draws the normals of a mesh buffer virtual void drawMeshBufferNormals(const scene::IMeshBuffer *mb, f32 length = 10.f, @@ -285,53 +293,70 @@ public: } protected: + /// Links a hardware buffer to either a vertex or index buffer struct SHWBufferLink { - SHWBufferLink(const scene::IMeshBuffer *_MeshBuffer) : - MeshBuffer(_MeshBuffer), - ChangedID_Vertex(0), ChangedID_Index(0), - Mapped_Vertex(scene::EHM_NEVER), Mapped_Index(scene::EHM_NEVER) + SHWBufferLink(const scene::IVertexBuffer *vb) : + VertexBuffer(vb), ChangedID(0), IsVertex(true) { - if (MeshBuffer) { - MeshBuffer->grab(); - MeshBuffer->setHWBuffer(reinterpret_cast(this)); + if (VertexBuffer) { + VertexBuffer->grab(); + VertexBuffer->setHWBuffer(this); + } + } + SHWBufferLink(const scene::IIndexBuffer *ib) : + IndexBuffer(ib), ChangedID(0), IsVertex(false) + { + if (IndexBuffer) { + IndexBuffer->grab(); + IndexBuffer->setHWBuffer(this); } } virtual ~SHWBufferLink() { - if (MeshBuffer) { - MeshBuffer->setHWBuffer(NULL); - MeshBuffer->drop(); + if (IsVertex && VertexBuffer) { + VertexBuffer->setHWBuffer(nullptr); + VertexBuffer->drop(); + } else if (!IsVertex && IndexBuffer) { + IndexBuffer->setHWBuffer(nullptr); + IndexBuffer->drop(); } } - const scene::IMeshBuffer *MeshBuffer; - u32 ChangedID_Vertex; - u32 ChangedID_Index; - scene::E_HARDWARE_MAPPING Mapped_Vertex; - scene::E_HARDWARE_MAPPING Mapped_Index; - std::list::iterator listPosition; + union { + const scene::IVertexBuffer *VertexBuffer; + const scene::IIndexBuffer *IndexBuffer; + }; + u32 ChangedID; + bool IsVertex; + std::list::iterator listPosition; }; - //! Gets hardware buffer link from a meshbuffer (may create or update buffer) - virtual SHWBufferLink *getBufferLink(const scene::IMeshBuffer *mb); + //! Gets hardware buffer link from a vertex buffer (may create or update buffer) + virtual SHWBufferLink *getBufferLink(const scene::IVertexBuffer *mb); + + //! Gets hardware buffer link from a index buffer (may create or update buffer) + virtual SHWBufferLink *getBufferLink(const scene::IIndexBuffer *mb); //! updates hardware buffer if needed (only some drivers can) virtual bool updateHardwareBuffer(SHWBufferLink *HWBuffer) { return false; } - //! Draw hardware buffer (only some drivers can) - virtual void drawHardwareBuffer(SHWBufferLink *HWBuffer) {} - //! Delete hardware buffer virtual void deleteHardwareBuffer(SHWBufferLink *HWBuffer); - //! Create hardware buffer from mesh (only some drivers can) - virtual SHWBufferLink *createHardwareBuffer(const scene::IMeshBuffer *mb) { return 0; } + //! Create hardware buffer from vertex buffer + virtual SHWBufferLink *createHardwareBuffer(const scene::IVertexBuffer *vb) { return 0; } + + //! Create hardware buffer from index buffer + virtual SHWBufferLink *createHardwareBuffer(const scene::IIndexBuffer *ib) { return 0; } public: //! Remove hardware buffer - void removeHardwareBuffer(const scene::IMeshBuffer *mb) override; + void removeHardwareBuffer(const scene::IVertexBuffer *vb) override; + + //! Remove hardware buffer + void removeHardwareBuffer(const scene::IIndexBuffer *ib) override; //! Remove all hardware buffers void removeAllHardwareBuffers() override; @@ -339,8 +364,11 @@ public: //! Update all hardware buffers, remove unused ones virtual void updateAllHardwareBuffers(); - //! is vbo recommended on this mesh? - virtual bool isHardwareBufferRecommend(const scene::IMeshBuffer *mb); + //! is vbo recommended? + virtual bool isHardwareBufferRecommend(const scene::IVertexBuffer *mb); + + //! is vbo recommended? + virtual bool isHardwareBufferRecommend(const scene::IIndexBuffer *mb); //! Create occlusion query. /** Use node for identification and mesh for occlusion test. */ @@ -494,19 +522,6 @@ public: //! looks if the image is already loaded video::ITexture *findTexture(const io::path &filename) override; - //! Set/unset a clipping plane. - //! There are at least 6 clipping planes available for the user to set at will. - //! \param index: The plane index. Must be between 0 and MaxUserClipPlanes. - //! \param plane: The plane itself. - //! \param enable: If true, enable the clipping plane else disable it. - bool setClipPlane(u32 index, const core::plane3df &plane, bool enable = false) override; - - //! Enable/disable a clipping plane. - //! There are at least 6 clipping planes available for the user to set at will. - //! \param index: The plane index. Must be between 0 and MaxUserClipPlanes. - //! \param enable: If true, enable the clipping plane else disable it. - void enableClipPlane(u32 index, bool enable) override; - //! Returns the graphics card vendor name. core::stringc getVendorInfo() override { return "Not available on this driver."; } @@ -551,8 +566,6 @@ public: virtual void convertColor(const void *sP, ECOLOR_FORMAT sF, s32 sN, void *dP, ECOLOR_FORMAT dF) const override; - bool checkDriverReset() override { return false; } - protected: //! deletes all textures void deleteAllTextures(); @@ -565,14 +578,14 @@ protected: virtual ITexture *createDeviceDependentTexture(const io::path &name, IImage *image); - virtual ITexture *createDeviceDependentTextureCubemap(const io::path &name, const core::array &image); + virtual ITexture *createDeviceDependentTextureCubemap(const io::path &name, const std::vector &image); //! checks triangle count and print warning if wrong bool checkPrimitiveCount(u32 prmcnt) const; bool checkImage(IImage *image) const; - bool checkImage(const core::array &image) const; + bool checkImage(const std::vector &image) const; // adds a material renderer and drops it afterwards. To be used for internal creation s32 addAndDropMaterialRenderer(IMaterialRenderer *m); @@ -583,6 +596,12 @@ protected: // prints renderer version void printVersion(); + inline void accountHWBufferUpload(u32 size) + { + FrameStats.HWBuffersUploaded++; + FrameStats.HWBuffersUploadedSize += size; + } + inline bool getWriteZBuffer(const SMaterial &material) const { switch (material.ZWriteEnable) { @@ -709,8 +728,8 @@ protected: core::matrix4 TransformationMatrix; CFPSCounter FPSCounter; + SFrameStats FrameStats; - u32 PrimitivesDrawn; u32 MinVertexCountForVBO; u32 TextureCreationFlags; diff --git a/irr/src/COBJMeshFileLoader.cpp b/irr/src/COBJMeshFileLoader.cpp index 4c5a5328d..97e90c322 100644 --- a/irr/src/COBJMeshFileLoader.cpp +++ b/irr/src/COBJMeshFileLoader.cpp @@ -182,7 +182,7 @@ IAnimatedMesh *COBJMeshFileLoader::createMesh(io::IReadFile *file) mtlChanged = false; } if (currMtl) - v.Color = currMtl->Meshbuffer->Material.DiffuseColor; + v.Color = video::SColorf(0.8f, 0.8f, 0.8f, 1.0f).toSColor(); // get all vertices data in this face (current line of obj file) const core::stringc wordBuffer = copyLine(bufPtr, bufEnd); @@ -192,6 +192,7 @@ IAnimatedMesh *COBJMeshFileLoader::createMesh(io::IReadFile *file) faceCorners.set_used(0); // fast clear // read in all vertices + auto &Vertices = currMtl->Meshbuffer->Vertices->Data; linePtr = goNextWord(linePtr, endPtr); while (0 != linePtr[0]) { // Array to communicate with retrieveVertexIndices() @@ -228,8 +229,8 @@ IAnimatedMesh *COBJMeshFileLoader::createMesh(io::IReadFile *file) if (n != currMtl->VertMap.end()) { vertLocation = n->second; } else { - currMtl->Meshbuffer->Vertices.push_back(v); - vertLocation = currMtl->Meshbuffer->Vertices.size() - 1; + Vertices.push_back(v); + vertLocation = Vertices.size() - 1; currMtl->VertMap.emplace(v, vertLocation); } @@ -247,15 +248,16 @@ IAnimatedMesh *COBJMeshFileLoader::createMesh(io::IReadFile *file) } // triangulate the face + auto &Indices = currMtl->Meshbuffer->Indices->Data; const int c = faceCorners[0]; for (u32 i = 1; i < faceCorners.size() - 1; ++i) { // Add a triangle const int a = faceCorners[i + 1]; const int b = faceCorners[i]; if (a != b && a != c && b != c) { // ignore degenerated faces. We can get them when we merge vertices above in the VertMap. - currMtl->Meshbuffer->Indices.push_back(a); - currMtl->Meshbuffer->Indices.push_back(b); - currMtl->Meshbuffer->Indices.push_back(c); + Indices.push_back(a); + Indices.push_back(b); + Indices.push_back(c); } else { ++degeneratedFaces; } diff --git a/irr/src/COBJMeshFileLoader.h b/irr/src/COBJMeshFileLoader.h index 63e768e4f..467392a3e 100644 --- a/irr/src/COBJMeshFileLoader.h +++ b/irr/src/COBJMeshFileLoader.h @@ -43,10 +43,6 @@ private: RecalculateNormals(false) { Meshbuffer = new SMeshBuffer(); - Meshbuffer->Material.Shininess = 0.0f; - Meshbuffer->Material.AmbientColor = video::SColorf(0.2f, 0.2f, 0.2f, 1.0f).toSColor(); - Meshbuffer->Material.DiffuseColor = video::SColorf(0.8f, 0.8f, 0.8f, 1.0f).toSColor(); - Meshbuffer->Material.SpecularColor = video::SColorf(1.0f, 1.0f, 1.0f, 1.0f).toSColor(); } SObjMtl(const SObjMtl &o) : diff --git a/irr/src/COGLESCommon.h b/irr/src/COGLESCommon.h deleted file mode 100644 index 1d439546a..000000000 --- a/irr/src/COGLESCommon.h +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (C) 2015 Patryk Nadrowski -// This file is part of the "Irrlicht Engine". -// For conditions of distribution and use, see copyright notice in irrlicht.h - -#pragma once - -#ifdef _IRR_COMPILE_WITH_OGLES1_ - -#if defined(_IRR_COMPILE_WITH_IOS_DEVICE_) -#include -#include -#elif defined(_IRR_OGLES1_USE_KHRONOS_API_HEADERS_) -#include -#include -typedef char GLchar; -#else // or only when defined(_IRR_COMPILE_WITH_ANDROID_DEVICE_) ? -#include -#include -#include -#endif - -#ifndef GL_BGRA -#define GL_BGRA 0x80E1; -#endif - -// Blending definitions. - -#if defined(GL_OES_blend_subtract) -#define GL_FUNC_ADD GL_FUNC_ADD_OES -#else -#define GL_FUNC_ADD 0 -#endif - -// FBO definitions. - -#ifdef GL_OES_framebuffer_object -#define GL_NONE 0 // iOS has missing definition of GL_NONE_OES -#define GL_FRAMEBUFFER GL_FRAMEBUFFER_OES -#define GL_DEPTH_COMPONENT16 GL_DEPTH_COMPONENT16_OES -#define GL_COLOR_ATTACHMENT0 GL_COLOR_ATTACHMENT0_OES -#define GL_DEPTH_ATTACHMENT GL_DEPTH_ATTACHMENT_OES -#define GL_STENCIL_ATTACHMENT GL_STENCIL_ATTACHMENT_OES -#define GL_FRAMEBUFFER_COMPLETE GL_FRAMEBUFFER_COMPLETE_OES -#define GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER 1 -#define GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER 2 -#define GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_OES -#define GL_FRAMEBUFFER_INCOMPLETE_FORMATS GL_FRAMEBUFFER_INCOMPLETE_FORMATS_OES -#define GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_OES -#define GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_OES -#define GL_FRAMEBUFFER_UNSUPPORTED GL_FRAMEBUFFER_UNSUPPORTED_OES -#else -#define GL_NONE 0 -#define GL_FRAMEBUFFER 0 -#define GL_DEPTH_COMPONENT16 0 -#define GL_COLOR_ATTACHMENT0 0 -#define GL_DEPTH_ATTACHMENT 0 -#define GL_STENCIL_ATTACHMENT 0 -#define GL_FRAMEBUFFER_COMPLETE 0 -#define GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER 1 -#define GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER 2 -#define GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT 3 -#define GL_FRAMEBUFFER_INCOMPLETE_FORMATS 4 -#define GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS 5 -#define GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT 6 -#define GL_FRAMEBUFFER_UNSUPPORTED 7 -#endif - -#define GL_DEPTH_COMPONENT 0x1902 - -// Texture definitions. - -#ifdef GL_OES_texture_cube_map -#define GL_TEXTURE_CUBE_MAP GL_TEXTURE_CUBE_MAP_OES -#define GL_TEXTURE_CUBE_MAP_POSITIVE_X GL_TEXTURE_CUBE_MAP_POSITIVE_X_OES -#define GL_TEXTURE_CUBE_MAP_NEGATIVE_X GL_TEXTURE_CUBE_MAP_NEGATIVE_X_OES -#define GL_TEXTURE_CUBE_MAP_POSITIVE_Y GL_TEXTURE_CUBE_MAP_POSITIVE_Y_OES -#define GL_TEXTURE_CUBE_MAP_NEGATIVE_Y GL_TEXTURE_CUBE_MAP_NEGATIVE_Y_OES -#define GL_TEXTURE_CUBE_MAP_POSITIVE_Z GL_TEXTURE_CUBE_MAP_POSITIVE_Z_OES -#define GL_TEXTURE_CUBE_MAP_NEGATIVE_Z GL_TEXTURE_CUBE_MAP_NEGATIVE_Z_OES -#else -#define GL_TEXTURE_CUBE_MAP 0 -#define GL_TEXTURE_CUBE_MAP_POSITIVE_X 0 -#define GL_TEXTURE_CUBE_MAP_NEGATIVE_X 0 -#define GL_TEXTURE_CUBE_MAP_POSITIVE_Y 0 -#define GL_TEXTURE_CUBE_MAP_NEGATIVE_Y 0 -#define GL_TEXTURE_CUBE_MAP_POSITIVE_Z 0 -#define GL_TEXTURE_CUBE_MAP_NEGATIVE_Z 0 -#endif - -// to check if this header is in the current compile unit (different GL implementation used different "GLCommon" headers in Irrlicht -#define IRR_COMPILE_GLES_COMMON - -// macro used with COGLES1Driver -#define TEST_GL_ERROR(cls) (cls)->testGLError(__LINE__) - -namespace irr -{ -namespace video -{ - -// Forward declarations. - -class COpenGLCoreFeature; - -template -class COpenGLCoreTexture; - -template -class COpenGLCoreRenderTarget; - -template -class COpenGLCoreCacheHandler; - -class COGLES1Driver; -typedef COpenGLCoreTexture COGLES1Texture; -typedef COpenGLCoreRenderTarget COGLES1RenderTarget; -typedef COpenGLCoreCacheHandler COGLES1CacheHandler; - -} -} - -#endif diff --git a/irr/src/COGLESDriver.cpp b/irr/src/COGLESDriver.cpp deleted file mode 100644 index 524aedd50..000000000 --- a/irr/src/COGLESDriver.cpp +++ /dev/null @@ -1,2397 +0,0 @@ -// Copyright (C) 2002-2008 Nikolaus Gebhardt -// This file is part of the "Irrlicht Engine". -// For conditions of distribution and use, see copyright notice in irrlicht.h - -#include "COGLESDriver.h" -#include -#include "CNullDriver.h" -#include "IContextManager.h" - -#ifdef _IRR_COMPILE_WITH_OGLES1_ - -#include "COpenGLCoreTexture.h" -#include "COpenGLCoreRenderTarget.h" -#include "COpenGLCoreCacheHandler.h" - -#include "COGLESMaterialRenderer.h" - -#include "EVertexAttributes.h" -#include "CImage.h" -#include "os.h" - -namespace irr -{ -namespace video -{ - -COGLES1Driver::COGLES1Driver(const SIrrlichtCreationParameters ¶ms, io::IFileSystem *io, IContextManager *contextManager) : - CNullDriver(io, params.WindowSize), COGLES1ExtensionHandler(), CacheHandler(0), CurrentRenderMode(ERM_NONE), - ResetRenderStates(true), Transformation3DChanged(true), AntiAlias(params.AntiAlias), - ColorFormat(ECF_R8G8B8), Params(params), ContextManager(contextManager) -{ -#ifdef _DEBUG - setDebugName("COGLESDriver"); -#endif - - core::dimension2d windowSize(0, 0); - - if (!ContextManager) - return; - - ContextManager->grab(); - ContextManager->generateSurface(); - ContextManager->generateContext(); - ExposedData = ContextManager->getContext(); - ContextManager->activateContext(ExposedData, false); - - windowSize = params.WindowSize; - - genericDriverInit(windowSize, params.Stencilbuffer); -} - -COGLES1Driver::~COGLES1Driver() -{ - deleteMaterialRenders(); - - CacheHandler->getTextureCache().clear(); - - removeAllRenderTargets(); - deleteAllTextures(); - removeAllOcclusionQueries(); - removeAllHardwareBuffers(); - - delete CacheHandler; - - if (ContextManager) { - ContextManager->destroyContext(); - ContextManager->destroySurface(); - ContextManager->terminate(); - ContextManager->drop(); - } -} - -// ----------------------------------------------------------------------- -// METHODS -// ----------------------------------------------------------------------- - -bool COGLES1Driver::genericDriverInit(const core::dimension2d &screenSize, bool stencilBuffer) -{ - Name = glGetString(GL_VERSION); - printVersion(); - - // print renderer information - VendorName = glGetString(GL_VENDOR); - os::Printer::log(VendorName.c_str(), ELL_INFORMATION); - - // load extensions - initExtensions(); - - // reset cache handler - delete CacheHandler; - CacheHandler = new COGLES1CacheHandler(this); - - StencilBuffer = stencilBuffer; - - DriverAttributes->setAttribute("MaxTextures", (s32)Feature.MaxTextureUnits); - DriverAttributes->setAttribute("MaxSupportedTextures", (s32)Feature.MaxTextureUnits); - DriverAttributes->setAttribute("MaxAnisotropy", MaxAnisotropy); - DriverAttributes->setAttribute("MaxIndices", (s32)MaxIndices); - DriverAttributes->setAttribute("MaxTextureSize", (s32)MaxTextureSize); - DriverAttributes->setAttribute("MaxTextureLODBias", MaxTextureLODBias); - DriverAttributes->setAttribute("Version", Version); - DriverAttributes->setAttribute("AntiAlias", AntiAlias); - - glPixelStorei(GL_PACK_ALIGNMENT, 1); - - UserClipPlane.reallocate(MaxUserClipPlanes); - UserClipPlaneEnabled.resize(MaxUserClipPlanes); - - for (s32 i = 0; i < MaxUserClipPlanes; ++i) { - UserClipPlane.push_back(core::plane3df()); - UserClipPlaneEnabled[i] = false; - } - - for (s32 i = 0; i < ETS_COUNT; ++i) - setTransform(static_cast(i), core::IdentityMatrix); - - setAmbientLight(SColorf(0.0f, 0.0f, 0.0f, 0.0f)); - glClearDepthf(1.0f); - - glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_FASTEST); - glHint(GL_GENERATE_MIPMAP_HINT, GL_FASTEST); - glDepthFunc(GL_LEQUAL); - glFrontFace(GL_CW); - glAlphaFunc(GL_GREATER, 0.f); - - // create material renderers - createMaterialRenderers(); - - // set the renderstates - setRenderStates3DMode(); - - // set fog mode - setFog(FogColor, FogType, FogStart, FogEnd, FogDensity, PixelFog, RangeFog); - - // create matrix for flipping textures - TextureFlipMatrix.buildTextureTransform(0.0f, core::vector2df(0, 0), core::vector2df(0, 1.0f), core::vector2df(1.0f, -1.0f)); - - // We need to reset once more at the beginning of the first rendering. - // This fixes problems with intermediate changes to the material during texture load. - ResetRenderStates = true; - - testGLError(__LINE__); - - return true; -} - -void COGLES1Driver::createMaterialRenderers() -{ - addAndDropMaterialRenderer(new COGLES1MaterialRenderer_SOLID(this)); - addAndDropMaterialRenderer(new COGLES1MaterialRenderer_TRANSPARENT_ALPHA_CHANNEL(this)); - addAndDropMaterialRenderer(new COGLES1MaterialRenderer_TRANSPARENT_ALPHA_CHANNEL_REF(this)); - addAndDropMaterialRenderer(new COGLES1MaterialRenderer_TRANSPARENT_VERTEX_ALPHA(this)); - addAndDropMaterialRenderer(new COGLES1MaterialRenderer_ONETEXTURE_BLEND(this)); -} - -bool COGLES1Driver::beginScene(u16 clearFlag, SColor clearColor, f32 clearDepth, u8 clearStencil, const SExposedVideoData &videoData, core::rect *sourceRect) -{ - CNullDriver::beginScene(clearFlag, clearColor, clearDepth, clearStencil, videoData, sourceRect); - - if (ContextManager) - ContextManager->activateContext(videoData, true); - - clearBuffers(clearFlag, clearColor, clearDepth, clearStencil); - - return true; -} - -bool COGLES1Driver::endScene() -{ - CNullDriver::endScene(); - - glFlush(); - - if (ContextManager) - return ContextManager->swapBuffers(); - - return false; -} - -//! Returns the transformation set by setTransform -const core::matrix4 &COGLES1Driver::getTransform(E_TRANSFORMATION_STATE state) const -{ - return Matrices[state]; -} - -//! sets transformation -void COGLES1Driver::setTransform(E_TRANSFORMATION_STATE state, const core::matrix4 &mat) -{ - Matrices[state] = mat; - Transformation3DChanged = true; - - switch (state) { - case ETS_VIEW: - case ETS_WORLD: { - // OGLES1 only has a model matrix, view and world is not existent. so lets fake these two. - glMatrixMode(GL_MODELVIEW); - glLoadMatrixf((Matrices[ETS_VIEW] * Matrices[ETS_WORLD]).pointer()); - // we have to update the clip planes to the latest view matrix - for (u32 i = 0; i < MaxUserClipPlanes; ++i) - if (UserClipPlaneEnabled[i]) - uploadClipPlane(i); - } break; - case ETS_PROJECTION: { - GLfloat glmat[16]; - getGLMatrix(glmat, mat); - // flip z to compensate OGLES1s right-hand coordinate system - glmat[12] *= -1.0f; - glMatrixMode(GL_PROJECTION); - glLoadMatrixf(glmat); - } break; - default: - break; - } -} - -bool COGLES1Driver::updateVertexHardwareBuffer(SHWBufferLink_opengl *HWBuffer) -{ - if (!HWBuffer) - return false; - - const scene::IMeshBuffer *mb = HWBuffer->MeshBuffer; - const void *vertices = mb->getVertices(); - const u32 vertexCount = mb->getVertexCount(); - const E_VERTEX_TYPE vType = mb->getVertexType(); - const u32 vertexSize = getVertexPitchFromType(vType); - - // buffer vertex data, and convert colours... - core::array buffer(vertexSize * vertexCount); - buffer.set_used(vertexSize * vertexCount); - memcpy(buffer.pointer(), vertices, vertexSize * vertexCount); - - // in order to convert the colors into opengl format (RGBA) - switch (vType) { - case EVT_STANDARD: { - S3DVertex *pb = reinterpret_cast(buffer.pointer()); - const S3DVertex *po = static_cast(vertices); - for (u32 i = 0; i < vertexCount; i++) { - po[i].Color.toOpenGLColor((u8 *)&(pb[i].Color.color)); - } - } break; - case EVT_2TCOORDS: { - S3DVertex2TCoords *pb = reinterpret_cast(buffer.pointer()); - const S3DVertex2TCoords *po = static_cast(vertices); - for (u32 i = 0; i < vertexCount; i++) { - po[i].Color.toOpenGLColor((u8 *)&(pb[i].Color.color)); - } - } break; - case EVT_TANGENTS: { - S3DVertexTangents *pb = reinterpret_cast(buffer.pointer()); - const S3DVertexTangents *po = static_cast(vertices); - for (u32 i = 0; i < vertexCount; i++) { - po[i].Color.toOpenGLColor((u8 *)&(pb[i].Color.color)); - } - } break; - default: { - return false; - } - } - - // get or create buffer - bool newBuffer = false; - if (!HWBuffer->vbo_verticesID) { - glGenBuffers(1, &HWBuffer->vbo_verticesID); - if (!HWBuffer->vbo_verticesID) - return false; - newBuffer = true; - } else if (HWBuffer->vbo_verticesSize < vertexCount * vertexSize) { - newBuffer = true; - } - - glBindBuffer(GL_ARRAY_BUFFER, HWBuffer->vbo_verticesID); - - // copy data to graphics card - if (!newBuffer) - glBufferSubData(GL_ARRAY_BUFFER, 0, vertexCount * vertexSize, buffer.const_pointer()); - else { - HWBuffer->vbo_verticesSize = vertexCount * vertexSize; - - if (HWBuffer->Mapped_Vertex == scene::EHM_STATIC) - glBufferData(GL_ARRAY_BUFFER, vertexCount * vertexSize, buffer.const_pointer(), GL_STATIC_DRAW); - else - glBufferData(GL_ARRAY_BUFFER, vertexCount * vertexSize, buffer.const_pointer(), GL_DYNAMIC_DRAW); - } - - glBindBuffer(GL_ARRAY_BUFFER, 0); - - return (!testGLError(__LINE__)); -} - -bool COGLES1Driver::updateIndexHardwareBuffer(SHWBufferLink_opengl *HWBuffer) -{ - if (!HWBuffer) - return false; - - const scene::IMeshBuffer *mb = HWBuffer->MeshBuffer; - - const void *indices = mb->getIndices(); - u32 indexCount = mb->getIndexCount(); - - GLenum indexSize; - switch (mb->getIndexType()) { - case (EIT_16BIT): { - indexSize = sizeof(u16); - break; - } - case (EIT_32BIT): { - indexSize = sizeof(u32); - break; - } - default: { - return false; - } - } - - // get or create buffer - bool newBuffer = false; - if (!HWBuffer->vbo_indicesID) { - glGenBuffers(1, &HWBuffer->vbo_indicesID); - if (!HWBuffer->vbo_indicesID) - return false; - newBuffer = true; - } else if (HWBuffer->vbo_indicesSize < indexCount * indexSize) { - newBuffer = true; - } - - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, HWBuffer->vbo_indicesID); - - // copy data to graphics card - if (!newBuffer) - glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, indexCount * indexSize, indices); - else { - HWBuffer->vbo_indicesSize = indexCount * indexSize; - - if (HWBuffer->Mapped_Index == scene::EHM_STATIC) - glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexCount * indexSize, indices, GL_STATIC_DRAW); - else - glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexCount * indexSize, indices, GL_DYNAMIC_DRAW); - } - - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); - - return (!testGLError(__LINE__)); -} - -//! updates hardware buffer if needed -bool COGLES1Driver::updateHardwareBuffer(SHWBufferLink *HWBuffer) -{ - if (!HWBuffer) - return false; - - if (HWBuffer->Mapped_Vertex != scene::EHM_NEVER) { - if (HWBuffer->ChangedID_Vertex != HWBuffer->MeshBuffer->getChangedID_Vertex() || !static_cast(HWBuffer)->vbo_verticesID) { - - HWBuffer->ChangedID_Vertex = HWBuffer->MeshBuffer->getChangedID_Vertex(); - - if (!updateVertexHardwareBuffer(static_cast(HWBuffer))) - return false; - } - } - - if (HWBuffer->Mapped_Index != scene::EHM_NEVER) { - if (HWBuffer->ChangedID_Index != HWBuffer->MeshBuffer->getChangedID_Index() || !((SHWBufferLink_opengl *)HWBuffer)->vbo_indicesID) { - - HWBuffer->ChangedID_Index = HWBuffer->MeshBuffer->getChangedID_Index(); - - if (!updateIndexHardwareBuffer(static_cast(HWBuffer))) - return false; - } - } - - return true; -} - -//! Create hardware buffer from meshbuffer -COGLES1Driver::SHWBufferLink *COGLES1Driver::createHardwareBuffer(const scene::IMeshBuffer *mb) -{ - if (!mb || (mb->getHardwareMappingHint_Index() == scene::EHM_NEVER && mb->getHardwareMappingHint_Vertex() == scene::EHM_NEVER)) - return 0; - - SHWBufferLink_opengl *HWBuffer = new SHWBufferLink_opengl(mb); - - // add to map - HWBuffer->listPosition = HWBufferList.insert(HWBufferList.end(), HWBuffer); - - HWBuffer->ChangedID_Vertex = HWBuffer->MeshBuffer->getChangedID_Vertex(); - HWBuffer->ChangedID_Index = HWBuffer->MeshBuffer->getChangedID_Index(); - HWBuffer->Mapped_Vertex = mb->getHardwareMappingHint_Vertex(); - HWBuffer->Mapped_Index = mb->getHardwareMappingHint_Index(); - HWBuffer->vbo_verticesID = 0; - HWBuffer->vbo_indicesID = 0; - HWBuffer->vbo_verticesSize = 0; - HWBuffer->vbo_indicesSize = 0; - - if (!updateHardwareBuffer(HWBuffer)) { - deleteHardwareBuffer(HWBuffer); - return 0; - } - - return HWBuffer; -} - -void COGLES1Driver::deleteHardwareBuffer(SHWBufferLink *_HWBuffer) -{ - if (!_HWBuffer) - return; - - SHWBufferLink_opengl *HWBuffer = static_cast(_HWBuffer); - if (HWBuffer->vbo_verticesID) { - glDeleteBuffers(1, &HWBuffer->vbo_verticesID); - HWBuffer->vbo_verticesID = 0; - } - if (HWBuffer->vbo_indicesID) { - glDeleteBuffers(1, &HWBuffer->vbo_indicesID); - HWBuffer->vbo_indicesID = 0; - } - - CNullDriver::deleteHardwareBuffer(_HWBuffer); -} - -//! Draw hardware buffer -void COGLES1Driver::drawHardwareBuffer(SHWBufferLink *_HWBuffer) -{ - if (!_HWBuffer) - return; - - SHWBufferLink_opengl *HWBuffer = static_cast(_HWBuffer); - - updateHardwareBuffer(HWBuffer); // check if update is needed - - const scene::IMeshBuffer *mb = HWBuffer->MeshBuffer; - const void *vertices = mb->getVertices(); - const void *indexList = mb->getIndices(); - - if (HWBuffer->Mapped_Vertex != scene::EHM_NEVER) { - glBindBuffer(GL_ARRAY_BUFFER, HWBuffer->vbo_verticesID); - vertices = 0; - } - - if (HWBuffer->Mapped_Index != scene::EHM_NEVER) { - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, HWBuffer->vbo_indicesID); - indexList = 0; - } - - drawVertexPrimitiveList(vertices, mb->getVertexCount(), indexList, - mb->getPrimitiveCount(), mb->getVertexType(), - mb->getPrimitiveType(), mb->getIndexType()); - - if (HWBuffer->Mapped_Vertex != scene::EHM_NEVER) - glBindBuffer(GL_ARRAY_BUFFER, 0); - - if (HWBuffer->Mapped_Index != scene::EHM_NEVER) - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); -} - -IRenderTarget *COGLES1Driver::addRenderTarget() -{ - COGLES1RenderTarget *renderTarget = new COGLES1RenderTarget(this); - RenderTargets.push_back(renderTarget); - - return renderTarget; -} - -// small helper function to create vertex buffer object adress offsets -static inline u8 *buffer_offset(const long offset) -{ - return ((u8 *)0 + offset); -} - -//! draws a vertex primitive list -void COGLES1Driver::drawVertexPrimitiveList(const void *vertices, u32 vertexCount, - const void *indexList, u32 primitiveCount, - E_VERTEX_TYPE vType, scene::E_PRIMITIVE_TYPE pType, E_INDEX_TYPE iType) -{ - if (!checkPrimitiveCount(primitiveCount)) - return; - - setRenderStates3DMode(); - - drawVertexPrimitiveList2d3d(vertices, vertexCount, (const u16 *)indexList, primitiveCount, vType, pType, iType); -} - -void COGLES1Driver::drawVertexPrimitiveList2d3d(const void *vertices, u32 vertexCount, - const void *indexList, u32 primitiveCount, - E_VERTEX_TYPE vType, scene::E_PRIMITIVE_TYPE pType, E_INDEX_TYPE iType, bool threed) -{ - if (!primitiveCount || !vertexCount) - return; - - if (!threed && !checkPrimitiveCount(primitiveCount)) - return; - - CNullDriver::drawVertexPrimitiveList(vertices, vertexCount, indexList, primitiveCount, vType, pType, iType); - - if (vertices) { - // convert colors to gl color format. - vertexCount *= 4; // reused as color component count - ColorBuffer.set_used(vertexCount); - u32 i; - - switch (vType) { - case EVT_STANDARD: { - const S3DVertex *p = static_cast(vertices); - for (i = 0; i < vertexCount; i += 4) { - p->Color.toOpenGLColor(&ColorBuffer[i]); - ++p; - } - } break; - case EVT_2TCOORDS: { - const S3DVertex2TCoords *p = static_cast(vertices); - for (i = 0; i < vertexCount; i += 4) { - p->Color.toOpenGLColor(&ColorBuffer[i]); - ++p; - } - } break; - case EVT_TANGENTS: { - const S3DVertexTangents *p = static_cast(vertices); - for (i = 0; i < vertexCount; i += 4) { - p->Color.toOpenGLColor(&ColorBuffer[i]); - ++p; - } - } break; - } - } - - // draw everything - glClientActiveTexture(GL_TEXTURE0); - glEnableClientState(GL_COLOR_ARRAY); - glEnableClientState(GL_VERTEX_ARRAY); - if ((pType != scene::EPT_POINTS) && (pType != scene::EPT_POINT_SPRITES)) - glEnableClientState(GL_TEXTURE_COORD_ARRAY); -#ifdef GL_OES_point_size_array - else if (FeatureAvailable[COGLESCoreExtensionHandler::IRR_GL_OES_point_size_array] && (Material.Thickness == 0.0f)) - glEnableClientState(GL_POINT_SIZE_ARRAY_OES); -#endif - if (threed && (pType != scene::EPT_POINTS) && (pType != scene::EPT_POINT_SPRITES)) - glEnableClientState(GL_NORMAL_ARRAY); - - if (vertices) - glColorPointer(4, GL_UNSIGNED_BYTE, 0, &ColorBuffer[0]); - - switch (vType) { - case EVT_STANDARD: - if (vertices) { - if (threed) - glNormalPointer(GL_FLOAT, sizeof(S3DVertex), &(static_cast(vertices))[0].Normal); - glTexCoordPointer(2, GL_FLOAT, sizeof(S3DVertex), &(static_cast(vertices))[0].TCoords); - glVertexPointer((threed ? 3 : 2), GL_FLOAT, sizeof(S3DVertex), &(static_cast(vertices))[0].Pos); - } else { - glNormalPointer(GL_FLOAT, sizeof(S3DVertex), buffer_offset(12)); - glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(S3DVertex), buffer_offset(24)); - glTexCoordPointer(2, GL_FLOAT, sizeof(S3DVertex), buffer_offset(28)); - glVertexPointer(3, GL_FLOAT, sizeof(S3DVertex), 0); - } - - if (Feature.MaxTextureUnits > 0 && CacheHandler->getTextureCache().get(1)) { - glClientActiveTexture(GL_TEXTURE0 + 1); - glEnableClientState(GL_TEXTURE_COORD_ARRAY); - if (vertices) - glTexCoordPointer(2, GL_FLOAT, sizeof(S3DVertex), &(static_cast(vertices))[0].TCoords); - else - glTexCoordPointer(2, GL_FLOAT, sizeof(S3DVertex), buffer_offset(28)); - } - break; - case EVT_2TCOORDS: - if (vertices) { - if (threed) - glNormalPointer(GL_FLOAT, sizeof(S3DVertex2TCoords), &(static_cast(vertices))[0].Normal); - glTexCoordPointer(2, GL_FLOAT, sizeof(S3DVertex2TCoords), &(static_cast(vertices))[0].TCoords); - glVertexPointer((threed ? 3 : 2), GL_FLOAT, sizeof(S3DVertex2TCoords), &(static_cast(vertices))[0].Pos); - } else { - glNormalPointer(GL_FLOAT, sizeof(S3DVertex2TCoords), buffer_offset(12)); - glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(S3DVertex2TCoords), buffer_offset(24)); - glTexCoordPointer(2, GL_FLOAT, sizeof(S3DVertex2TCoords), buffer_offset(28)); - glVertexPointer(3, GL_FLOAT, sizeof(S3DVertex2TCoords), buffer_offset(0)); - } - - if (Feature.MaxTextureUnits > 0) { - glClientActiveTexture(GL_TEXTURE0 + 1); - glEnableClientState(GL_TEXTURE_COORD_ARRAY); - if (vertices) - glTexCoordPointer(2, GL_FLOAT, sizeof(S3DVertex2TCoords), &(static_cast(vertices))[0].TCoords2); - else - glTexCoordPointer(2, GL_FLOAT, sizeof(S3DVertex2TCoords), buffer_offset(36)); - } - break; - case EVT_TANGENTS: - if (vertices) { - if (threed) - glNormalPointer(GL_FLOAT, sizeof(S3DVertexTangents), &(static_cast(vertices))[0].Normal); - glTexCoordPointer(2, GL_FLOAT, sizeof(S3DVertexTangents), &(static_cast(vertices))[0].TCoords); - glVertexPointer((threed ? 3 : 2), GL_FLOAT, sizeof(S3DVertexTangents), &(static_cast(vertices))[0].Pos); - } else { - glNormalPointer(GL_FLOAT, sizeof(S3DVertexTangents), buffer_offset(12)); - glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(S3DVertexTangents), buffer_offset(24)); - glTexCoordPointer(2, GL_FLOAT, sizeof(S3DVertexTangents), buffer_offset(28)); - glVertexPointer(3, GL_FLOAT, sizeof(S3DVertexTangents), buffer_offset(0)); - } - - if (Feature.MaxTextureUnits > 0) { - glClientActiveTexture(GL_TEXTURE0 + 1); - glEnableClientState(GL_TEXTURE_COORD_ARRAY); - if (vertices) - glTexCoordPointer(3, GL_FLOAT, sizeof(S3DVertexTangents), &(static_cast(vertices))[0].Tangent); - else - glTexCoordPointer(3, GL_FLOAT, sizeof(S3DVertexTangents), buffer_offset(36)); - - glClientActiveTexture(GL_TEXTURE0 + 2); - glEnableClientState(GL_TEXTURE_COORD_ARRAY); - if (vertices) - glTexCoordPointer(3, GL_FLOAT, sizeof(S3DVertexTangents), &(static_cast(vertices))[0].Binormal); - else - glTexCoordPointer(3, GL_FLOAT, sizeof(S3DVertexTangents), buffer_offset(48)); - } - break; - } - - GLenum indexSize = 0; - - switch (iType) { - case (EIT_16BIT): { - indexSize = GL_UNSIGNED_SHORT; - break; - } - case (EIT_32BIT): { -#ifdef GL_OES_element_index_uint -#ifndef GL_UNSIGNED_INT -#define GL_UNSIGNED_INT 0x1405 -#endif - if (FeatureAvailable[COGLESCoreExtensionHandler::IRR_GL_OES_element_index_uint]) - indexSize = GL_UNSIGNED_INT; - else -#endif - indexSize = GL_UNSIGNED_SHORT; - break; - } - } - - switch (pType) { - case scene::EPT_POINTS: - case scene::EPT_POINT_SPRITES: { -#ifdef GL_OES_point_sprite - if (pType == scene::EPT_POINT_SPRITES && FeatureAvailable[COGLESCoreExtensionHandler::IRR_GL_OES_point_sprite]) - glEnable(GL_POINT_SPRITE_OES); -#endif - // if ==0 we use the point size array - if (Material.Thickness != 0.f) { - float quadratic[] = {0.0f, 0.0f, 10.01f}; - glPointParameterfv(GL_POINT_DISTANCE_ATTENUATION, quadratic); - float maxParticleSize = 1.0f; - glGetFloatv(GL_POINT_SIZE_MAX, &maxParticleSize); - // maxParticleSize=maxParticleSize 0) { - if (vType == EVT_TANGENTS) { - glClientActiveTexture(GL_TEXTURE0 + 2); - glDisableClientState(GL_TEXTURE_COORD_ARRAY); - } - if ((vType != EVT_STANDARD) || CacheHandler->getTextureCache().get(1)) { - glClientActiveTexture(GL_TEXTURE0 + 1); - glDisableClientState(GL_TEXTURE_COORD_ARRAY); - } - glClientActiveTexture(GL_TEXTURE0); - } - -#ifdef GL_OES_point_size_array - if (FeatureAvailable[COGLESCoreExtensionHandler::IRR_GL_OES_point_size_array] && (Material.Thickness == 0.0f)) - glDisableClientState(GL_POINT_SIZE_ARRAY_OES); -#endif - - glDisableClientState(GL_COLOR_ARRAY); - glDisableClientState(GL_VERTEX_ARRAY); - glDisableClientState(GL_NORMAL_ARRAY); - glDisableClientState(GL_TEXTURE_COORD_ARRAY); -} - -//! draws a 2d image, using a color and the alpha channel of the texture -void COGLES1Driver::draw2DImage(const video::ITexture *texture, - const core::position2d &pos, - const core::rect &sourceRect, - const core::rect *clipRect, SColor color, - bool useAlphaChannelOfTexture) -{ - if (!texture) - return; - - if (!sourceRect.isValid()) - return; - - core::position2d targetPos(pos); - core::position2d sourcePos(sourceRect.UpperLeftCorner); - core::dimension2d sourceSize(sourceRect.getSize()); - if (clipRect) { - if (targetPos.X < clipRect->UpperLeftCorner.X) { - sourceSize.Width += targetPos.X - clipRect->UpperLeftCorner.X; - if (sourceSize.Width <= 0) - return; - - sourcePos.X -= targetPos.X - clipRect->UpperLeftCorner.X; - targetPos.X = clipRect->UpperLeftCorner.X; - } - - if (targetPos.X + sourceSize.Width > clipRect->LowerRightCorner.X) { - sourceSize.Width -= (targetPos.X + sourceSize.Width) - clipRect->LowerRightCorner.X; - if (sourceSize.Width <= 0) - return; - } - - if (targetPos.Y < clipRect->UpperLeftCorner.Y) { - sourceSize.Height += targetPos.Y - clipRect->UpperLeftCorner.Y; - if (sourceSize.Height <= 0) - return; - - sourcePos.Y -= targetPos.Y - clipRect->UpperLeftCorner.Y; - targetPos.Y = clipRect->UpperLeftCorner.Y; - } - - if (targetPos.Y + sourceSize.Height > clipRect->LowerRightCorner.Y) { - sourceSize.Height -= (targetPos.Y + sourceSize.Height) - clipRect->LowerRightCorner.Y; - if (sourceSize.Height <= 0) - return; - } - } - - // clip these coordinates - - if (targetPos.X < 0) { - sourceSize.Width += targetPos.X; - if (sourceSize.Width <= 0) - return; - - sourcePos.X -= targetPos.X; - targetPos.X = 0; - } - - const core::dimension2d &renderTargetSize = getCurrentRenderTargetSize(); - - if (targetPos.X + sourceSize.Width > (s32)renderTargetSize.Width) { - sourceSize.Width -= (targetPos.X + sourceSize.Width) - renderTargetSize.Width; - if (sourceSize.Width <= 0) - return; - } - - if (targetPos.Y < 0) { - sourceSize.Height += targetPos.Y; - if (sourceSize.Height <= 0) - return; - - sourcePos.Y -= targetPos.Y; - targetPos.Y = 0; - } - - if (targetPos.Y + sourceSize.Height > (s32)renderTargetSize.Height) { - sourceSize.Height -= (targetPos.Y + sourceSize.Height) - renderTargetSize.Height; - if (sourceSize.Height <= 0) - return; - } - - // ok, we've clipped everything. - // now draw it. - - // texcoords need to be flipped horizontally for RTTs - const bool isRTT = texture->isRenderTarget(); - const core::dimension2d &ss = texture->getOriginalSize(); - const f32 invW = 1.f / static_cast(ss.Width); - const f32 invH = 1.f / static_cast(ss.Height); - const core::rect tcoords( - sourcePos.X * invW, - (isRTT ? (sourcePos.Y + sourceSize.Height) : sourcePos.Y) * invH, - (sourcePos.X + sourceSize.Width) * invW, - (isRTT ? sourcePos.Y : (sourcePos.Y + sourceSize.Height)) * invH); - - const core::rect poss(targetPos, sourceSize); - - if (!CacheHandler->getTextureCache().set(0, texture)) - return; - - setRenderStates2DMode(color.getAlpha() < 255, true, useAlphaChannelOfTexture); - - u16 indices[] = {0, 1, 2, 3}; - S3DVertex vertices[4]; - vertices[0] = S3DVertex((f32)poss.UpperLeftCorner.X, (f32)poss.UpperLeftCorner.Y, 0, 0, 0, 1, color, tcoords.UpperLeftCorner.X, tcoords.UpperLeftCorner.Y); - vertices[1] = S3DVertex((f32)poss.LowerRightCorner.X, (f32)poss.UpperLeftCorner.Y, 0, 0, 0, 1, color, tcoords.LowerRightCorner.X, tcoords.UpperLeftCorner.Y); - vertices[2] = S3DVertex((f32)poss.LowerRightCorner.X, (f32)poss.LowerRightCorner.Y, 0, 0, 0, 1, color, tcoords.LowerRightCorner.X, tcoords.LowerRightCorner.Y); - vertices[3] = S3DVertex((f32)poss.UpperLeftCorner.X, (f32)poss.LowerRightCorner.Y, 0, 0, 0, 1, color, tcoords.UpperLeftCorner.X, tcoords.LowerRightCorner.Y); - drawVertexPrimitiveList2d3d(vertices, 4, indices, 2, video::EVT_STANDARD, scene::EPT_TRIANGLE_FAN, EIT_16BIT, false); -} - -//! The same, but with a four element array of colors, one for each vertex -void COGLES1Driver::draw2DImage(const video::ITexture *texture, const core::rect &destRect, - const core::rect &sourceRect, const core::rect *clipRect, - const video::SColor *const colors, bool useAlphaChannelOfTexture) -{ - if (!texture) - return; - - // texcoords need to be flipped horizontally for RTTs - const bool isRTT = texture->isRenderTarget(); - const core::dimension2du &ss = texture->getOriginalSize(); - const f32 invW = 1.f / static_cast(ss.Width); - const f32 invH = 1.f / static_cast(ss.Height); - const core::rect tcoords( - sourceRect.UpperLeftCorner.X * invW, - (isRTT ? sourceRect.LowerRightCorner.Y : sourceRect.UpperLeftCorner.Y) * invH, - sourceRect.LowerRightCorner.X * invW, - (isRTT ? sourceRect.UpperLeftCorner.Y : sourceRect.LowerRightCorner.Y) * invH); - - const video::SColor temp[4] = { - 0xFFFFFFFF, - 0xFFFFFFFF, - 0xFFFFFFFF, - 0xFFFFFFFF, - }; - - const video::SColor *const useColor = colors ? colors : temp; - - if (!CacheHandler->getTextureCache().set(0, texture)) - return; - - setRenderStates2DMode(useColor[0].getAlpha() < 255 || useColor[1].getAlpha() < 255 || - useColor[2].getAlpha() < 255 || useColor[3].getAlpha() < 255, - true, useAlphaChannelOfTexture); - - if (clipRect) { - if (!clipRect->isValid()) - return; - - glEnable(GL_SCISSOR_TEST); - const core::dimension2d &renderTargetSize = getCurrentRenderTargetSize(); - glScissor(clipRect->UpperLeftCorner.X, renderTargetSize.Height - clipRect->LowerRightCorner.Y, - clipRect->getWidth(), clipRect->getHeight()); - } - - u16 indices[] = {0, 1, 2, 3}; - S3DVertex vertices[4]; - vertices[0] = S3DVertex((f32)destRect.UpperLeftCorner.X, (f32)destRect.UpperLeftCorner.Y, 0, 0, 0, 1, useColor[0], tcoords.UpperLeftCorner.X, tcoords.UpperLeftCorner.Y); - vertices[1] = S3DVertex((f32)destRect.LowerRightCorner.X, (f32)destRect.UpperLeftCorner.Y, 0, 0, 0, 1, useColor[3], tcoords.LowerRightCorner.X, tcoords.UpperLeftCorner.Y); - vertices[2] = S3DVertex((f32)destRect.LowerRightCorner.X, (f32)destRect.LowerRightCorner.Y, 0, 0, 0, 1, useColor[2], tcoords.LowerRightCorner.X, tcoords.LowerRightCorner.Y); - vertices[3] = S3DVertex((f32)destRect.UpperLeftCorner.X, (f32)destRect.LowerRightCorner.Y, 0, 0, 0, 1, useColor[1], tcoords.UpperLeftCorner.X, tcoords.LowerRightCorner.Y); - drawVertexPrimitiveList2d3d(vertices, 4, indices, 2, video::EVT_STANDARD, scene::EPT_TRIANGLE_FAN, EIT_16BIT, false); - - if (clipRect) - glDisable(GL_SCISSOR_TEST); -} - -void COGLES1Driver::draw2DImage(const video::ITexture *texture, u32 layer, bool flip) -{ - if (!texture || !CacheHandler->getTextureCache().set(0, texture)) - return; - - setRenderStates2DMode(false, true, true); - - glMatrixMode(GL_PROJECTION); - glLoadIdentity(); - glMatrixMode(GL_MODELVIEW); - glLoadIdentity(); - - Transformation3DChanged = true; - - u16 indices[] = {0, 1, 2, 3}; - S3DVertex vertices[4]; - - vertices[0].Pos = core::vector3df(-1.f, 1.f, 0.f); - vertices[1].Pos = core::vector3df(1.f, 1.f, 0.f); - vertices[2].Pos = core::vector3df(1.f, -1.f, 0.f); - vertices[3].Pos = core::vector3df(-1.f, -1.f, 0.f); - - f32 modificator = (flip) ? 1.f : 0.f; - - vertices[0].TCoords = core::vector2df(0.f, 0.f + modificator); - vertices[1].TCoords = core::vector2df(1.f, 0.f + modificator); - vertices[2].TCoords = core::vector2df(1.f, 1.f - modificator); - vertices[3].TCoords = core::vector2df(0.f, 1.f - modificator); - - vertices[0].Color = 0xFFFFFFFF; - vertices[1].Color = 0xFFFFFFFF; - vertices[2].Color = 0xFFFFFFFF; - vertices[3].Color = 0xFFFFFFFF; - - drawVertexPrimitiveList2d3d(vertices, 4, indices, 2, video::EVT_STANDARD, scene::EPT_TRIANGLE_FAN, EIT_16BIT, false); -} - -//! draws a set of 2d images, using a color and the alpha channel of the texture if desired. -void COGLES1Driver::draw2DImageBatch(const video::ITexture *texture, - const core::array> &positions, - const core::array> &sourceRects, - const core::rect *clipRect, - SColor color, - bool useAlphaChannelOfTexture) -{ - if (!texture) - return; - - const u32 drawCount = core::min_(positions.size(), sourceRects.size()); - if (!drawCount) - return; - - const core::dimension2d &ss = texture->getOriginalSize(); - if (!ss.Width || !ss.Height) - return; - const f32 invW = 1.f / static_cast(ss.Width); - const f32 invH = 1.f / static_cast(ss.Height); - const core::dimension2d &renderTargetSize = getCurrentRenderTargetSize(); - - if (!CacheHandler->getTextureCache().set(0, texture)) - return; - - setRenderStates2DMode(color.getAlpha() < 255, true, useAlphaChannelOfTexture); - - core::array vertices; - core::array quadIndices; - vertices.reallocate(drawCount * 4); - quadIndices.reallocate(drawCount * 6); - - for (u32 i = 0; i < drawCount; ++i) { - if (!sourceRects[i].isValid()) - continue; - - core::position2d targetPos(positions[i]); - core::position2d sourcePos(sourceRects[i].UpperLeftCorner); - // This needs to be signed as it may go negative. - core::dimension2d sourceSize(sourceRects[i].getSize()); - if (clipRect) { - if (targetPos.X < clipRect->UpperLeftCorner.X) { - sourceSize.Width += targetPos.X - clipRect->UpperLeftCorner.X; - if (sourceSize.Width <= 0) - continue; - - sourcePos.X -= targetPos.X - clipRect->UpperLeftCorner.X; - targetPos.X = clipRect->UpperLeftCorner.X; - } - - if (targetPos.X + sourceSize.Width > clipRect->LowerRightCorner.X) { - sourceSize.Width -= (targetPos.X + sourceSize.Width) - clipRect->LowerRightCorner.X; - if (sourceSize.Width <= 0) - continue; - } - - if (targetPos.Y < clipRect->UpperLeftCorner.Y) { - sourceSize.Height += targetPos.Y - clipRect->UpperLeftCorner.Y; - if (sourceSize.Height <= 0) - continue; - - sourcePos.Y -= targetPos.Y - clipRect->UpperLeftCorner.Y; - targetPos.Y = clipRect->UpperLeftCorner.Y; - } - - if (targetPos.Y + sourceSize.Height > clipRect->LowerRightCorner.Y) { - sourceSize.Height -= (targetPos.Y + sourceSize.Height) - clipRect->LowerRightCorner.Y; - if (sourceSize.Height <= 0) - continue; - } - } - - // clip these coordinates - - if (targetPos.X < 0) { - sourceSize.Width += targetPos.X; - if (sourceSize.Width <= 0) - continue; - - sourcePos.X -= targetPos.X; - targetPos.X = 0; - } - - if (targetPos.X + sourceSize.Width > (s32)renderTargetSize.Width) { - sourceSize.Width -= (targetPos.X + sourceSize.Width) - renderTargetSize.Width; - if (sourceSize.Width <= 0) - continue; - } - - if (targetPos.Y < 0) { - sourceSize.Height += targetPos.Y; - if (sourceSize.Height <= 0) - continue; - - sourcePos.Y -= targetPos.Y; - targetPos.Y = 0; - } - - if (targetPos.Y + sourceSize.Height > (s32)renderTargetSize.Height) { - sourceSize.Height -= (targetPos.Y + sourceSize.Height) - renderTargetSize.Height; - if (sourceSize.Height <= 0) - continue; - } - - // ok, we've clipped everything. - - const core::rect tcoords( - sourcePos.X * invW, - sourcePos.Y * invH, - (sourcePos.X + sourceSize.Width) * invW, - (sourcePos.Y + sourceSize.Height) * invH); - - const core::rect poss(targetPos, sourceSize); - - const u32 vstart = vertices.size(); - - vertices.push_back(S3DVertex((f32)poss.UpperLeftCorner.X, (f32)poss.UpperLeftCorner.Y, 0, 0, 0, 1, color, tcoords.UpperLeftCorner.X, tcoords.UpperLeftCorner.Y)); - vertices.push_back(S3DVertex((f32)poss.LowerRightCorner.X, (f32)poss.UpperLeftCorner.Y, 0, 0, 0, 1, color, tcoords.LowerRightCorner.X, tcoords.UpperLeftCorner.Y)); - vertices.push_back(S3DVertex((f32)poss.LowerRightCorner.X, (f32)poss.LowerRightCorner.Y, 0, 0, 0, 1, color, tcoords.LowerRightCorner.X, tcoords.LowerRightCorner.Y)); - vertices.push_back(S3DVertex((f32)poss.UpperLeftCorner.X, (f32)poss.LowerRightCorner.Y, 0, 0, 0, 1, color, tcoords.UpperLeftCorner.X, tcoords.LowerRightCorner.Y)); - - quadIndices.push_back(vstart); - quadIndices.push_back(vstart + 1); - quadIndices.push_back(vstart + 2); - quadIndices.push_back(vstart); - quadIndices.push_back(vstart + 2); - quadIndices.push_back(vstart + 3); - } - if (vertices.size()) - drawVertexPrimitiveList2d3d(vertices.pointer(), vertices.size(), - quadIndices.pointer(), vertices.size() / 2, - video::EVT_STANDARD, scene::EPT_TRIANGLES, - EIT_16BIT, false); -} - -//! draw a 2d rectangle -void COGLES1Driver::draw2DRectangle(SColor color, const core::rect &position, - const core::rect *clip) -{ - setRenderStates2DMode(color.getAlpha() < 255, false, false); - - core::rect pos = position; - - if (clip) - pos.clipAgainst(*clip); - - if (!pos.isValid()) - return; - - u16 indices[] = {0, 1, 2, 3}; - S3DVertex vertices[4]; - vertices[0] = S3DVertex((f32)pos.UpperLeftCorner.X, (f32)pos.UpperLeftCorner.Y, 0, 0, 0, 1, color, 0, 0); - vertices[1] = S3DVertex((f32)pos.LowerRightCorner.X, (f32)pos.UpperLeftCorner.Y, 0, 0, 0, 1, color, 0, 0); - vertices[2] = S3DVertex((f32)pos.LowerRightCorner.X, (f32)pos.LowerRightCorner.Y, 0, 0, 0, 1, color, 0, 0); - vertices[3] = S3DVertex((f32)pos.UpperLeftCorner.X, (f32)pos.LowerRightCorner.Y, 0, 0, 0, 1, color, 0, 0); - drawVertexPrimitiveList2d3d(vertices, 4, indices, 2, video::EVT_STANDARD, scene::EPT_TRIANGLE_FAN, EIT_16BIT, false); -} - -//! draw an 2d rectangle -void COGLES1Driver::draw2DRectangle(const core::rect &position, - SColor colorLeftUp, SColor colorRightUp, SColor colorLeftDown, SColor colorRightDown, - const core::rect *clip) -{ - core::rect pos = position; - - if (clip) - pos.clipAgainst(*clip); - - if (!pos.isValid()) - return; - - setRenderStates2DMode(colorLeftUp.getAlpha() < 255 || - colorRightUp.getAlpha() < 255 || - colorLeftDown.getAlpha() < 255 || - colorRightDown.getAlpha() < 255, - false, false); - - u16 indices[] = {0, 1, 2, 3}; - S3DVertex vertices[4]; - vertices[0] = S3DVertex((f32)pos.UpperLeftCorner.X, (f32)pos.UpperLeftCorner.Y, 0, 0, 0, 1, colorLeftUp, 0, 0); - vertices[1] = S3DVertex((f32)pos.LowerRightCorner.X, (f32)pos.UpperLeftCorner.Y, 0, 0, 0, 1, colorRightUp, 0, 0); - vertices[2] = S3DVertex((f32)pos.LowerRightCorner.X, (f32)pos.LowerRightCorner.Y, 0, 0, 0, 1, colorRightDown, 0, 0); - vertices[3] = S3DVertex((f32)pos.UpperLeftCorner.X, (f32)pos.LowerRightCorner.Y, 0, 0, 0, 1, colorLeftDown, 0, 0); - drawVertexPrimitiveList2d3d(vertices, 4, indices, 2, video::EVT_STANDARD, scene::EPT_TRIANGLE_FAN, EIT_16BIT, false); -} - -//! Draws a 2d line. -void COGLES1Driver::draw2DLine(const core::position2d &start, - const core::position2d &end, - SColor color) -{ - setRenderStates2DMode(color.getAlpha() < 255, false, false); - - u16 indices[] = {0, 1}; - S3DVertex vertices[2]; - vertices[0] = S3DVertex((f32)start.X, (f32)start.Y, 0, 0, 0, 1, color, 0, 0); - vertices[1] = S3DVertex((f32)end.X, (f32)end.Y, 0, 0, 0, 1, color, 1, 1); - drawVertexPrimitiveList2d3d(vertices, 2, indices, 1, video::EVT_STANDARD, scene::EPT_LINES, EIT_16BIT, false); -} - -//! creates a matrix in supplied GLfloat array to pass to OGLES1 -inline void COGLES1Driver::getGLMatrix(GLfloat gl_matrix[16], const core::matrix4 &m) -{ - memcpy(gl_matrix, m.pointer(), 16 * sizeof(f32)); -} - -//! creates a opengltexturematrix from a D3D style texture matrix -inline void COGLES1Driver::getGLTextureMatrix(GLfloat *o, const core::matrix4 &m) -{ - o[0] = m[0]; - o[1] = m[1]; - o[2] = 0.f; - o[3] = 0.f; - - o[4] = m[4]; - o[5] = m[5]; - o[6] = 0.f; - o[7] = 0.f; - - o[8] = 0.f; - o[9] = 0.f; - o[10] = 1.f; - o[11] = 0.f; - - o[12] = m[8]; - o[13] = m[9]; - o[14] = 0.f; - o[15] = 1.f; -} - -ITexture *COGLES1Driver::createDeviceDependentTexture(const io::path &name, IImage *image) -{ - core::array imageArray(1); - imageArray.push_back(image); - - COGLES1Texture *texture = new COGLES1Texture(name, imageArray, ETT_2D, this); - - return texture; -} - -ITexture *COGLES1Driver::createDeviceDependentTextureCubemap(const io::path &name, const core::array &image) -{ - COGLES1Texture *texture = new COGLES1Texture(name, image, ETT_CUBEMAP, this); - - return texture; -} - -//! Sets a material. All 3d drawing functions draw geometry now using this material. -void COGLES1Driver::setMaterial(const SMaterial &material) -{ - Material = material; - OverrideMaterial.apply(Material); - - for (u32 i = 0; i < Feature.MaxTextureUnits; ++i) - setTransform((E_TRANSFORMATION_STATE)(ETS_TEXTURE_0 + i), material.getTextureMatrix(i)); -} - -//! prints error if an error happened. -bool COGLES1Driver::testGLError(int code) -{ - if (!Params.DriverDebug) - return false; - - GLenum g = glGetError(); - switch (g) { - case GL_NO_ERROR: - return false; - case GL_INVALID_ENUM: - os::Printer::log("GL_INVALID_ENUM", core::stringc(code).c_str(), ELL_ERROR); - break; - case GL_INVALID_VALUE: - os::Printer::log("GL_INVALID_VALUE", core::stringc(code).c_str(), ELL_ERROR); - break; - case GL_INVALID_OPERATION: - os::Printer::log("GL_INVALID_OPERATION", core::stringc(code).c_str(), ELL_ERROR); - break; - case GL_STACK_OVERFLOW: - os::Printer::log("GL_STACK_OVERFLOW", core::stringc(code).c_str(), ELL_ERROR); - break; - case GL_STACK_UNDERFLOW: - os::Printer::log("GL_STACK_UNDERFLOW", core::stringc(code).c_str(), ELL_ERROR); - break; - case GL_OUT_OF_MEMORY: - os::Printer::log("GL_OUT_OF_MEMORY", core::stringc(code).c_str(), ELL_ERROR); - break; - }; - return true; -} - -//! sets the needed renderstates -void COGLES1Driver::setRenderStates3DMode() -{ - if (CurrentRenderMode != ERM_3D) { - // Reset Texture Stages - CacheHandler->setBlend(false); - glDisable(GL_ALPHA_TEST); - CacheHandler->setBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - // switch back the matrices - glMatrixMode(GL_MODELVIEW); - glLoadMatrixf((Matrices[ETS_VIEW] * Matrices[ETS_WORLD]).pointer()); - - GLfloat glmat[16]; - getGLMatrix(glmat, Matrices[ETS_PROJECTION]); - glmat[12] *= -1.0f; - glMatrixMode(GL_PROJECTION); - glLoadMatrixf(glmat); - - ResetRenderStates = true; - } - - if (ResetRenderStates || LastMaterial != Material) { - // unset old material - - if (LastMaterial.MaterialType != Material.MaterialType && - static_cast(LastMaterial.MaterialType) < MaterialRenderers.size()) - MaterialRenderers[LastMaterial.MaterialType].Renderer->OnUnsetMaterial(); - - // set new material. - if (static_cast(Material.MaterialType) < MaterialRenderers.size()) - MaterialRenderers[Material.MaterialType].Renderer->OnSetMaterial( - Material, LastMaterial, ResetRenderStates, this); - - LastMaterial = Material; - CacheHandler->correctCacheMaterial(LastMaterial); - ResetRenderStates = false; - } - - if (static_cast(Material.MaterialType) < MaterialRenderers.size()) - MaterialRenderers[Material.MaterialType].Renderer->OnRender(this, video::EVT_STANDARD); - - CurrentRenderMode = ERM_3D; -} - -GLint COGLES1Driver::getTextureWrapMode(u8 clamp) const -{ - switch (clamp) { - case ETC_CLAMP: - // return GL_CLAMP; not supported in ogl-es - return GL_CLAMP_TO_EDGE; - break; - case ETC_CLAMP_TO_EDGE: - return GL_CLAMP_TO_EDGE; - break; - case ETC_CLAMP_TO_BORDER: - // return GL_CLAMP_TO_BORDER; not supported in ogl-es - return GL_CLAMP_TO_EDGE; - break; - case ETC_MIRROR: -#ifdef GL_OES_texture_mirrored_repeat - if (FeatureAvailable[COGLESCoreExtensionHandler::IRR_GL_OES_texture_mirrored_repeat]) - return GL_MIRRORED_REPEAT_OES; - else -#endif - return GL_REPEAT; - break; - // the next three are not yet supported at all - case ETC_MIRROR_CLAMP: - case ETC_MIRROR_CLAMP_TO_EDGE: - case ETC_MIRROR_CLAMP_TO_BORDER: -#ifdef GL_OES_texture_mirrored_repeat - if (FeatureAvailable[COGLESCoreExtensionHandler::IRR_GL_OES_texture_mirrored_repeat]) - return GL_MIRRORED_REPEAT_OES; - else -#endif - return GL_CLAMP_TO_EDGE; - break; - case ETC_REPEAT: - default: - return GL_REPEAT; - break; - } -} - -//! Can be called by an IMaterialRenderer to make its work easier. -void COGLES1Driver::setBasicRenderStates(const SMaterial &material, const SMaterial &lastmaterial, - bool resetAllRenderStates) -{ - if (resetAllRenderStates || - lastmaterial.ColorMaterial != material.ColorMaterial) { - // we only have diffuse_and_ambient in ogl-es - if (material.ColorMaterial == ECM_DIFFUSE_AND_AMBIENT) - glEnable(GL_COLOR_MATERIAL); - else - glDisable(GL_COLOR_MATERIAL); - } - - if (resetAllRenderStates || - lastmaterial.AmbientColor != material.AmbientColor || - lastmaterial.DiffuseColor != material.DiffuseColor || - lastmaterial.EmissiveColor != material.EmissiveColor || - lastmaterial.ColorMaterial != material.ColorMaterial) { - GLfloat color[4]; - - const f32 inv = 1.0f / 255.0f; - - if ((material.ColorMaterial != video::ECM_AMBIENT) && - (material.ColorMaterial != video::ECM_DIFFUSE_AND_AMBIENT)) { - color[0] = material.AmbientColor.getRed() * inv; - color[1] = material.AmbientColor.getGreen() * inv; - color[2] = material.AmbientColor.getBlue() * inv; - color[3] = material.AmbientColor.getAlpha() * inv; - glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, color); - } - - if ((material.ColorMaterial != video::ECM_DIFFUSE) && - (material.ColorMaterial != video::ECM_DIFFUSE_AND_AMBIENT)) { - color[0] = material.DiffuseColor.getRed() * inv; - color[1] = material.DiffuseColor.getGreen() * inv; - color[2] = material.DiffuseColor.getBlue() * inv; - color[3] = material.DiffuseColor.getAlpha() * inv; - glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, color); - } - - if (material.ColorMaterial != video::ECM_EMISSIVE) { - color[0] = material.EmissiveColor.getRed() * inv; - color[1] = material.EmissiveColor.getGreen() * inv; - color[2] = material.EmissiveColor.getBlue() * inv; - color[3] = material.EmissiveColor.getAlpha() * inv; - glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, color); - } - } - - if (resetAllRenderStates || - lastmaterial.SpecularColor != material.SpecularColor || - lastmaterial.Shininess != material.Shininess) { - GLfloat color[] = {0.f, 0.f, 0.f, 1.f}; - const f32 inv = 1.0f / 255.0f; - - // disable Specular colors if no shininess is set - if ((material.Shininess != 0.0f) && - (material.ColorMaterial != video::ECM_SPECULAR)) { -#ifdef GL_EXT_separate_specular_color - if (FeatureAvailable[IRR_EXT_separate_specular_color]) - glLightModeli(GL_LIGHT_MODEL_COLOR_CONTROL, GL_SEPARATE_SPECULAR_COLOR); -#endif - glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, material.Shininess); - color[0] = material.SpecularColor.getRed() * inv; - color[1] = material.SpecularColor.getGreen() * inv; - color[2] = material.SpecularColor.getBlue() * inv; - color[3] = material.SpecularColor.getAlpha() * inv; - glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, color); - } -#ifdef GL_EXT_separate_specular_color - else if (FeatureAvailable[IRR_EXT_separate_specular_color]) - glLightModeli(GL_LIGHT_MODEL_COLOR_CONTROL, GL_SINGLE_COLOR); -#endif - } - - // TODO ogl-es - // fillmode - // if (resetAllRenderStates || (lastmaterial.Wireframe != material.Wireframe) || (lastmaterial.PointCloud != material.PointCloud)) - // glPolygonMode(GL_FRONT_AND_BACK, material.Wireframe ? GL_LINE : material.PointCloud? GL_POINT : GL_FILL); - - // shademode - if (resetAllRenderStates || (lastmaterial.GouraudShading != material.GouraudShading)) { - if (material.GouraudShading) - glShadeModel(GL_SMOOTH); - else - glShadeModel(GL_FLAT); - } - - // lighting - if (resetAllRenderStates || (lastmaterial.Lighting != material.Lighting)) { - if (material.Lighting) - glEnable(GL_LIGHTING); - else - glDisable(GL_LIGHTING); - } - - // zbuffer - if (resetAllRenderStates || lastmaterial.ZBuffer != material.ZBuffer) { - switch (material.ZBuffer) { - case ECFN_DISABLED: - glDisable(GL_DEPTH_TEST); - break; - case ECFN_LESSEQUAL: - glEnable(GL_DEPTH_TEST); - glDepthFunc(GL_LEQUAL); - break; - case ECFN_EQUAL: - glEnable(GL_DEPTH_TEST); - glDepthFunc(GL_EQUAL); - break; - case ECFN_LESS: - glEnable(GL_DEPTH_TEST); - glDepthFunc(GL_LESS); - break; - case ECFN_NOTEQUAL: - glEnable(GL_DEPTH_TEST); - glDepthFunc(GL_NOTEQUAL); - break; - case ECFN_GREATEREQUAL: - glEnable(GL_DEPTH_TEST); - glDepthFunc(GL_GEQUAL); - break; - case ECFN_GREATER: - glEnable(GL_DEPTH_TEST); - glDepthFunc(GL_GREATER); - break; - case ECFN_ALWAYS: - glEnable(GL_DEPTH_TEST); - glDepthFunc(GL_ALWAYS); - break; - case ECFN_NEVER: - glEnable(GL_DEPTH_TEST); - glDepthFunc(GL_NEVER); - break; - } - } - - // zwrite - if (getWriteZBuffer(material)) { - glDepthMask(GL_TRUE); - } else { - glDepthMask(GL_FALSE); - } - - // back face culling - if (resetAllRenderStates || (lastmaterial.FrontfaceCulling != material.FrontfaceCulling) || (lastmaterial.BackfaceCulling != material.BackfaceCulling)) { - if ((material.FrontfaceCulling) && (material.BackfaceCulling)) { - glCullFace(GL_FRONT_AND_BACK); - glEnable(GL_CULL_FACE); - } else if (material.BackfaceCulling) { - glCullFace(GL_BACK); - glEnable(GL_CULL_FACE); - } else if (material.FrontfaceCulling) { - glCullFace(GL_FRONT); - glEnable(GL_CULL_FACE); - } else - glDisable(GL_CULL_FACE); - } - - // fog - if (resetAllRenderStates || lastmaterial.FogEnable != material.FogEnable) { - if (material.FogEnable) - glEnable(GL_FOG); - else - glDisable(GL_FOG); - } - - // normalization - if (resetAllRenderStates || lastmaterial.NormalizeNormals != material.NormalizeNormals) { - if (material.NormalizeNormals) - glEnable(GL_NORMALIZE); - else - glDisable(GL_NORMALIZE); - } - - // Color Mask - if (resetAllRenderStates || lastmaterial.ColorMask != material.ColorMask) { - glColorMask( - (material.ColorMask & ECP_RED) ? GL_TRUE : GL_FALSE, - (material.ColorMask & ECP_GREEN) ? GL_TRUE : GL_FALSE, - (material.ColorMask & ECP_BLUE) ? GL_TRUE : GL_FALSE, - (material.ColorMask & ECP_ALPHA) ? GL_TRUE : GL_FALSE); - } - - // Blend Equation - if (material.BlendOperation == EBO_NONE) - CacheHandler->setBlend(false); - else { - CacheHandler->setBlend(true); - - if (queryFeature(EVDF_BLEND_OPERATIONS)) { - switch (material.BlendOperation) { - case EBO_ADD: -#if defined(GL_OES_blend_subtract) - CacheHandler->setBlendEquation(GL_FUNC_ADD_OES); -#endif - break; - case EBO_SUBTRACT: -#if defined(GL_OES_blend_subtract) - CacheHandler->setBlendEquation(GL_FUNC_SUBTRACT_OES); -#endif - break; - case EBO_REVSUBTRACT: -#if defined(GL_OES_blend_subtract) - CacheHandler->setBlendEquation(GL_FUNC_REVERSE_SUBTRACT_OES); -#endif - break; - default: - break; - } - } - } - - // Blend Factor - if (IR(material.BlendFactor) & 0xFFFFFFFF // TODO: why the & 0xFFFFFFFF? - && material.MaterialType != EMT_ONETEXTURE_BLEND) { - E_BLEND_FACTOR srcRGBFact = EBF_ZERO; - E_BLEND_FACTOR dstRGBFact = EBF_ZERO; - E_BLEND_FACTOR srcAlphaFact = EBF_ZERO; - E_BLEND_FACTOR dstAlphaFact = EBF_ZERO; - E_MODULATE_FUNC modulo = EMFN_MODULATE_1X; - u32 alphaSource = 0; - - unpack_textureBlendFuncSeparate(srcRGBFact, dstRGBFact, srcAlphaFact, dstAlphaFact, modulo, alphaSource, material.BlendFactor); - - if (queryFeature(EVDF_BLEND_SEPARATE)) { - CacheHandler->setBlendFuncSeparate(getGLBlend(srcRGBFact), getGLBlend(dstRGBFact), - getGLBlend(srcAlphaFact), getGLBlend(dstAlphaFact)); - } else { - CacheHandler->setBlendFunc(getGLBlend(srcRGBFact), getGLBlend(dstRGBFact)); - } - } - - // TODO: Polygon Offset. Not sure if it was left out deliberately or if it won't work with this driver. - - // thickness - if (resetAllRenderStates || lastmaterial.Thickness != material.Thickness) { - if (AntiAlias) { - // glPointSize(core::clamp(static_cast(material.Thickness), DimSmoothedPoint[0], DimSmoothedPoint[1])); - // we don't use point smoothing - glPointSize(core::clamp(static_cast(material.Thickness), DimAliasedPoint[0], DimAliasedPoint[1])); - } else { - glPointSize(core::clamp(static_cast(material.Thickness), DimAliasedPoint[0], DimAliasedPoint[1])); - glLineWidth(core::clamp(static_cast(material.Thickness), DimAliasedLine[0], DimAliasedLine[1])); - } - } - - // Anti aliasing - if (resetAllRenderStates || lastmaterial.AntiAliasing != material.AntiAliasing) { - if (material.AntiAliasing & EAAM_ALPHA_TO_COVERAGE) - glEnable(GL_SAMPLE_ALPHA_TO_COVERAGE); - else if (lastmaterial.AntiAliasing & EAAM_ALPHA_TO_COVERAGE) - glDisable(GL_SAMPLE_ALPHA_TO_COVERAGE); - - if ((AntiAlias >= 2) && (material.AntiAliasing & (EAAM_SIMPLE | EAAM_QUALITY))) - glEnable(GL_MULTISAMPLE); - else - glDisable(GL_MULTISAMPLE); - } - - // Texture parameters - setTextureRenderStates(material, resetAllRenderStates); -} - -//! Compare in SMaterial doesn't check texture parameters, so we should call this on each OnRender call. -void COGLES1Driver::setTextureRenderStates(const SMaterial &material, bool resetAllRenderstates) -{ - // Set textures to TU/TIU and apply filters to them - - for (s32 i = Feature.MaxTextureUnits - 1; i >= 0; --i) { - CacheHandler->getTextureCache().set(i, material.TextureLayers[i].Texture); - - const COGLES1Texture *tmpTexture = CacheHandler->getTextureCache().get(i); - - if (!tmpTexture) - continue; - - GLenum tmpTextureType = tmpTexture->getOpenGLTextureType(); - - CacheHandler->setActiveTexture(GL_TEXTURE0 + i); - - { - const bool isRTT = tmpTexture->isRenderTarget(); - - glMatrixMode(GL_TEXTURE); - - if (!isRTT && Matrices[ETS_TEXTURE_0 + i].isIdentity()) - glLoadIdentity(); - else { - GLfloat glmat[16]; - if (isRTT) - getGLTextureMatrix(glmat, Matrices[ETS_TEXTURE_0 + i] * TextureFlipMatrix); - else - getGLTextureMatrix(glmat, Matrices[ETS_TEXTURE_0 + i]); - glLoadMatrixf(glmat); - } - } - - COGLES1Texture::SStatesCache &statesCache = tmpTexture->getStatesCache(); - - if (resetAllRenderstates) - statesCache.IsCached = false; - -#if defined(GL_EXT_texture_lod_bias) - if (FeatureAvailable[COGLESCoreExtensionHandler::IRR_GL_EXT_texture_lod_bias]) { - if (material.TextureLayers[i].LODBias) { - const float tmp = core::clamp(material.TextureLayers[i].LODBias * 0.125f, -MaxTextureLODBias, MaxTextureLODBias); - glTexEnvf(GL_TEXTURE_FILTER_CONTROL_EXT, GL_TEXTURE_LOD_BIAS_EXT, tmp); - } else - glTexEnvf(GL_TEXTURE_FILTER_CONTROL_EXT, GL_TEXTURE_LOD_BIAS_EXT, 0.f); - } -#endif - - if (!statesCache.IsCached || material.TextureLayers[i].MagFilter != statesCache.MagFilter) { - E_TEXTURE_MAG_FILTER magFilter = material.TextureLayers[i].MagFilter; - glTexParameteri(tmpTextureType, GL_TEXTURE_MAG_FILTER, - magFilter == ETMAGF_NEAREST ? GL_NEAREST : (assert(magFilter == ETMAGF_LINEAR), GL_LINEAR)); - - statesCache.MagFilter = magFilter; - } - - if (material.UseMipMaps && tmpTexture->hasMipMaps()) { - if (!statesCache.IsCached || material.TextureLayers[i].MinFilter != statesCache.MinFilter || - !statesCache.MipMapStatus) { - E_TEXTURE_MIN_FILTER minFilter = material.TextureLayers[i].MinFilter; - glTexParameteri(tmpTextureType, GL_TEXTURE_MIN_FILTER, - minFilter == ETMINF_NEAREST_MIPMAP_NEAREST ? GL_NEAREST_MIPMAP_NEAREST : minFilter == ETMINF_LINEAR_MIPMAP_NEAREST ? GL_LINEAR_MIPMAP_NEAREST - : minFilter == ETMINF_NEAREST_MIPMAP_LINEAR ? GL_NEAREST_MIPMAP_LINEAR - : (assert(minFilter == ETMINF_LINEAR_MIPMAP_LINEAR), GL_LINEAR_MIPMAP_LINEAR)); - - statesCache.MinFilter = minFilter; - statesCache.MipMapStatus = true; - } - } else { - if (!statesCache.IsCached || material.TextureLayers[i].MinFilter != statesCache.MinFilter || - statesCache.MipMapStatus) { - E_TEXTURE_MIN_FILTER minFilter = material.TextureLayers[i].MinFilter; - glTexParameteri(tmpTextureType, GL_TEXTURE_MIN_FILTER, - (minFilter == ETMINF_NEAREST_MIPMAP_NEAREST || minFilter == ETMINF_NEAREST_MIPMAP_LINEAR) ? GL_NEAREST : (assert(minFilter == ETMINF_LINEAR_MIPMAP_NEAREST || minFilter == ETMINF_LINEAR_MIPMAP_LINEAR), GL_LINEAR)); - - statesCache.MinFilter = minFilter; - statesCache.MipMapStatus = false; - } - } - -#ifdef GL_EXT_texture_filter_anisotropic - if (FeatureAvailable[COGLESCoreExtensionHandler::IRR_GL_EXT_texture_filter_anisotropic] && - (!statesCache.IsCached || material.TextureLayers[i].AnisotropicFilter != statesCache.AnisotropicFilter)) { - glTexParameteri(tmpTextureType, GL_TEXTURE_MAX_ANISOTROPY_EXT, - material.TextureLayers[i].AnisotropicFilter > 1 ? core::min_(MaxAnisotropy, material.TextureLayers[i].AnisotropicFilter) : 1); - - statesCache.AnisotropicFilter = material.TextureLayers[i].AnisotropicFilter; - } -#endif - - if (!statesCache.IsCached || material.TextureLayers[i].TextureWrapU != statesCache.WrapU) { - glTexParameteri(tmpTextureType, GL_TEXTURE_WRAP_S, getTextureWrapMode(material.TextureLayers[i].TextureWrapU)); - statesCache.WrapU = material.TextureLayers[i].TextureWrapU; - } - - if (!statesCache.IsCached || material.TextureLayers[i].TextureWrapV != statesCache.WrapV) { - glTexParameteri(tmpTextureType, GL_TEXTURE_WRAP_T, getTextureWrapMode(material.TextureLayers[i].TextureWrapV)); - statesCache.WrapV = material.TextureLayers[i].TextureWrapV; - } - - statesCache.IsCached = true; - } - - // be sure to leave in texture stage 0 - CacheHandler->setActiveTexture(GL_TEXTURE0); -} - -//! sets the needed renderstates -void COGLES1Driver::setRenderStates2DMode(bool alpha, bool texture, bool alphaChannel) -{ - if (CurrentRenderMode != ERM_2D || Transformation3DChanged) { - // unset last 3d material - if (CurrentRenderMode == ERM_3D) { - if (static_cast(LastMaterial.MaterialType) < MaterialRenderers.size()) - MaterialRenderers[LastMaterial.MaterialType].Renderer->OnUnsetMaterial(); - } - if (Transformation3DChanged) { - glMatrixMode(GL_PROJECTION); - - const core::dimension2d &renderTargetSize = getCurrentRenderTargetSize(); - core::matrix4 m(core::matrix4::EM4CONST_NOTHING); - m.buildProjectionMatrixOrthoLH(f32(renderTargetSize.Width), f32(-(s32)(renderTargetSize.Height)), -1.0f, 1.0f); - m.setTranslation(core::vector3df(-1, 1, 0)); - glLoadMatrixf(m.pointer()); - - glMatrixMode(GL_MODELVIEW); - glLoadIdentity(); - - Transformation3DChanged = false; - } - } - - Material = (OverrideMaterial2DEnabled) ? OverrideMaterial2D : InitMaterial2D; - Material.Lighting = false; - Material.TextureLayers[0].Texture = (texture) ? const_cast(CacheHandler->getTextureCache().get(0)) : 0; - setTransform(ETS_TEXTURE_0, core::IdentityMatrix); - - setBasicRenderStates(Material, LastMaterial, false); - - LastMaterial = Material; - CacheHandler->correctCacheMaterial(LastMaterial); - - // no alphaChannel without texture - alphaChannel &= texture; - - if (alphaChannel || alpha) { - CacheHandler->setBlend(true); - CacheHandler->setBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - CacheHandler->setBlendEquation(GL_FUNC_ADD); - glEnable(GL_ALPHA_TEST); - glAlphaFunc(GL_GREATER, 0.f); - } else { - CacheHandler->setBlend(false); - glDisable(GL_ALPHA_TEST); - } - - if (texture) { - // Due to the transformation change, the previous line would call a reset each frame - // but we can safely reset the variable as it was false before - Transformation3DChanged = false; - - if (alphaChannel) { - // if alpha and alpha texture just modulate, otherwise use only the alpha channel - if (alpha) { - glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); - } else { - glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); - glTexEnvf(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE); - glTexEnvf(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_TEXTURE); - // rgb always modulates - glTexEnvf(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE); - glTexEnvf(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_TEXTURE); - glTexEnvf(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PRIMARY_COLOR); - } - } else { - if (alpha) { - glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); - glTexEnvf(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE); - glTexEnvf(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_PRIMARY_COLOR); - // rgb always modulates - glTexEnvf(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE); - glTexEnvf(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_TEXTURE); - glTexEnvf(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PRIMARY_COLOR); - } else { - glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); - } - } - } - - CurrentRenderMode = ERM_2D; -} - -//! \return Returns the name of the video driver. -const char *COGLES1Driver::getName() const -{ - return Name.c_str(); -} - -//! Sets the dynamic ambient light color. -void COGLES1Driver::setAmbientLight(const SColorf &color) -{ - CNullDriver::setAmbientLight(color); - GLfloat data[4] = {color.r, color.g, color.b, color.a}; - glLightModelfv(GL_LIGHT_MODEL_AMBIENT, data); -} - -// this code was sent in by Oliver Klems, thank you -void COGLES1Driver::setViewPort(const core::rect &area) -{ - core::rect vp = area; - core::rect rendert(0, 0, getCurrentRenderTargetSize().Width, getCurrentRenderTargetSize().Height); - vp.clipAgainst(rendert); - - if (vp.getHeight() > 0 && vp.getWidth() > 0) - CacheHandler->setViewport(vp.UpperLeftCorner.X, getCurrentRenderTargetSize().Height - vp.UpperLeftCorner.Y - vp.getHeight(), vp.getWidth(), vp.getHeight()); - - ViewPort = vp; -} - -void COGLES1Driver::setViewPortRaw(u32 width, u32 height) -{ - CacheHandler->setViewport(0, 0, width, height); - ViewPort = core::recti(0, 0, width, height); -} - -//! Sets the fog mode. -void COGLES1Driver::setFog(SColor c, E_FOG_TYPE fogType, f32 start, - f32 end, f32 density, bool pixelFog, bool rangeFog) -{ - CNullDriver::setFog(c, fogType, start, end, density, pixelFog, rangeFog); - - glFogf(GL_FOG_MODE, GLfloat((fogType == EFT_FOG_LINEAR) ? GL_LINEAR : (fogType == EFT_FOG_EXP) ? GL_EXP - : GL_EXP2)); - -#ifdef GL_EXT_fog_coord - if (FeatureAvailable[IRR_EXT_fog_coord]) - glFogi(GL_FOG_COORDINATE_SOURCE, GL_FRAGMENT_DEPTH); -#endif - - if (fogType == EFT_FOG_LINEAR) { - glFogf(GL_FOG_START, start); - glFogf(GL_FOG_END, end); - } else - glFogf(GL_FOG_DENSITY, density); - - if (pixelFog) - glHint(GL_FOG_HINT, GL_NICEST); - else - glHint(GL_FOG_HINT, GL_FASTEST); - - SColorf color(c); - GLfloat data[4] = {color.r, color.g, color.b, color.a}; - glFogfv(GL_FOG_COLOR, data); -} - -//! Draws a 3d line. -void COGLES1Driver::draw3DLine(const core::vector3df &start, - const core::vector3df &end, SColor color) -{ - setRenderStates3DMode(); - - u16 indices[] = {0, 1}; - S3DVertex vertices[2]; - vertices[0] = S3DVertex(start.X, start.Y, start.Z, 0, 0, 1, color, 0, 0); - vertices[1] = S3DVertex(end.X, end.Y, end.Z, 0, 0, 1, color, 0, 0); - drawVertexPrimitiveList2d3d(vertices, 2, indices, 1, video::EVT_STANDARD, scene::EPT_LINES); -} - -//! Only used by the internal engine. Used to notify the driver that -//! the window was resized. -void COGLES1Driver::OnResize(const core::dimension2d &size) -{ - CNullDriver::OnResize(size); - CacheHandler->setViewport(0, 0, size.Width, size.Height); - Transformation3DChanged = true; -} - -//! Returns type of video driver -E_DRIVER_TYPE COGLES1Driver::getDriverType() const -{ - return EDT_OGLES1; -} - -//! returns color format -ECOLOR_FORMAT COGLES1Driver::getColorFormat() const -{ - return ColorFormat; -} - -//! Get a vertex shader constant index. -s32 COGLES1Driver::getVertexShaderConstantID(const c8 *name) -{ - return getPixelShaderConstantID(name); -} - -//! Get a pixel shader constant index. -s32 COGLES1Driver::getPixelShaderConstantID(const c8 *name) -{ - os::Printer::log("Error: Please use IMaterialRendererServices from IShaderConstantSetCallBack::OnSetConstants not VideoDriver->getPixelShaderConstantID()."); - return -1; -} - -//! Sets a constant for the vertex shader based on an index. -bool COGLES1Driver::setVertexShaderConstant(s32 index, const f32 *floats, int count) -{ - // pass this along, as in GLSL the same routine is used for both vertex and fragment shaders - return setPixelShaderConstant(index, floats, count); -} - -//! Int interface for the above. -bool COGLES1Driver::setVertexShaderConstant(s32 index, const s32 *ints, int count) -{ - return setPixelShaderConstant(index, ints, count); -} - -bool COGLES1Driver::setVertexShaderConstant(s32 index, const u32 *ints, int count) -{ - return setPixelShaderConstant(index, ints, count); -} - -//! Sets a constant for the pixel shader based on an index. -bool COGLES1Driver::setPixelShaderConstant(s32 index, const f32 *floats, int count) -{ - os::Printer::log("Error: Please use IMaterialRendererServices from IShaderConstantSetCallBack::OnSetConstants not VideoDriver->setPixelShaderConstant()."); - return false; -} - -//! Int interface for the above. -bool COGLES1Driver::setPixelShaderConstant(s32 index, const s32 *ints, int count) -{ - os::Printer::log("Error: Please use IMaterialRendererServices from IShaderConstantSetCallBack::OnSetConstants not VideoDriver->setPixelShaderConstant()."); - return false; -} - -bool COGLES1Driver::setPixelShaderConstant(s32 index, const u32 *ints, int count) -{ - os::Printer::log("Error: Please use IMaterialRendererServices from IShaderConstantSetCallBack::OnSetConstants not VideoDriver->setPixelShaderConstant()."); - return false; -} - -//! Adds a new material renderer to the VideoDriver, using GLSL to render geometry. -s32 COGLES1Driver::addHighLevelShaderMaterial( - const c8 *vertexShaderProgram, - const c8 *vertexShaderEntryPointName, - E_VERTEX_SHADER_TYPE vsCompileTarget, - const c8 *pixelShaderProgram, - const c8 *pixelShaderEntryPointName, - E_PIXEL_SHADER_TYPE psCompileTarget, - const c8 *geometryShaderProgram, - const c8 *geometryShaderEntryPointName, - E_GEOMETRY_SHADER_TYPE gsCompileTarget, - scene::E_PRIMITIVE_TYPE inType, - scene::E_PRIMITIVE_TYPE outType, - u32 verticesOut, - IShaderConstantSetCallBack *callback, - E_MATERIAL_TYPE baseMaterial, - s32 userData) -{ - os::Printer::log("No shader support."); - return -1; -} - -//! Returns a pointer to the IVideoDriver interface. (Implementation for -//! IMaterialRendererServices) -IVideoDriver *COGLES1Driver::getVideoDriver() -{ - return this; -} - -//! Returns pointer to the IGPUProgrammingServices interface. -IGPUProgrammingServices *COGLES1Driver::getGPUProgrammingServices() -{ - return this; -} - -ITexture *COGLES1Driver::addRenderTargetTexture(const core::dimension2d &size, - const io::path &name, const ECOLOR_FORMAT format) -{ - // disable mip-mapping - bool generateMipLevels = getTextureCreationFlag(ETCF_CREATE_MIP_MAPS); - setTextureCreationFlag(ETCF_CREATE_MIP_MAPS, false); - - bool supportForFBO = (Feature.ColorAttachment > 0); - - core::dimension2du destSize(size); - - if (!supportForFBO) { - destSize = core::dimension2d(core::min_(size.Width, ScreenSize.Width), core::min_(size.Height, ScreenSize.Height)); - destSize = destSize.getOptimalSize((size == size.getOptimalSize()), false, false); - } - - COGLES1Texture *renderTargetTexture = new COGLES1Texture(name, destSize, ETT_2D, format, this); - addTexture(renderTargetTexture); - renderTargetTexture->drop(); - - // restore mip-mapping - setTextureCreationFlag(ETCF_CREATE_MIP_MAPS, generateMipLevels); - - return renderTargetTexture; -} - -ITexture *COGLES1Driver::addRenderTargetTextureCubemap(const irr::u32 sideLen, const io::path &name, const ECOLOR_FORMAT format) -{ - // disable mip-mapping - bool generateMipLevels = getTextureCreationFlag(ETCF_CREATE_MIP_MAPS); - setTextureCreationFlag(ETCF_CREATE_MIP_MAPS, false); - - bool supportForFBO = (Feature.ColorAttachment > 0); - - const core::dimension2d size(sideLen, sideLen); - core::dimension2du destSize(size); - - if (!supportForFBO) { - destSize = core::dimension2d(core::min_(size.Width, ScreenSize.Width), core::min_(size.Height, ScreenSize.Height)); - destSize = destSize.getOptimalSize((size == size.getOptimalSize()), false, false); - } - - COGLES1Texture *renderTargetTexture = new COGLES1Texture(name, destSize, ETT_CUBEMAP, format, this); - addTexture(renderTargetTexture); - renderTargetTexture->drop(); - - // restore mip-mapping - setTextureCreationFlag(ETCF_CREATE_MIP_MAPS, generateMipLevels); - - return renderTargetTexture; -} - -//! Returns the maximum amount of primitives -u32 COGLES1Driver::getMaximalPrimitiveCount() const -{ - return 65535; -} - -bool COGLES1Driver::setRenderTargetEx(IRenderTarget *target, u16 clearFlag, SColor clearColor, f32 clearDepth, u8 clearStencil) -{ - if (target && target->getDriverType() != EDT_OGLES1) { - os::Printer::log("Fatal Error: Tried to set a render target not owned by OpenGL driver.", ELL_ERROR); - return false; - } - - bool supportForFBO = (Feature.ColorAttachment > 0); - - core::dimension2d destRenderTargetSize(0, 0); - - if (target) { - COGLES1RenderTarget *renderTarget = static_cast(target); - - if (supportForFBO) { - CacheHandler->setFBO(renderTarget->getBufferID()); - renderTarget->update(); - } - - destRenderTargetSize = renderTarget->getSize(); - - setViewPortRaw(destRenderTargetSize.Width, destRenderTargetSize.Height); - } else { - if (supportForFBO) - CacheHandler->setFBO(0); - else { - COGLES1RenderTarget *prevRenderTarget = static_cast(CurrentRenderTarget); - COGLES1Texture *renderTargetTexture = static_cast(prevRenderTarget->getTexture()); - - if (renderTargetTexture) { - const COGLES1Texture *prevTexture = CacheHandler->getTextureCache().get(0); - - CacheHandler->getTextureCache().set(0, renderTargetTexture); - - const core::dimension2d size = renderTargetTexture->getSize(); - glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, size.Width, size.Height); - - CacheHandler->getTextureCache().set(0, prevTexture); - } - } - - destRenderTargetSize = core::dimension2d(0, 0); - - setViewPortRaw(ScreenSize.Width, ScreenSize.Height); - } - - if (CurrentRenderTargetSize != destRenderTargetSize) { - CurrentRenderTargetSize = destRenderTargetSize; - - Transformation3DChanged = true; - } - - CurrentRenderTarget = target; - - if (!supportForFBO) { - clearFlag |= ECBF_COLOR; - clearFlag |= ECBF_DEPTH; - } - - clearBuffers(clearFlag, clearColor, clearDepth, clearStencil); - - return true; -} - -void COGLES1Driver::clearBuffers(u16 flag, SColor color, f32 depth, u8 stencil) -{ - GLbitfield mask = 0; - - if (flag & ECBF_COLOR) { - glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); - - const f32 inv = 1.0f / 255.0f; - glClearColor(color.getRed() * inv, color.getGreen() * inv, - color.getBlue() * inv, color.getAlpha() * inv); - - mask |= GL_COLOR_BUFFER_BIT; - } - - if (flag & ECBF_DEPTH) { - glDepthMask(GL_TRUE); - glClearDepthf(depth); - mask |= GL_DEPTH_BUFFER_BIT; - } - - if (flag & ECBF_STENCIL) { - glClearStencil(stencil); - mask |= GL_STENCIL_BUFFER_BIT; - } - - if (mask) - glClear(mask); -} - -//! Returns an image created from the last rendered frame. -// We want to read the front buffer to get the latest render finished. -// This is not possible under ogl-es, though, so one has to call this method -// outside of the render loop only. -IImage *COGLES1Driver::createScreenShot(video::ECOLOR_FORMAT format, video::E_RENDER_TARGET target) -{ - if (target == video::ERT_MULTI_RENDER_TEXTURES || target == video::ERT_RENDER_TEXTURE || target == video::ERT_STEREO_BOTH_BUFFERS) - return 0; - GLint internalformat = GL_RGBA; - GLint type = GL_UNSIGNED_BYTE; - if (false && (FeatureAvailable[COGLESCoreExtensionHandler::IRR_GL_IMG_read_format] || FeatureAvailable[COGLESCoreExtensionHandler::IRR_GL_OES_read_format] || FeatureAvailable[COGLESCoreExtensionHandler::IRR_GL_EXT_read_format_bgra])) { -#ifdef GL_IMPLEMENTATION_COLOR_READ_TYPE_OES - glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT_OES, &internalformat); - glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE_OES, &type); -#endif - // there are formats we don't support ATM - if (GL_UNSIGNED_SHORT_4_4_4_4 == type) - type = GL_UNSIGNED_SHORT_5_5_5_1; -#ifdef GL_EXT_read_format_bgra - else if (GL_UNSIGNED_SHORT_4_4_4_4_REV_EXT == type) - type = GL_UNSIGNED_SHORT_1_5_5_5_REV_EXT; -#endif - } - - IImage *newImage = 0; - if ((GL_RGBA == internalformat) -#ifdef GL_EXT_read_format_bgra - || (GL_BGRA_EXT == internalformat) -#endif - ) { - if (GL_UNSIGNED_BYTE == type) - newImage = new CImage(ECF_A8R8G8B8, ScreenSize); - else - newImage = new CImage(ECF_A1R5G5B5, ScreenSize); - } else { - if (GL_UNSIGNED_BYTE == type) - newImage = new CImage(ECF_R8G8B8, ScreenSize); - else - newImage = new CImage(ECF_R5G6B5, ScreenSize); - } - - u8 *pixels = static_cast(newImage->getData()); - if (!pixels) { - newImage->drop(); - return 0; - } - - glReadPixels(0, 0, ScreenSize.Width, ScreenSize.Height, internalformat, type, pixels); - - // opengl images are horizontally flipped, so we have to fix that here. - const s32 pitch = newImage->getPitch(); - u8 *p2 = pixels + (ScreenSize.Height - 1) * pitch; - u8 *tmpBuffer = new u8[pitch]; - for (u32 i = 0; i < ScreenSize.Height; i += 2) { - memcpy(tmpBuffer, pixels, pitch); - memcpy(pixels, p2, pitch); - memcpy(p2, tmpBuffer, pitch); - pixels += pitch; - p2 -= pitch; - } - delete[] tmpBuffer; - - if (testGLError(__LINE__)) { - newImage->drop(); - return 0; - } - - return newImage; -} - -void COGLES1Driver::removeTexture(ITexture *texture) -{ - CacheHandler->getTextureCache().remove(texture); - CNullDriver::removeTexture(texture); -} - -//! Set/unset a clipping plane. -bool COGLES1Driver::setClipPlane(u32 index, const core::plane3df &plane, bool enable) -{ - if (index >= MaxUserClipPlanes) - return false; - - UserClipPlane[index] = plane; - enableClipPlane(index, enable); - return true; -} - -void COGLES1Driver::uploadClipPlane(u32 index) -{ - // opengl needs an array of doubles for the plane equation - float clip_plane[4]; - clip_plane[0] = UserClipPlane[index].Normal.X; - clip_plane[1] = UserClipPlane[index].Normal.Y; - clip_plane[2] = UserClipPlane[index].Normal.Z; - clip_plane[3] = UserClipPlane[index].D; - glClipPlanef(GL_CLIP_PLANE0 + index, clip_plane); -} - -//! Enable/disable a clipping plane. -void COGLES1Driver::enableClipPlane(u32 index, bool enable) -{ - if (index >= MaxUserClipPlanes) - return; - if (enable) { - if (!UserClipPlaneEnabled[index]) { - uploadClipPlane(index); - glEnable(GL_CLIP_PLANE0 + index); - } - } else - glDisable(GL_CLIP_PLANE0 + index); - - UserClipPlaneEnabled[index] = enable; -} - -core::dimension2du COGLES1Driver::getMaxTextureSize() const -{ - return core::dimension2du(MaxTextureSize, MaxTextureSize); -} - -GLenum COGLES1Driver::getGLBlend(E_BLEND_FACTOR factor) const -{ - static GLenum const blendTable[] = { - GL_ZERO, - GL_ONE, - GL_DST_COLOR, - GL_ONE_MINUS_DST_COLOR, - GL_SRC_COLOR, - GL_ONE_MINUS_SRC_COLOR, - GL_SRC_ALPHA, - GL_ONE_MINUS_SRC_ALPHA, - GL_DST_ALPHA, - GL_ONE_MINUS_DST_ALPHA, - GL_SRC_ALPHA_SATURATE, - }; - - return blendTable[factor]; -} - -GLenum COGLES1Driver::getZBufferBits() const -{ - GLenum bits = 0; - - switch (Params.ZBufferBits) { - case 24: -#if defined(GL_OES_depth24) - if (queryGLESFeature(COGLESCoreExtensionHandler::IRR_GL_OES_depth24)) - bits = GL_DEPTH_COMPONENT24_OES; - else -#endif - bits = GL_DEPTH_COMPONENT16; - break; - case 32: -#if defined(GL_OES_depth32) - if (queryGLESFeature(COGLESCoreExtensionHandler::IRR_GL_OES_depth32)) - bits = GL_DEPTH_COMPONENT32_OES; - else -#endif - bits = GL_DEPTH_COMPONENT16; - break; - default: - bits = GL_DEPTH_COMPONENT16; - break; - } - - return bits; -} - -bool COGLES1Driver::getColorFormatParameters(ECOLOR_FORMAT format, GLint &internalFormat, GLenum &pixelFormat, - GLenum &pixelType, void (**converter)(const void *, s32, void *)) const -{ - bool supported = false; - internalFormat = GL_RGBA; - pixelFormat = GL_RGBA; - pixelType = GL_UNSIGNED_BYTE; - *converter = 0; - - switch (format) { - case ECF_A1R5G5B5: - supported = true; - internalFormat = GL_RGBA; - pixelFormat = GL_RGBA; - pixelType = GL_UNSIGNED_SHORT_5_5_5_1; - *converter = CColorConverter::convert_A1R5G5B5toR5G5B5A1; - break; - case ECF_R5G6B5: - supported = true; - internalFormat = GL_RGB; - pixelFormat = GL_RGB; - pixelType = GL_UNSIGNED_SHORT_5_6_5; - break; - case ECF_R8G8B8: - supported = true; - internalFormat = GL_RGB; - pixelFormat = GL_RGB; - pixelType = GL_UNSIGNED_BYTE; - break; - case ECF_A8R8G8B8: - supported = true; - if (queryGLESFeature(COGLESCoreExtensionHandler::IRR_GL_IMG_texture_format_BGRA8888) || - queryGLESFeature(COGLESCoreExtensionHandler::IRR_GL_EXT_texture_format_BGRA8888) || - queryGLESFeature(COGLESCoreExtensionHandler::IRR_GL_APPLE_texture_format_BGRA8888)) { - internalFormat = GL_BGRA; - pixelFormat = GL_BGRA; - } else { - internalFormat = GL_RGBA; - pixelFormat = GL_RGBA; - *converter = CColorConverter::convert_A8R8G8B8toA8B8G8R8; - } - pixelType = GL_UNSIGNED_BYTE; - break; - case ECF_D16: - supported = true; - internalFormat = GL_DEPTH_COMPONENT16; - pixelFormat = GL_DEPTH_COMPONENT; - pixelType = GL_UNSIGNED_SHORT; - break; - case ECF_D32: -#if defined(GL_OES_depth32) - if (queryGLESFeature(COGLESCoreExtensionHandler::IRR_GL_OES_depth32)) { - supported = true; - internalFormat = GL_DEPTH_COMPONENT32_OES; - pixelFormat = GL_DEPTH_COMPONENT; - pixelType = GL_UNSIGNED_INT; - } -#endif - break; - case ECF_D24S8: -#ifdef GL_OES_packed_depth_stencil - if (queryGLESFeature(COGLESCoreExtensionHandler::IRR_GL_OES_packed_depth_stencil)) { - supported = true; - internalFormat = GL_DEPTH24_STENCIL8_OES; - pixelFormat = GL_DEPTH_STENCIL_OES; - pixelType = GL_UNSIGNED_INT_24_8_OES; - } -#endif - break; - case ECF_R8: - break; - case ECF_R8G8: - break; - case ECF_R16: - break; - case ECF_R16G16: - break; - case ECF_R16F: - break; - case ECF_G16R16F: - break; - case ECF_A16B16G16R16F: - break; - case ECF_R32F: - break; - case ECF_G32R32F: - break; - case ECF_A32B32G32R32F: - break; - default: - break; - } - -#ifdef _IRR_IOS_PLATFORM_ - if (internalFormat == GL_BGRA) - internalFormat = GL_RGBA; -#endif - - return supported; -} - -bool COGLES1Driver::queryTextureFormat(ECOLOR_FORMAT format) const -{ - GLint dummyInternalFormat; - GLenum dummyPixelFormat; - GLenum dummyPixelType; - void (*dummyConverter)(const void *, s32, void *); - return getColorFormatParameters(format, dummyInternalFormat, dummyPixelFormat, dummyPixelType, &dummyConverter); -} - -bool COGLES1Driver::needsTransparentRenderPass(const irr::video::SMaterial &material) const -{ - return CNullDriver::needsTransparentRenderPass(material) || material.isAlphaBlendOperation(); -} - -COGLES1CacheHandler *COGLES1Driver::getCacheHandler() const -{ - return CacheHandler; -} - -} // end namespace -} // end namespace - -#endif // _IRR_COMPILE_WITH_OGLES1_ - -namespace irr -{ -namespace video -{ - -#ifndef _IRR_COMPILE_WITH_OGLES1_ -class IVideoDriver; -class IContextManager; -#endif - -IVideoDriver *createOGLES1Driver(const SIrrlichtCreationParameters ¶ms, io::IFileSystem *io, IContextManager *contextManager) -{ -#ifdef _IRR_COMPILE_WITH_OGLES1_ - return new COGLES1Driver(params, io, contextManager); -#else - return 0; -#endif // _IRR_COMPILE_WITH_OGLES1_ -} - -} // end namespace -} // end namespace diff --git a/irr/src/COGLESDriver.h b/irr/src/COGLESDriver.h deleted file mode 100644 index 29e0d94f1..000000000 --- a/irr/src/COGLESDriver.h +++ /dev/null @@ -1,327 +0,0 @@ -// Copyright (C) 2002-20014 Nikolaus Gebhardt -// This file is part of the "Irrlicht Engine". -// For conditions of distribution and use, see copyright notice in Irrlicht.h - -#pragma once - -#include "SIrrCreationParameters.h" - -#ifdef _IRR_COMPILE_WITH_OGLES1_ - -#include "CNullDriver.h" -#include "IMaterialRendererServices.h" -#include "EDriverFeatures.h" -#include "fast_atof.h" -#include "COGLESExtensionHandler.h" -#include "IContextManager.h" - -#define TEST_GL_ERROR(cls) (cls)->testGLError(__LINE__) - -namespace irr -{ -namespace video -{ - -class COGLES1Driver : public CNullDriver, public IMaterialRendererServices, public COGLES1ExtensionHandler -{ - friend class COpenGLCoreTexture; - -public: - //! constructor - COGLES1Driver(const SIrrlichtCreationParameters ¶ms, io::IFileSystem *io, IContextManager *contextManager); - - //! destructor - virtual ~COGLES1Driver(); - - virtual bool beginScene(u16 clearFlag, SColor clearColor = SColor(255, 0, 0, 0), f32 clearDepth = 1.f, u8 clearStencil = 0, - const SExposedVideoData &videoData = SExposedVideoData(), core::rect *sourceRect = 0) override; - - bool endScene() override; - - //! sets transformation - void setTransform(E_TRANSFORMATION_STATE state, const core::matrix4 &mat) override; - - struct SHWBufferLink_opengl : public SHWBufferLink - { - SHWBufferLink_opengl(const scene::IMeshBuffer *_MeshBuffer) : - SHWBufferLink(_MeshBuffer), vbo_verticesID(0), vbo_indicesID(0) {} - - GLuint vbo_verticesID; // tmp - GLuint vbo_indicesID; // tmp - - GLuint vbo_verticesSize; // tmp - GLuint vbo_indicesSize; // tmp - }; - - bool updateVertexHardwareBuffer(SHWBufferLink_opengl *HWBuffer); - bool updateIndexHardwareBuffer(SHWBufferLink_opengl *HWBuffer); - - //! updates hardware buffer if needed - bool updateHardwareBuffer(SHWBufferLink *HWBuffer) override; - - //! Create hardware buffer from mesh - SHWBufferLink *createHardwareBuffer(const scene::IMeshBuffer *mb) override; - - //! Delete hardware buffer (only some drivers can) - void deleteHardwareBuffer(SHWBufferLink *HWBuffer) override; - - //! Draw hardware buffer - void drawHardwareBuffer(SHWBufferLink *HWBuffer) override; - - IRenderTarget *addRenderTarget() override; - - //! draws a vertex primitive list - virtual void drawVertexPrimitiveList(const void *vertices, u32 vertexCount, - const void *indexList, u32 primitiveCount, - E_VERTEX_TYPE vType, scene::E_PRIMITIVE_TYPE pType, E_INDEX_TYPE iType) override; - - void drawVertexPrimitiveList2d3d(const void *vertices, u32 vertexCount, const void *indexList, u32 primitiveCount, E_VERTEX_TYPE vType, scene::E_PRIMITIVE_TYPE pType, E_INDEX_TYPE iType = EIT_16BIT, bool threed = true); - - //! queries the features of the driver, returns true if feature is available - bool queryFeature(E_VIDEO_DRIVER_FEATURE feature) const override - { - // return FeatureEnabled[feature] && COGLES1ExtensionHandler::queryFeature(feature); - return COGLES1ExtensionHandler::queryFeature(feature); - } - - //! Sets a material. - void setMaterial(const SMaterial &material) override; - - virtual void draw2DImage(const video::ITexture *texture, const core::position2d &destPos, - const core::rect &sourceRect, const core::rect *clipRect = 0, - SColor color = SColor(255, 255, 255, 255), bool useAlphaChannelOfTexture = false) override; - - virtual void draw2DImage(const video::ITexture *texture, const core::rect &destRect, - const core::rect &sourceRect, const core::rect *clipRect = 0, - const video::SColor *const colors = 0, bool useAlphaChannelOfTexture = false) override; - - virtual void draw2DImage(const video::ITexture *texture, u32 layer, bool flip); - - //! draws a set of 2d images, using a color and the alpha channel of the texture if desired. - virtual void draw2DImageBatch(const video::ITexture *texture, - const core::array> &positions, - const core::array> &sourceRects, - const core::rect *clipRect = 0, - SColor color = SColor(255, 255, 255, 255), - bool useAlphaChannelOfTexture = false) override; - - //! draw an 2d rectangle - virtual void draw2DRectangle(SColor color, const core::rect &pos, - const core::rect *clip = 0) override; - - //! Draws an 2d rectangle with a gradient. - virtual void draw2DRectangle(const core::rect &pos, - SColor colorLeftUp, SColor colorRightUp, SColor colorLeftDown, SColor colorRightDown, - const core::rect *clip = 0) override; - - //! Draws a 2d line. - virtual void draw2DLine(const core::position2d &start, - const core::position2d &end, - SColor color = SColor(255, 255, 255, 255)) override; - - //! Draws a 3d line. - virtual void draw3DLine(const core::vector3df &start, - const core::vector3df &end, - SColor color = SColor(255, 255, 255, 255)) override; - - //! Returns the name of the video driver. - const char *getName() const override; - - //! Sets the dynamic ambient light color. - void setAmbientLight(const SColorf &color) override; - - //! sets a viewport - void setViewPort(const core::rect &area) override; - - //! Sets the fog mode. - virtual void setFog(SColor color, E_FOG_TYPE fogType, f32 start, - f32 end, f32 density, bool pixelFog, bool rangeFog) override; - - //! Only used internally by the engine - void OnResize(const core::dimension2d &size) override; - - //! Returns type of video driver - E_DRIVER_TYPE getDriverType() const override; - - //! get color format of the current color buffer - ECOLOR_FORMAT getColorFormat() const override; - - //! Returns the transformation set by setTransform - const core::matrix4 &getTransform(E_TRANSFORMATION_STATE state) const override; - - //! Can be called by an IMaterialRenderer to make its work easier. - virtual void setBasicRenderStates(const SMaterial &material, const SMaterial &lastmaterial, - bool resetAllRenderstates) override; - - //! Compare in SMaterial doesn't check texture parameters, so we should call this on each OnRender call. - virtual void setTextureRenderStates(const SMaterial &material, bool resetAllRenderstates); - - //! Get a vertex shader constant index. - s32 getVertexShaderConstantID(const c8 *name) override; - - //! Get a pixel shader constant index. - s32 getPixelShaderConstantID(const c8 *name) override; - - //! Sets a constant for the vertex shader based on an index. - bool setVertexShaderConstant(s32 index, const f32 *floats, int count) override; - - //! Int interface for the above. - bool setVertexShaderConstant(s32 index, const s32 *ints, int count) override; - - //! Uint interface for the above. - bool setVertexShaderConstant(s32 index, const u32 *ints, int count) override; - - //! Sets a constant for the pixel shader based on an index. - bool setPixelShaderConstant(s32 index, const f32 *floats, int count) override; - - //! Int interface for the above. - bool setPixelShaderConstant(s32 index, const s32 *ints, int count) override; - - //! Uint interface for the above. - bool setPixelShaderConstant(s32 index, const u32 *ints, int count) override; - - //! Adds a new material renderer to the VideoDriver - virtual s32 addHighLevelShaderMaterial(const c8 *vertexShaderProgram, const c8 *vertexShaderEntryPointName, - E_VERTEX_SHADER_TYPE vsCompileTarget, const c8 *pixelShaderProgram, const c8 *pixelShaderEntryPointName, - E_PIXEL_SHADER_TYPE psCompileTarget, const c8 *geometryShaderProgram, const c8 *geometryShaderEntryPointName, - E_GEOMETRY_SHADER_TYPE gsCompileTarget, scene::E_PRIMITIVE_TYPE inType, scene::E_PRIMITIVE_TYPE outType, - u32 verticesOut, IShaderConstantSetCallBack *callback, E_MATERIAL_TYPE baseMaterial, - s32 userData) override; - - //! Returns pointer to the IGPUProgrammingServices interface. - IGPUProgrammingServices *getGPUProgrammingServices() override; - - //! Returns a pointer to the IVideoDriver interface. - IVideoDriver *getVideoDriver() override; - - //! Returns the maximum amount of primitives - u32 getMaximalPrimitiveCount() const override; - - virtual ITexture *addRenderTargetTexture(const core::dimension2d &size, - const io::path &name, const ECOLOR_FORMAT format = ECF_UNKNOWN) override; - - //! Creates a render target texture for a cubemap - ITexture *addRenderTargetTextureCubemap(const irr::u32 sideLen, - const io::path &name, const ECOLOR_FORMAT format) override; - - virtual bool setRenderTargetEx(IRenderTarget *target, u16 clearFlag, SColor clearColor = SColor(255, 0, 0, 0), - f32 clearDepth = 1.f, u8 clearStencil = 0) override; - - void clearBuffers(u16 flag, SColor color = SColor(255, 0, 0, 0), f32 depth = 1.f, u8 stencil = 0) override; - - //! Returns an image created from the last rendered frame. - IImage *createScreenShot(video::ECOLOR_FORMAT format = video::ECF_UNKNOWN, video::E_RENDER_TARGET target = video::ERT_FRAME_BUFFER) override; - - //! checks if an OpenGL error has happened and prints it (+ some internal code which is usually the line number) - bool testGLError(int code = 0); - - //! Set/unset a clipping plane. - bool setClipPlane(u32 index, const core::plane3df &plane, bool enable = false) override; - - //! Enable/disable a clipping plane. - void enableClipPlane(u32 index, bool enable) override; - - //! Returns the graphics card vendor name. - core::stringc getVendorInfo() override - { - return VendorName; - } - - //! Get the maximal texture size for this driver - core::dimension2du getMaxTextureSize() const override; - - void removeTexture(ITexture *texture) override; - - //! Check if the driver supports creating textures with the given color format - bool queryTextureFormat(ECOLOR_FORMAT format) const override; - - //! Used by some SceneNodes to check if a material should be rendered in the transparent render pass - bool needsTransparentRenderPass(const irr::video::SMaterial &material) const override; - - //! Convert E_BLEND_FACTOR to OpenGL equivalent - GLenum getGLBlend(E_BLEND_FACTOR factor) const; - - //! Get ZBuffer bits. - GLenum getZBufferBits() const; - - bool getColorFormatParameters(ECOLOR_FORMAT format, GLint &internalFormat, GLenum &pixelFormat, - GLenum &pixelType, void (**converter)(const void *, s32, void *)) const; - - COGLES1CacheHandler *getCacheHandler() const; - -private: - void uploadClipPlane(u32 index); - - //! inits the opengl-es driver - bool genericDriverInit(const core::dimension2d &screenSize, bool stencilBuffer); - - ITexture *createDeviceDependentTexture(const io::path &name, IImage *image) override; - - ITexture *createDeviceDependentTextureCubemap(const io::path &name, const core::array &image) override; - - //! creates a transposed matrix in supplied GLfloat array to pass to OGLES1 - inline void getGLMatrix(GLfloat gl_matrix[16], const core::matrix4 &m); - inline void getGLTextureMatrix(GLfloat gl_matrix[16], const core::matrix4 &m); - - //! Set GL pipeline to desired texture wrap modes of the material - void setWrapMode(const SMaterial &material); - - //! Get OpenGL wrap enum from Irrlicht enum - GLint getTextureWrapMode(u8 clamp) const; - - //! sets the needed renderstates - void setRenderStates3DMode(); - - //! sets the needed renderstates - void setRenderStates2DMode(bool alpha, bool texture, bool alphaChannel); - - void createMaterialRenderers(); - - //! Assign a hardware light to the specified requested light, if any - //! free hardware lights exist. - //! \param[in] lightIndex: the index of the requesting light - void assignHardwareLight(u32 lightIndex); - - //! Same as `CacheHandler->setViewport`, but also sets `ViewPort` - virtual void setViewPortRaw(u32 width, u32 height); - - COGLES1CacheHandler *CacheHandler; - - core::stringc Name; - core::matrix4 Matrices[ETS_COUNT]; - core::array ColorBuffer; - - //! enumeration for rendering modes such as 2d and 3d for minimizing the switching of renderStates. - enum E_RENDER_MODE - { - ERM_NONE = 0, // no render state has been set yet. - ERM_2D, // 2d drawing rendermode - ERM_3D // 3d rendering mode - }; - - E_RENDER_MODE CurrentRenderMode; - //! bool to make all renderstates reset if set to true. - bool ResetRenderStates; - bool Transformation3DChanged; - u8 AntiAlias; - - SMaterial Material, LastMaterial; - core::array UserClipPlane; - std::vector UserClipPlaneEnabled; - - core::stringc VendorName; - - core::matrix4 TextureFlipMatrix; - - //! Color buffer format - ECOLOR_FORMAT ColorFormat; - - SIrrlichtCreationParameters Params; - - IContextManager *ContextManager; -}; - -} // end namespace video -} // end namespace irr - -#endif // _IRR_COMPILE_WITH_OGLES1_ diff --git a/irr/src/COGLESExtensionHandler.cpp b/irr/src/COGLESExtensionHandler.cpp deleted file mode 100644 index 82f8e9261..000000000 --- a/irr/src/COGLESExtensionHandler.cpp +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (C) 2008 Christian Stehno -// Heavily based on the OpenGL driver implemented by Nikolaus Gebhardt -// 2017 modified by Michael Zeilfelder (unifying extension handlers) -// This file is part of the "Irrlicht Engine". -// For conditions of distribution and use, see copyright notice in Irrlicht.h - -#include "COGLESExtensionHandler.h" - -#ifdef _IRR_COMPILE_WITH_OGLES1_ - -#include "irrString.h" -#include "SMaterial.h" -#include "fast_atof.h" - -#if defined(_IRR_COMPILE_WITH_WINDOWS_DEVICE_) -#include -#else -#include -#endif - -namespace irr -{ -namespace video -{ - -COGLES1ExtensionHandler::COGLES1ExtensionHandler() : - COGLESCoreExtensionHandler(), - MaxUserClipPlanes(0), MaxLights(0), pGlBlendEquationOES(0), pGlBlendFuncSeparateOES(0), - pGlBindFramebufferOES(0), pGlDeleteFramebuffersOES(0), - pGlGenFramebuffersOES(0), pGlCheckFramebufferStatusOES(0), - pGlFramebufferTexture2DOES(0), pGlGenerateMipmapOES(0) -{ -} - -void COGLES1ExtensionHandler::initExtensions() -{ - getGLVersion(); - - if (Version >= 100) - os::Printer::log("OpenGL ES driver version is 1.1.", ELL_INFORMATION); - else - os::Printer::log("OpenGL ES driver version is 1.0.", ELL_WARNING); - - getGLExtensions(); - - GLint val = 0; - - if (Version > 100 || FeatureAvailable[IRR_GL_IMG_user_clip_plane]) { - glGetIntegerv(GL_MAX_CLIP_PLANES, &val); - MaxUserClipPlanes = static_cast(val); - } - - glGetIntegerv(GL_MAX_LIGHTS, &val); - MaxLights = static_cast(val); - - glGetIntegerv(GL_MAX_TEXTURE_UNITS, &val); - Feature.MaxTextureUnits = static_cast(val); - -#ifdef GL_EXT_texture_filter_anisotropic - if (FeatureAvailable[IRR_GL_EXT_texture_filter_anisotropic]) { - glGetIntegerv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &val); - MaxAnisotropy = static_cast(val); - } -#endif -#ifdef GL_MAX_ELEMENTS_INDICES - glGetIntegerv(GL_MAX_ELEMENTS_INDICES, &val); - MaxIndices = val; -#endif - glGetIntegerv(GL_MAX_TEXTURE_SIZE, &val); - MaxTextureSize = static_cast(val); -#ifdef GL_EXT_texture_lod_bias - if (FeatureAvailable[IRR_GL_EXT_texture_lod_bias]) - glGetFloatv(GL_MAX_TEXTURE_LOD_BIAS_EXT, &MaxTextureLODBias); -#endif - glGetFloatv(GL_ALIASED_LINE_WIDTH_RANGE, DimAliasedLine); - glGetFloatv(GL_ALIASED_POINT_SIZE_RANGE, DimAliasedPoint); - - Feature.MaxTextureUnits = core::min_(Feature.MaxTextureUnits, static_cast(MATERIAL_MAX_TEXTURES)); - Feature.ColorAttachment = 1; - - pGlBlendEquationOES = (PFNGLBLENDEQUATIONOESPROC)eglGetProcAddress("glBlendEquationOES"); - pGlBlendFuncSeparateOES = (PFNGLBLENDFUNCSEPARATEOESPROC)eglGetProcAddress("glBlendFuncSeparateOES"); - pGlBindFramebufferOES = (PFNGLBINDFRAMEBUFFEROESPROC)eglGetProcAddress("glBindFramebufferOES"); - pGlDeleteFramebuffersOES = (PFNGLDELETEFRAMEBUFFERSOESPROC)eglGetProcAddress("glDeleteFramebuffersOES"); - pGlGenFramebuffersOES = (PFNGLGENFRAMEBUFFERSOESPROC)eglGetProcAddress("glGenFramebuffersOES"); - pGlCheckFramebufferStatusOES = (PFNGLCHECKFRAMEBUFFERSTATUSOESPROC)eglGetProcAddress("glCheckFramebufferStatusOES"); - pGlFramebufferTexture2DOES = (PFNGLFRAMEBUFFERTEXTURE2DOESPROC)eglGetProcAddress("glFramebufferTexture2DOES"); - pGlGenerateMipmapOES = (PFNGLGENERATEMIPMAPOESPROC)eglGetProcAddress("glGenerateMipmapOES"); -} - -} // end namespace video -} // end namespace irr - -#endif // _IRR_COMPILE_WITH_OGLES2_ diff --git a/irr/src/COGLESExtensionHandler.h b/irr/src/COGLESExtensionHandler.h deleted file mode 100644 index d9e1ac4bc..000000000 --- a/irr/src/COGLESExtensionHandler.h +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright (C) 2008 Christian Stehno -// Heavily based on the OpenGL driver implemented by Nikolaus Gebhardt -// This file is part of the "Irrlicht Engine". -// For conditions of distribution and use, see copyright notice in Irrlicht.h - -#pragma once - -#ifdef _IRR_COMPILE_WITH_OGLES1_ - -#include "EDriverFeatures.h" -#include "irrTypes.h" -#include "os.h" - -#include "COGLESCommon.h" - -#include "COGLESCoreExtensionHandler.h" - -namespace irr -{ -namespace video -{ - -class COGLES1ExtensionHandler : public COGLESCoreExtensionHandler -{ -public: - COGLES1ExtensionHandler(); - - void initExtensions(); - - bool queryFeature(video::E_VIDEO_DRIVER_FEATURE feature) const - { - switch (feature) { - case EVDF_RENDER_TO_TARGET: - case EVDF_HARDWARE_TL: - case EVDF_MULTITEXTURE: - case EVDF_BILINEAR_FILTER: - case EVDF_MIP_MAP: - case EVDF_TEXTURE_NSQUARE: - case EVDF_STENCIL_BUFFER: - case EVDF_ALPHA_TO_COVERAGE: - case EVDF_COLOR_MASK: - case EVDF_POLYGON_OFFSET: - case EVDF_TEXTURE_MATRIX: - return true; - case EVDF_TEXTURE_NPOT: - return FeatureAvailable[IRR_GL_APPLE_texture_2D_limited_npot] || FeatureAvailable[IRR_GL_OES_texture_npot]; - case EVDF_MIP_MAP_AUTO_UPDATE: - return Version > 100; - case EVDF_BLEND_OPERATIONS: - return FeatureAvailable[IRR_GL_OES_blend_subtract]; - case EVDF_BLEND_SEPARATE: - return FeatureAvailable[IRR_GL_OES_blend_func_separate]; - case EVDF_FRAMEBUFFER_OBJECT: - return FeatureAvailable[IRR_GL_OES_framebuffer_object]; - case EVDF_VERTEX_BUFFER_OBJECT: - return Version > 100; - default: - return true; - }; - } - - inline void irrGlActiveTexture(GLenum texture) - { - glActiveTexture(texture); - } - - inline void irrGlCompressedTexImage2D(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, - GLsizei imageSize, const void *data) - { - glCompressedTexImage2D(target, level, internalformat, width, height, border, imageSize, data); - } - - inline void irrGlCompressedTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, - GLenum format, GLsizei imageSize, const void *data) - { - glCompressedTexSubImage2D(target, level, xoffset, yoffset, width, height, format, imageSize, data); - } - - inline void irrGlUseProgram(GLuint prog) - { - } - - inline void irrGlBindFramebuffer(GLenum target, GLuint framebuffer) - { - if (pGlBindFramebufferOES) - pGlBindFramebufferOES(target, framebuffer); - } - - inline void irrGlDeleteFramebuffers(GLsizei n, const GLuint *framebuffers) - { - if (pGlDeleteFramebuffersOES) - pGlDeleteFramebuffersOES(n, framebuffers); - } - - inline void irrGlGenFramebuffers(GLsizei n, GLuint *framebuffers) - { - if (pGlGenFramebuffersOES) - pGlGenFramebuffersOES(n, framebuffers); - } - - inline GLenum irrGlCheckFramebufferStatus(GLenum target) - { - if (pGlCheckFramebufferStatusOES) - return pGlCheckFramebufferStatusOES(target); - else - return 0; - } - - inline void irrGlFramebufferTexture2D(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level) - { - if (pGlFramebufferTexture2DOES) - pGlFramebufferTexture2DOES(target, attachment, textarget, texture, level); - } - - inline void irrGlGenerateMipmap(GLenum target) - { - if (pGlGenerateMipmapOES) - pGlGenerateMipmapOES(target); - } - - inline void irrGlActiveStencilFace(GLenum face) - { - } - - inline void irrGlDrawBuffer(GLenum mode) - { - } - - inline void irrGlDrawBuffers(GLsizei n, const GLenum *bufs) - { - } - - inline void irrGlBlendFuncSeparate(GLenum srcRGB, GLenum dstRGB, GLenum srcAlpha, GLenum dstAlpha) - { - if (pGlBlendFuncSeparateOES) - pGlBlendFuncSeparateOES(srcRGB, dstRGB, srcAlpha, dstAlpha); - } - - inline void irrGlBlendEquation(GLenum mode) - { - if (pGlBlendEquationOES) - pGlBlendEquationOES(mode); - } - - inline void irrGlEnableIndexed(GLenum target, GLuint index) - { - } - - inline void irrGlDisableIndexed(GLenum target, GLuint index) - { - } - - inline void irrGlColorMaskIndexed(GLuint buf, GLboolean r, GLboolean g, GLboolean b, GLboolean a) - { - } - - inline void irrGlBlendFuncIndexed(GLuint buf, GLenum src, GLenum dst) - { - } - - inline void irrGlBlendFuncSeparateIndexed(GLuint buf, GLenum srcRGB, GLenum dstRGB, GLenum srcAlpha, GLenum dstAlpha) - { - } - - inline void irrGlBlendEquationIndexed(GLuint buf, GLenum mode) - { - } - - inline void irrGlBlendEquationSeparateIndexed(GLuint buf, GLenum modeRGB, GLenum modeAlpha) - { - } - -protected: - u8 MaxUserClipPlanes; - u8 MaxLights; - - PFNGLBLENDEQUATIONOESPROC pGlBlendEquationOES; - PFNGLBLENDFUNCSEPARATEOESPROC pGlBlendFuncSeparateOES; - PFNGLBINDFRAMEBUFFEROESPROC pGlBindFramebufferOES; - PFNGLDELETEFRAMEBUFFERSOESPROC pGlDeleteFramebuffersOES; - PFNGLGENFRAMEBUFFERSOESPROC pGlGenFramebuffersOES; - PFNGLCHECKFRAMEBUFFERSTATUSOESPROC pGlCheckFramebufferStatusOES; - PFNGLFRAMEBUFFERTEXTURE2DOESPROC pGlFramebufferTexture2DOES; - PFNGLGENERATEMIPMAPOESPROC pGlGenerateMipmapOES; -}; - -} -} - -#endif diff --git a/irr/src/COGLESMaterialRenderer.h b/irr/src/COGLESMaterialRenderer.h deleted file mode 100644 index 69d1b81ff..000000000 --- a/irr/src/COGLESMaterialRenderer.h +++ /dev/null @@ -1,286 +0,0 @@ -// Copyright (C) 2002-2008 Nikolaus Gebhardt -// This file is part of the "Irrlicht Engine". -// For conditions of distribution and use, see copyright notice in irrlicht.h - -#pragma once - -#ifdef _IRR_COMPILE_WITH_OGLES1_ - -#include "COGLESDriver.h" -#include "IMaterialRenderer.h" - -namespace irr -{ -namespace video -{ - -//! Base class for all internal OGLES1 material renderers -class COGLES1MaterialRenderer : public IMaterialRenderer -{ -public: - //! Constructor - COGLES1MaterialRenderer(video::COGLES1Driver *driver) : - Driver(driver) - { - } - -protected: - video::COGLES1Driver *Driver; -}; - -//! Solid material renderer -class COGLES1MaterialRenderer_SOLID : public COGLES1MaterialRenderer -{ -public: - COGLES1MaterialRenderer_SOLID(video::COGLES1Driver *d) : - COGLES1MaterialRenderer(d) {} - - virtual void OnSetMaterial(const SMaterial &material, const SMaterial &lastMaterial, - bool resetAllRenderstates, IMaterialRendererServices *services) - { - Driver->setBasicRenderStates(material, lastMaterial, resetAllRenderstates); - - if (resetAllRenderstates || (material.MaterialType != lastMaterial.MaterialType)) { - // thanks to Murphy, the following line removed some - // bugs with several OGLES1 implementations. - glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); - } - } -}; - -//! Generic Texture Blend -class COGLES1MaterialRenderer_ONETEXTURE_BLEND : public COGLES1MaterialRenderer -{ -public: - COGLES1MaterialRenderer_ONETEXTURE_BLEND(video::COGLES1Driver *d) : - COGLES1MaterialRenderer(d) {} - - virtual void OnSetMaterial(const SMaterial &material, const SMaterial &lastMaterial, - bool resetAllRenderstates, IMaterialRendererServices *services) - { - Driver->setBasicRenderStates(material, lastMaterial, resetAllRenderstates); - - // if (material.MaterialType != lastMaterial.MaterialType || - // material.MaterialTypeParam != lastMaterial.MaterialTypeParam || - // resetAllRenderstates) - { - E_BLEND_FACTOR srcRGBFact, dstRGBFact, srcAlphaFact, dstAlphaFact; - E_MODULATE_FUNC modulate; - u32 alphaSource; - unpack_textureBlendFuncSeparate(srcRGBFact, dstRGBFact, srcAlphaFact, dstAlphaFact, modulate, alphaSource, material.MaterialTypeParam); - - Driver->getCacheHandler()->setBlend(true); - - if (Driver->queryFeature(EVDF_BLEND_SEPARATE)) { - Driver->getCacheHandler()->setBlendFuncSeparate(Driver->getGLBlend(srcRGBFact), Driver->getGLBlend(dstRGBFact), - Driver->getGLBlend(srcAlphaFact), Driver->getGLBlend(dstAlphaFact)); - } else { - Driver->getCacheHandler()->setBlendFunc(Driver->getGLBlend(srcRGBFact), Driver->getGLBlend(dstRGBFact)); - } - - glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); - glTexEnvf(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE); - glTexEnvf(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_TEXTURE); - glTexEnvf(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PREVIOUS); - - glTexEnvf(GL_TEXTURE_ENV, GL_RGB_SCALE, (f32)modulate); - - glEnable(GL_ALPHA_TEST); - glAlphaFunc(GL_GREATER, 0.f); - - if (textureBlendFunc_hasAlpha(srcRGBFact) || textureBlendFunc_hasAlpha(dstRGBFact) || - textureBlendFunc_hasAlpha(srcAlphaFact) || textureBlendFunc_hasAlpha(dstAlphaFact)) { - glTexEnvf(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE); - glTexEnvf(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_TEXTURE); - - glTexEnvf(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PRIMARY_COLOR); - } - } - } - - virtual void OnUnsetMaterial() - { - glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); - glTexEnvf(GL_TEXTURE_ENV, GL_RGB_SCALE, 1.f); - glTexEnvf(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PREVIOUS); - - Driver->getCacheHandler()->setBlend(false); - glDisable(GL_ALPHA_TEST); - } - - //! Returns if the material is transparent. - /** Is not always transparent, but mostly. */ - virtual bool isTransparent() const - { - return true; - } - -private: - u32 getGLBlend(E_BLEND_FACTOR factor) const - { - u32 r = 0; - switch (factor) { - case EBF_ZERO: - r = GL_ZERO; - break; - case EBF_ONE: - r = GL_ONE; - break; - case EBF_DST_COLOR: - r = GL_DST_COLOR; - break; - case EBF_ONE_MINUS_DST_COLOR: - r = GL_ONE_MINUS_DST_COLOR; - break; - case EBF_SRC_COLOR: - r = GL_SRC_COLOR; - break; - case EBF_ONE_MINUS_SRC_COLOR: - r = GL_ONE_MINUS_SRC_COLOR; - break; - case EBF_SRC_ALPHA: - r = GL_SRC_ALPHA; - break; - case EBF_ONE_MINUS_SRC_ALPHA: - r = GL_ONE_MINUS_SRC_ALPHA; - break; - case EBF_DST_ALPHA: - r = GL_DST_ALPHA; - break; - case EBF_ONE_MINUS_DST_ALPHA: - r = GL_ONE_MINUS_DST_ALPHA; - break; - case EBF_SRC_ALPHA_SATURATE: - r = GL_SRC_ALPHA_SATURATE; - break; - } - return r; - } -}; - -//! Transparent vertex alpha material renderer -class COGLES1MaterialRenderer_TRANSPARENT_VERTEX_ALPHA : public COGLES1MaterialRenderer -{ -public: - COGLES1MaterialRenderer_TRANSPARENT_VERTEX_ALPHA(video::COGLES1Driver *d) : - COGLES1MaterialRenderer(d) {} - - virtual void OnSetMaterial(const SMaterial &material, const SMaterial &lastMaterial, - bool resetAllRenderstates, IMaterialRendererServices *services) - { - Driver->setBasicRenderStates(material, lastMaterial, resetAllRenderstates); - - Driver->getCacheHandler()->setBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); - Driver->getCacheHandler()->setBlend(true); - - if (material.MaterialType != lastMaterial.MaterialType || resetAllRenderstates) { - glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); - - glTexEnvf(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE); - glTexEnvf(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_PRIMARY_COLOR); - - glTexEnvf(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE); - glTexEnvf(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_PRIMARY_COLOR); - glTexEnvf(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_TEXTURE); - } - } - - virtual void OnUnsetMaterial() - { - // default values - glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); - glTexEnvf(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_MODULATE); - glTexEnvf(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_TEXTURE); - glTexEnvf(GL_TEXTURE_ENV, GL_SRC1_ALPHA, GL_PREVIOUS); - glTexEnvf(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE); - glTexEnvf(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_TEXTURE); - glTexEnvf(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PREVIOUS); - - Driver->getCacheHandler()->setBlend(false); - } - - //! Returns if the material is transparent. - virtual bool isTransparent() const - { - return true; - } -}; - -//! Transparent alpha channel material renderer -class COGLES1MaterialRenderer_TRANSPARENT_ALPHA_CHANNEL : public COGLES1MaterialRenderer -{ -public: - COGLES1MaterialRenderer_TRANSPARENT_ALPHA_CHANNEL(video::COGLES1Driver *d) : - COGLES1MaterialRenderer(d) {} - - virtual void OnSetMaterial(const SMaterial &material, const SMaterial &lastMaterial, - bool resetAllRenderstates, IMaterialRendererServices *services) - { - Driver->setBasicRenderStates(material, lastMaterial, resetAllRenderstates); - - Driver->getCacheHandler()->setBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - Driver->getCacheHandler()->setBlend(true); - - if (material.MaterialType != lastMaterial.MaterialType || resetAllRenderstates || material.MaterialTypeParam != lastMaterial.MaterialTypeParam) { - glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); - glTexEnvf(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE); - glTexEnvf(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_TEXTURE); - glTexEnvf(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PREVIOUS); - - glTexEnvf(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE); - glTexEnvf(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_TEXTURE); - - glEnable(GL_ALPHA_TEST); - - glAlphaFunc(GL_GREATER, material.MaterialTypeParam); - } - } - - virtual void OnUnsetMaterial() - { - glDisable(GL_ALPHA_TEST); - Driver->getCacheHandler()->setBlend(false); - } - - //! Returns if the material is transparent. - virtual bool isTransparent() const - { - return true; - } -}; - -//! Transparent alpha channel material renderer -class COGLES1MaterialRenderer_TRANSPARENT_ALPHA_CHANNEL_REF : public COGLES1MaterialRenderer -{ -public: - COGLES1MaterialRenderer_TRANSPARENT_ALPHA_CHANNEL_REF(video::COGLES1Driver *d) : - COGLES1MaterialRenderer(d) {} - - virtual void OnSetMaterial(const SMaterial &material, const SMaterial &lastMaterial, - bool resetAllRenderstates, IMaterialRendererServices *services) - { - Driver->setBasicRenderStates(material, lastMaterial, resetAllRenderstates); - - if (material.MaterialType != lastMaterial.MaterialType || resetAllRenderstates) { - glEnable(GL_ALPHA_TEST); - glAlphaFunc(GL_GREATER, 0.5f); - glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); - } - } - - virtual void OnUnsetMaterial() - { - glDisable(GL_ALPHA_TEST); - } - - //! Returns if the material is transparent. - virtual bool isTransparent() const - { - return false; // this material is not really transparent because it does no blending. - } -}; - -} // end namespace video -} // end namespace irr - -#endif diff --git a/irr/src/COpenGLCoreCacheHandler.h b/irr/src/COpenGLCoreCacheHandler.h index ae3661313..a1277bfed 100644 --- a/irr/src/COpenGLCoreCacheHandler.h +++ b/irr/src/COpenGLCoreCacheHandler.h @@ -84,12 +84,12 @@ class COpenGLCoreCacheHandler if (curTextureType != prevTextureType) { GL.BindTexture(prevTextureType, 0); -#if (defined(IRR_COMPILE_GL_COMMON) || defined(IRR_COMPILE_GLES_COMMON)) +#if defined(IRR_COMPILE_GL_COMMON) GL.Disable(prevTextureType); GL.Enable(curTextureType); #endif } -#if (defined(IRR_COMPILE_GL_COMMON) || defined(IRR_COMPILE_GLES_COMMON)) +#if defined(IRR_COMPILE_GL_COMMON) else if (!prevTexture) GL.Enable(curTextureType); #endif @@ -109,7 +109,7 @@ class COpenGLCoreCacheHandler GL.BindTexture(prevTextureType, 0); -#if (defined(IRR_COMPILE_GL_COMMON) || defined(IRR_COMPILE_GLES_COMMON)) +#if defined(IRR_COMPILE_GL_COMMON) GL.Disable(prevTextureType); #endif } @@ -222,7 +222,7 @@ public: Driver->irrGlActiveTexture(ActiveTexture); -#if (defined(IRR_COMPILE_GL_COMMON) || defined(IRR_COMPILE_GLES_COMMON)) +#if defined(IRR_COMPILE_GL_COMMON) GL.Disable(GL_TEXTURE_2D); #endif diff --git a/irr/src/COpenGLCoreTexture.h b/irr/src/COpenGLCoreTexture.h index 79a855cc1..95cac4c22 100644 --- a/irr/src/COpenGLCoreTexture.h +++ b/irr/src/COpenGLCoreTexture.h @@ -4,7 +4,7 @@ #pragma once -#include "irrArray.h" +#include #include "SMaterialLayer.h" #include "ITexture.h" #include "EDriverFeatures.h" @@ -43,19 +43,19 @@ public: bool IsCached; }; - COpenGLCoreTexture(const io::path &name, const core::array &images, E_TEXTURE_TYPE type, TOpenGLDriver *driver) : + COpenGLCoreTexture(const io::path &name, const std::vector &srcImages, E_TEXTURE_TYPE type, TOpenGLDriver *driver) : ITexture(name, type), Driver(driver), TextureType(GL_TEXTURE_2D), TextureName(0), InternalFormat(GL_RGBA), PixelFormat(GL_RGBA), PixelType(GL_UNSIGNED_BYTE), Converter(0), LockReadOnly(false), LockImage(0), LockLayer(0), KeepImage(false), MipLevelStored(0), LegacyAutoGenerateMipMaps(false) { - _IRR_DEBUG_BREAK_IF(images.size() == 0) + _IRR_DEBUG_BREAK_IF(srcImages.empty()) DriverType = Driver->getDriverType(); TextureType = TextureTypeIrrToGL(Type); HasMipMaps = Driver->getTextureCreationFlag(ETCF_CREATE_MIP_MAPS); KeepImage = Driver->getTextureCreationFlag(ETCF_ALLOW_MEMORY_COPY); - getImageValues(images[0]); + getImageValues(srcImages[0]); if (!InternalFormat) return; @@ -71,22 +71,22 @@ public: os::Printer::log(lbuf, ELL_DEBUG); #endif - const core::array *tmpImages = &images; + const auto *tmpImages = &srcImages; if (KeepImage || OriginalSize != Size || OriginalColorFormat != ColorFormat) { - Images.set_used(images.size()); + Images.resize(srcImages.size()); - for (u32 i = 0; i < images.size(); ++i) { + for (size_t i = 0; i < srcImages.size(); ++i) { Images[i] = Driver->createImage(ColorFormat, Size); - if (images[i]->getDimension() == Size) - images[i]->copyTo(Images[i]); + if (srcImages[i]->getDimension() == Size) + srcImages[i]->copyTo(Images[i]); else - images[i]->copyToScaling(Images[i]); + srcImages[i]->copyToScaling(Images[i]); - if (images[i]->getMipMapsData()) { + if (srcImages[i]->getMipMapsData()) { if (OriginalSize == Size && OriginalColorFormat == ColorFormat) { - Images[i]->setMipMapsData(images[i]->getMipMapsData(), false); + Images[i]->setMipMapsData(srcImages[i]->getMipMapsData(), false); } else { // TODO: handle at least mipmap with changing color format os::Printer::log("COpenGLCoreTexture: Can't handle format changes for mipmap data. Mipmap data dropped", ELL_WARNING); @@ -118,19 +118,19 @@ public: TEST_GL_ERROR(Driver); - for (u32 i = 0; i < (*tmpImages).size(); ++i) + for (size_t i = 0; i < tmpImages->size(); ++i) uploadTexture(true, i, 0, (*tmpImages)[i]->getData()); if (HasMipMaps && !LegacyAutoGenerateMipMaps) { // Create mipmaps (either from image mipmaps or generate them) - for (u32 i = 0; i < (*tmpImages).size(); ++i) { + for (size_t i = 0; i < tmpImages->size(); ++i) { void *mipmapsData = (*tmpImages)[i]->getMipMapsData(); regenerateMipMapLevels(mipmapsData, i); } } if (!KeepImage) { - for (u32 i = 0; i < Images.size(); ++i) + for (size_t i = 0; i < Images.size(); ++i) Images[i]->drop(); Images.clear(); @@ -227,8 +227,8 @@ public: if (LockImage) LockImage->drop(); - for (u32 i = 0; i < Images.size(); ++i) - Images[i]->drop(); + for (auto *image : Images) + image->drop(); } void *lock(E_TEXTURE_LOCK_MODE mode = ETLM_READ_WRITE, u32 mipmapLevel = 0, u32 layer = 0, E_TEXTURE_LOCK_FLAGS lockFlags = ETLF_FLIP_Y_UP_RTT) override @@ -296,8 +296,7 @@ public: delete[] tmpBuffer; } -#elif (defined(IRR_COMPILE_GLES2_COMMON) || defined(IRR_COMPILE_GLES_COMMON)) - // TODO: on ES2 we can likely also work with glCopyTexImage2D instead of rendering which should be faster. +#elif defined(IRR_COMPILE_GLES2_COMMON) COpenGLCoreTexture *tmpTexture = new COpenGLCoreTexture("OGL_CORE_LOCK_TEXTURE", Size, ETT_2D, ColorFormat, Driver); GLuint tmpFBO = 0; @@ -621,7 +620,7 @@ protected: u32 LockLayer; bool KeepImage; - core::array Images; + std::vector Images; u8 MipLevelStored; bool LegacyAutoGenerateMipMaps; diff --git a/irr/src/COpenGLDriver.cpp b/irr/src/COpenGLDriver.cpp index 1ad66aef9..e5f070f8e 100644 --- a/irr/src/COpenGLDriver.cpp +++ b/irr/src/COpenGLDriver.cpp @@ -122,7 +122,6 @@ bool COpenGLDriver::genericDriverInit() DriverAttributes->setAttribute("MaxSupportedTextures", (s32)Feature.MaxTextureUnits); DriverAttributes->setAttribute("MaxLights", MaxLights); DriverAttributes->setAttribute("MaxAnisotropy", MaxAnisotropy); - DriverAttributes->setAttribute("MaxUserClipPlanes", MaxUserClipPlanes); DriverAttributes->setAttribute("MaxAuxBuffers", MaxAuxBuffers); DriverAttributes->setAttribute("MaxMultipleRenderTargets", (s32)Feature.MultipleRenderTarget); DriverAttributes->setAttribute("MaxIndices", (s32)MaxIndices); @@ -135,10 +134,6 @@ bool COpenGLDriver::genericDriverInit() glPixelStorei(GL_PACK_ALIGNMENT, 1); - UserClipPlanes.reallocate(MaxUserClipPlanes); - for (i = 0; i < MaxUserClipPlanes; ++i) - UserClipPlanes.push_back(SUserClipPlane()); - for (i = 0; i < ETS_COUNT; ++i) setTransform(static_cast(i), core::IdentityMatrix); @@ -244,11 +239,6 @@ void COpenGLDriver::setTransform(E_TRANSFORMATION_STATE state, const core::matri // first load the viewing transformation for user clip planes glLoadMatrixf((Matrices[ETS_VIEW]).pointer()); - // we have to update the clip planes to the latest view matrix - for (u32 i = 0; i < MaxUserClipPlanes; ++i) - if (UserClipPlanes[i].Enabled) - uploadClipPlane(i); - // now the real model-view matrix glMultMatrixf(Matrices[ETS_WORLD].pointer()); } break; @@ -270,12 +260,14 @@ bool COpenGLDriver::updateVertexHardwareBuffer(SHWBufferLink_opengl *HWBuffer) return false; #if defined(GL_ARB_vertex_buffer_object) - const scene::IMeshBuffer *mb = HWBuffer->MeshBuffer; - const void *vertices = mb->getVertices(); - const u32 vertexCount = mb->getVertexCount(); - const E_VERTEX_TYPE vType = mb->getVertexType(); + const auto *vb = HWBuffer->VertexBuffer; + const void *vertices = vb->getData(); + const u32 vertexCount = vb->getCount(); + const E_VERTEX_TYPE vType = vb->getType(); const u32 vertexSize = getVertexPitchFromType(vType); + accountHWBufferUpload(vertexSize * vertexCount); + const c8 *vbuf = static_cast(vertices); core::array buffer; if (!FeatureAvailable[IRR_ARB_vertex_array_bgra] && !FeatureAvailable[IRR_EXT_vertex_array_bgra]) { @@ -315,26 +307,26 @@ bool COpenGLDriver::updateVertexHardwareBuffer(SHWBufferLink_opengl *HWBuffer) // get or create buffer bool newBuffer = false; - if (!HWBuffer->vbo_verticesID) { - extGlGenBuffers(1, &HWBuffer->vbo_verticesID); - if (!HWBuffer->vbo_verticesID) + if (!HWBuffer->vbo_ID) { + extGlGenBuffers(1, &HWBuffer->vbo_ID); + if (!HWBuffer->vbo_ID) return false; newBuffer = true; - } else if (HWBuffer->vbo_verticesSize < vertexCount * vertexSize) { + } else if (HWBuffer->vbo_Size < vertexCount * vertexSize) { newBuffer = true; } - extGlBindBuffer(GL_ARRAY_BUFFER, HWBuffer->vbo_verticesID); + extGlBindBuffer(GL_ARRAY_BUFFER, HWBuffer->vbo_ID); // copy data to graphics card if (!newBuffer) extGlBufferSubData(GL_ARRAY_BUFFER, 0, vertexCount * vertexSize, vbuf); else { - HWBuffer->vbo_verticesSize = vertexCount * vertexSize; + HWBuffer->vbo_Size = vertexCount * vertexSize; - if (HWBuffer->Mapped_Vertex == scene::EHM_STATIC) + if (vb->getHardwareMappingHint() == scene::EHM_STATIC) extGlBufferData(GL_ARRAY_BUFFER, vertexCount * vertexSize, vbuf, GL_STATIC_DRAW); - else if (HWBuffer->Mapped_Vertex == scene::EHM_DYNAMIC) + else if (vb->getHardwareMappingHint() == scene::EHM_DYNAMIC) extGlBufferData(GL_ARRAY_BUFFER, vertexCount * vertexSize, vbuf, GL_DYNAMIC_DRAW); else // scene::EHM_STREAM extGlBufferData(GL_ARRAY_BUFFER, vertexCount * vertexSize, vbuf, GL_STREAM_DRAW); @@ -357,13 +349,13 @@ bool COpenGLDriver::updateIndexHardwareBuffer(SHWBufferLink_opengl *HWBuffer) return false; #if defined(GL_ARB_vertex_buffer_object) - const scene::IMeshBuffer *mb = HWBuffer->MeshBuffer; + const auto *ib = HWBuffer->IndexBuffer; - const void *indices = mb->getIndices(); - u32 indexCount = mb->getIndexCount(); + const void *indices = ib->getData(); + u32 indexCount = ib->getCount(); GLenum indexSize; - switch (mb->getIndexType()) { + switch (ib->getType()) { case EIT_16BIT: { indexSize = sizeof(u16); break; @@ -377,28 +369,30 @@ bool COpenGLDriver::updateIndexHardwareBuffer(SHWBufferLink_opengl *HWBuffer) } } + accountHWBufferUpload(indexCount * indexSize); + // get or create buffer bool newBuffer = false; - if (!HWBuffer->vbo_indicesID) { - extGlGenBuffers(1, &HWBuffer->vbo_indicesID); - if (!HWBuffer->vbo_indicesID) + if (!HWBuffer->vbo_ID) { + extGlGenBuffers(1, &HWBuffer->vbo_ID); + if (!HWBuffer->vbo_ID) return false; newBuffer = true; - } else if (HWBuffer->vbo_indicesSize < indexCount * indexSize) { + } else if (HWBuffer->vbo_Size < indexCount * indexSize) { newBuffer = true; } - extGlBindBuffer(GL_ELEMENT_ARRAY_BUFFER, HWBuffer->vbo_indicesID); + extGlBindBuffer(GL_ELEMENT_ARRAY_BUFFER, HWBuffer->vbo_ID); // copy data to graphics card if (!newBuffer) extGlBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, indexCount * indexSize, indices); else { - HWBuffer->vbo_indicesSize = indexCount * indexSize; + HWBuffer->vbo_Size = indexCount * indexSize; - if (HWBuffer->Mapped_Index == scene::EHM_STATIC) + if (ib->getHardwareMappingHint() == scene::EHM_STATIC) extGlBufferData(GL_ELEMENT_ARRAY_BUFFER, indexCount * indexSize, indices, GL_STATIC_DRAW); - else if (HWBuffer->Mapped_Index == scene::EHM_DYNAMIC) + else if (ib->getHardwareMappingHint() == scene::EHM_DYNAMIC) extGlBufferData(GL_ELEMENT_ARRAY_BUFFER, indexCount * indexSize, indices, GL_DYNAMIC_DRAW); else // scene::EHM_STREAM extGlBufferData(GL_ELEMENT_ARRAY_BUFFER, indexCount * indexSize, indices, GL_STREAM_DRAW); @@ -418,51 +412,62 @@ bool COpenGLDriver::updateHardwareBuffer(SHWBufferLink *HWBuffer) if (!HWBuffer) return false; - if (HWBuffer->Mapped_Vertex != scene::EHM_NEVER) { - if (HWBuffer->ChangedID_Vertex != HWBuffer->MeshBuffer->getChangedID_Vertex() || !((SHWBufferLink_opengl *)HWBuffer)->vbo_verticesID) { + auto *b = static_cast(HWBuffer); - HWBuffer->ChangedID_Vertex = HWBuffer->MeshBuffer->getChangedID_Vertex(); - - if (!updateVertexHardwareBuffer((SHWBufferLink_opengl *)HWBuffer)) + if (b->IsVertex) { + assert(b->VertexBuffer); + if (b->ChangedID != b->VertexBuffer->getChangedID() || !b->vbo_ID) { + if (!updateVertexHardwareBuffer(b)) return false; + b->ChangedID = b->VertexBuffer->getChangedID(); + } + } else { + assert(b->IndexBuffer); + if (b->ChangedID != b->IndexBuffer->getChangedID() || !b->vbo_ID) { + if (!updateIndexHardwareBuffer(b)) + return false; + b->ChangedID = b->IndexBuffer->getChangedID(); } } - - if (HWBuffer->Mapped_Index != scene::EHM_NEVER) { - if (HWBuffer->ChangedID_Index != HWBuffer->MeshBuffer->getChangedID_Index() || !((SHWBufferLink_opengl *)HWBuffer)->vbo_indicesID) { - - HWBuffer->ChangedID_Index = HWBuffer->MeshBuffer->getChangedID_Index(); - - if (!updateIndexHardwareBuffer((SHWBufferLink_opengl *)HWBuffer)) - return false; - } - } - return true; } //! Create hardware buffer from meshbuffer -COpenGLDriver::SHWBufferLink *COpenGLDriver::createHardwareBuffer(const scene::IMeshBuffer *mb) +COpenGLDriver::SHWBufferLink *COpenGLDriver::createHardwareBuffer(const scene::IVertexBuffer *vb) { #if defined(GL_ARB_vertex_buffer_object) - if (!mb || (mb->getHardwareMappingHint_Index() == scene::EHM_NEVER && mb->getHardwareMappingHint_Vertex() == scene::EHM_NEVER)) + if (!vb || vb->getHardwareMappingHint() == scene::EHM_NEVER) return 0; - SHWBufferLink_opengl *HWBuffer = new SHWBufferLink_opengl(mb); + SHWBufferLink_opengl *HWBuffer = new SHWBufferLink_opengl(vb); // add to map HWBuffer->listPosition = HWBufferList.insert(HWBufferList.end(), HWBuffer); - HWBuffer->ChangedID_Vertex = HWBuffer->MeshBuffer->getChangedID_Vertex(); - HWBuffer->ChangedID_Index = HWBuffer->MeshBuffer->getChangedID_Index(); - HWBuffer->Mapped_Vertex = mb->getHardwareMappingHint_Vertex(); - HWBuffer->Mapped_Index = mb->getHardwareMappingHint_Index(); - HWBuffer->vbo_verticesID = 0; - HWBuffer->vbo_indicesID = 0; - HWBuffer->vbo_verticesSize = 0; - HWBuffer->vbo_indicesSize = 0; + if (!updateVertexHardwareBuffer(HWBuffer)) { + deleteHardwareBuffer(HWBuffer); + return 0; + } - if (!updateHardwareBuffer(HWBuffer)) { + return HWBuffer; +#else + return 0; +#endif +} + +//! Create hardware buffer from meshbuffer +COpenGLDriver::SHWBufferLink *COpenGLDriver::createHardwareBuffer(const scene::IIndexBuffer *ib) +{ +#if defined(GL_ARB_vertex_buffer_object) + if (!ib || ib->getHardwareMappingHint() == scene::EHM_NEVER) + return 0; + + SHWBufferLink_opengl *HWBuffer = new SHWBufferLink_opengl(ib); + + // add to map + HWBuffer->listPosition = HWBufferList.insert(HWBufferList.end(), HWBuffer); + + if (!updateIndexHardwareBuffer(HWBuffer)) { deleteHardwareBuffer(HWBuffer); return 0; } @@ -479,51 +484,51 @@ void COpenGLDriver::deleteHardwareBuffer(SHWBufferLink *_HWBuffer) return; #if defined(GL_ARB_vertex_buffer_object) - SHWBufferLink_opengl *HWBuffer = (SHWBufferLink_opengl *)_HWBuffer; - if (HWBuffer->vbo_verticesID) { - extGlDeleteBuffers(1, &HWBuffer->vbo_verticesID); - HWBuffer->vbo_verticesID = 0; - } - if (HWBuffer->vbo_indicesID) { - extGlDeleteBuffers(1, &HWBuffer->vbo_indicesID); - HWBuffer->vbo_indicesID = 0; + auto *HWBuffer = (SHWBufferLink_opengl *)_HWBuffer; + if (HWBuffer->vbo_ID) { + extGlDeleteBuffers(1, &HWBuffer->vbo_ID); + HWBuffer->vbo_ID = 0; } #endif CNullDriver::deleteHardwareBuffer(_HWBuffer); } -//! Draw hardware buffer -void COpenGLDriver::drawHardwareBuffer(SHWBufferLink *_HWBuffer) +void COpenGLDriver::drawBuffers(const scene::IVertexBuffer *vb, + const scene::IIndexBuffer *ib, u32 PrimitiveCount, + scene::E_PRIMITIVE_TYPE PrimitiveType) { - if (!_HWBuffer) + if (!vb || !ib) return; - updateHardwareBuffer(_HWBuffer); // check if update is needed - #if defined(GL_ARB_vertex_buffer_object) - SHWBufferLink_opengl *HWBuffer = (SHWBufferLink_opengl *)_HWBuffer; + auto *hwvert = (SHWBufferLink_opengl *) getBufferLink(vb); + auto *hwidx = (SHWBufferLink_opengl *) getBufferLink(ib); + updateHardwareBuffer(hwvert); + updateHardwareBuffer(hwidx); - const scene::IMeshBuffer *mb = HWBuffer->MeshBuffer; - const void *vertices = mb->getVertices(); - const void *indexList = mb->getIndices(); - - if (HWBuffer->Mapped_Vertex != scene::EHM_NEVER) { - extGlBindBuffer(GL_ARRAY_BUFFER, HWBuffer->vbo_verticesID); + const void *vertices = vb->getData(); + if (hwvert) { + extGlBindBuffer(GL_ARRAY_BUFFER, hwvert->vbo_ID); vertices = 0; } - if (HWBuffer->Mapped_Index != scene::EHM_NEVER) { - extGlBindBuffer(GL_ELEMENT_ARRAY_BUFFER, HWBuffer->vbo_indicesID); + const void *indexList = ib->getData(); + if (hwidx) { + extGlBindBuffer(GL_ELEMENT_ARRAY_BUFFER, hwidx->vbo_ID); indexList = 0; } - drawVertexPrimitiveList(vertices, mb->getVertexCount(), indexList, mb->getPrimitiveCount(), mb->getVertexType(), mb->getPrimitiveType(), mb->getIndexType()); + drawVertexPrimitiveList(vertices, vb->getCount(), indexList, + PrimitiveCount, vb->getType(), PrimitiveType, ib->getType()); - if (HWBuffer->Mapped_Vertex != scene::EHM_NEVER) + if (hwvert) extGlBindBuffer(GL_ARRAY_BUFFER, 0); - if (HWBuffer->Mapped_Index != scene::EHM_NEVER) + if (hwidx) extGlBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); +#else + drawVertexPrimitiveList(vb->getData(), vb->getCount(), ib->getData(), + PrimitiveCount, vb->getType(), PrimitiveType, ib->getType()); #endif } @@ -1597,15 +1602,14 @@ inline void COpenGLDriver::getGLTextureMatrix(GLfloat *o, const core::matrix4 &m ITexture *COpenGLDriver::createDeviceDependentTexture(const io::path &name, IImage *image) { - core::array imageArray(1); - imageArray.push_back(image); + std::vector tmp { image }; - COpenGLTexture *texture = new COpenGLTexture(name, imageArray, ETT_2D, this); + COpenGLTexture *texture = new COpenGLTexture(name, tmp, ETT_2D, this); return texture; } -ITexture *COpenGLDriver::createDeviceDependentTextureCubemap(const io::path &name, const core::array &image) +ITexture *COpenGLDriver::createDeviceDependentTextureCubemap(const io::path &name, const std::vector &image) { COpenGLTexture *texture = new COpenGLTexture(name, image, ETT_CUBEMAP, this); @@ -1836,112 +1840,11 @@ void COpenGLDriver::setBasicRenderStates(const SMaterial &material, const SMater E_OPENGL_FIXED_PIPELINE_STATE tempState = FixedPipelineState; if (resetAllRenderStates || tempState == EOFPS_ENABLE || tempState == EOFPS_DISABLE_TO_ENABLE) { - // material colors - if (resetAllRenderStates || tempState == EOFPS_DISABLE_TO_ENABLE || - lastmaterial.ColorMaterial != material.ColorMaterial) { - switch (material.ColorMaterial) { - case ECM_NONE: - glDisable(GL_COLOR_MATERIAL); - break; - case ECM_DIFFUSE: - glColorMaterial(GL_FRONT_AND_BACK, GL_DIFFUSE); - break; - case ECM_AMBIENT: - glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT); - break; - case ECM_EMISSIVE: - glColorMaterial(GL_FRONT_AND_BACK, GL_EMISSION); - break; - case ECM_SPECULAR: - glColorMaterial(GL_FRONT_AND_BACK, GL_SPECULAR); - break; - case ECM_DIFFUSE_AND_AMBIENT: - glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE); - break; - } - if (material.ColorMaterial != ECM_NONE) - glEnable(GL_COLOR_MATERIAL); - } - - if (resetAllRenderStates || tempState == EOFPS_DISABLE_TO_ENABLE || - lastmaterial.AmbientColor != material.AmbientColor || - lastmaterial.DiffuseColor != material.DiffuseColor || - lastmaterial.EmissiveColor != material.EmissiveColor || - lastmaterial.ColorMaterial != material.ColorMaterial) { - GLfloat color[4]; - - const f32 inv = 1.0f / 255.0f; - - if ((material.ColorMaterial != video::ECM_AMBIENT) && - (material.ColorMaterial != video::ECM_DIFFUSE_AND_AMBIENT)) { - color[0] = material.AmbientColor.getRed() * inv; - color[1] = material.AmbientColor.getGreen() * inv; - color[2] = material.AmbientColor.getBlue() * inv; - color[3] = material.AmbientColor.getAlpha() * inv; - glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, color); - } - - if ((material.ColorMaterial != video::ECM_DIFFUSE) && - (material.ColorMaterial != video::ECM_DIFFUSE_AND_AMBIENT)) { - color[0] = material.DiffuseColor.getRed() * inv; - color[1] = material.DiffuseColor.getGreen() * inv; - color[2] = material.DiffuseColor.getBlue() * inv; - color[3] = material.DiffuseColor.getAlpha() * inv; - glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, color); - } - - if (material.ColorMaterial != video::ECM_EMISSIVE) { - color[0] = material.EmissiveColor.getRed() * inv; - color[1] = material.EmissiveColor.getGreen() * inv; - color[2] = material.EmissiveColor.getBlue() * inv; - color[3] = material.EmissiveColor.getAlpha() * inv; - glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, color); - } - } - - if (resetAllRenderStates || tempState == EOFPS_DISABLE_TO_ENABLE || - lastmaterial.SpecularColor != material.SpecularColor || - lastmaterial.Shininess != material.Shininess || - lastmaterial.ColorMaterial != material.ColorMaterial) { - GLfloat color[4] = {0.f, 0.f, 0.f, 1.f}; - const f32 inv = 1.0f / 255.0f; - - glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, material.Shininess); - // disable Specular colors if no shininess is set - if ((material.Shininess != 0.0f) && - (material.ColorMaterial != video::ECM_SPECULAR)) { -#ifdef GL_EXT_separate_specular_color - if (FeatureAvailable[IRR_EXT_separate_specular_color]) - glLightModeli(GL_LIGHT_MODEL_COLOR_CONTROL, GL_SEPARATE_SPECULAR_COLOR); -#endif - color[0] = material.SpecularColor.getRed() * inv; - color[1] = material.SpecularColor.getGreen() * inv; - color[2] = material.SpecularColor.getBlue() * inv; - color[3] = material.SpecularColor.getAlpha() * inv; - } -#ifdef GL_EXT_separate_specular_color - else if (FeatureAvailable[IRR_EXT_separate_specular_color]) - glLightModeli(GL_LIGHT_MODEL_COLOR_CONTROL, GL_SINGLE_COLOR); -#endif - glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, color); - } - - // shademode - if (resetAllRenderStates || tempState == EOFPS_DISABLE_TO_ENABLE || - lastmaterial.GouraudShading != material.GouraudShading) { - if (material.GouraudShading) - glShadeModel(GL_SMOOTH); - else - glShadeModel(GL_FLAT); - } - - // lighting - if (resetAllRenderStates || tempState == EOFPS_DISABLE_TO_ENABLE || - lastmaterial.Lighting != material.Lighting) { - if (material.Lighting) - glEnable(GL_LIGHTING); - else - glDisable(GL_LIGHTING); + if (resetAllRenderStates || tempState == EOFPS_DISABLE_TO_ENABLE) { + glDisable(GL_COLOR_MATERIAL); + glDisable(GL_LIGHTING); + glShadeModel(GL_SMOOTH); + glDisable(GL_NORMALIZE); } // fog @@ -1953,15 +1856,6 @@ void COpenGLDriver::setBasicRenderStates(const SMaterial &material, const SMater glDisable(GL_FOG); } - // normalization - if (resetAllRenderStates || tempState == EOFPS_DISABLE_TO_ENABLE || - lastmaterial.NormalizeNormals != material.NormalizeNormals) { - if (material.NormalizeNormals) - glEnable(GL_NORMALIZE); - else - glDisable(GL_NORMALIZE); - } - // Set fixed pipeline as active. tempState = EOFPS_ENABLE; } else if (tempState == EOFPS_ENABLE_TO_DISABLE) { @@ -2401,7 +2295,6 @@ void COpenGLDriver::setRenderStates2DMode(bool alpha, bool texture, bool alphaCh } SMaterial currentMaterial = (!OverrideMaterial2DEnabled) ? InitMaterial2D : OverrideMaterial2D; - currentMaterial.Lighting = false; if (texture) { setTransform(ETS_TEXTURE_0, core::IdentityMatrix); @@ -3062,44 +2955,6 @@ IImage *COpenGLDriver::createScreenShot(video::ECOLOR_FORMAT format, video::E_RE return newImage; } -//! Set/unset a clipping plane. -bool COpenGLDriver::setClipPlane(u32 index, const core::plane3df &plane, bool enable) -{ - if (index >= MaxUserClipPlanes) - return false; - - UserClipPlanes[index].Plane = plane; - enableClipPlane(index, enable); - return true; -} - -void COpenGLDriver::uploadClipPlane(u32 index) -{ - // opengl needs an array of doubles for the plane equation - GLdouble clip_plane[4]; - clip_plane[0] = UserClipPlanes[index].Plane.Normal.X; - clip_plane[1] = UserClipPlanes[index].Plane.Normal.Y; - clip_plane[2] = UserClipPlanes[index].Plane.Normal.Z; - clip_plane[3] = UserClipPlanes[index].Plane.D; - glClipPlane(GL_CLIP_PLANE0 + index, clip_plane); -} - -//! Enable/disable a clipping plane. -void COpenGLDriver::enableClipPlane(u32 index, bool enable) -{ - if (index >= MaxUserClipPlanes) - return; - if (enable) { - if (!UserClipPlanes[index].Enabled) { - uploadClipPlane(index); - glEnable(GL_CLIP_PLANE0 + index); - } - } else - glDisable(GL_CLIP_PLANE0 + index); - - UserClipPlanes[index].Enabled = enable; -} - core::dimension2du COpenGLDriver::getMaxTextureSize() const { return core::dimension2du(MaxTextureSize, MaxTextureSize); diff --git a/irr/src/COpenGLDriver.h b/irr/src/COpenGLDriver.h index 6fe8ac37d..9c4ecd3b3 100644 --- a/irr/src/COpenGLDriver.h +++ b/irr/src/COpenGLDriver.h @@ -58,27 +58,28 @@ public: struct SHWBufferLink_opengl : public SHWBufferLink { - SHWBufferLink_opengl(const scene::IMeshBuffer *_MeshBuffer) : - SHWBufferLink(_MeshBuffer), vbo_verticesID(0), vbo_indicesID(0) {} + SHWBufferLink_opengl(const scene::IVertexBuffer *vb) : SHWBufferLink(vb) {} + SHWBufferLink_opengl(const scene::IIndexBuffer *ib) : SHWBufferLink(ib) {} - GLuint vbo_verticesID; // tmp - GLuint vbo_indicesID; // tmp - - GLuint vbo_verticesSize; // tmp - GLuint vbo_indicesSize; // tmp + GLuint vbo_ID = 0; + u32 vbo_Size = 0; }; //! updates hardware buffer if needed bool updateHardwareBuffer(SHWBufferLink *HWBuffer) override; - //! Create hardware buffer from mesh - SHWBufferLink *createHardwareBuffer(const scene::IMeshBuffer *mb) override; + //! Create hardware buffer from vertex buffer + SHWBufferLink *createHardwareBuffer(const scene::IVertexBuffer *vb) override; + + //! Create hardware buffer from index buffer + SHWBufferLink *createHardwareBuffer(const scene::IIndexBuffer *ib) override; //! Delete hardware buffer (only some drivers can) void deleteHardwareBuffer(SHWBufferLink *HWBuffer) override; - //! Draw hardware buffer - void drawHardwareBuffer(SHWBufferLink *HWBuffer) override; + void drawBuffers(const scene::IVertexBuffer *vb, + const scene::IIndexBuffer *ib, u32 primCount, + scene::E_PRIMITIVE_TYPE pType = scene::EPT_TRIANGLES) override; //! Create occlusion query. /** Use node for identification and mesh for occlusion test. */ @@ -285,19 +286,6 @@ public: //! for performance reasons only available in debug mode bool testGLError(int code = 0); - //! Set/unset a clipping plane. - //! There are at least 6 clipping planes available for the user to set at will. - //! \param index: The plane index. Must be between 0 and MaxUserClipPlanes. - //! \param plane: The plane itself. - //! \param enable: If true, enable the clipping plane else disable it. - bool setClipPlane(u32 index, const core::plane3df &plane, bool enable = false) override; - - //! Enable/disable a clipping plane. - //! There are at least 6 clipping planes available for the user to set at will. - //! \param index: The plane index. Must be between 0 and MaxUserClipPlanes. - //! \param enable: If true, enable the clipping plane else disable it. - void enableClipPlane(u32 index, bool enable) override; - //! Enable the 2d override material void enableMaterial2D(bool enable = true) override; @@ -343,14 +331,12 @@ private: bool updateVertexHardwareBuffer(SHWBufferLink_opengl *HWBuffer); bool updateIndexHardwareBuffer(SHWBufferLink_opengl *HWBuffer); - void uploadClipPlane(u32 index); - //! inits the parts of the open gl driver used on all platforms bool genericDriverInit(); ITexture *createDeviceDependentTexture(const io::path &name, IImage *image) override; - ITexture *createDeviceDependentTextureCubemap(const io::path &name, const core::array &image) override; + ITexture *createDeviceDependentTextureCubemap(const io::path &name, const std::vector &image) override; //! creates a transposed matrix in supplied GLfloat array to pass to OpenGL inline void getGLMatrix(GLfloat gl_matrix[16], const core::matrix4 &m); @@ -404,15 +390,6 @@ private: SMaterial Material, LastMaterial; - struct SUserClipPlane - { - SUserClipPlane() : - Enabled(false) {} - core::plane3df Plane; - bool Enabled; - }; - core::array UserClipPlanes; - core::stringc VendorName; core::matrix4 TextureFlipMatrix; diff --git a/irr/src/COpenGLExtensionHandler.cpp b/irr/src/COpenGLExtensionHandler.cpp index d5aade126..6127d6e46 100644 --- a/irr/src/COpenGLExtensionHandler.cpp +++ b/irr/src/COpenGLExtensionHandler.cpp @@ -20,7 +20,7 @@ bool COpenGLExtensionHandler::needsDSAFramebufferHack = true; COpenGLExtensionHandler::COpenGLExtensionHandler() : StencilBuffer(false), TextureCompressionExtension(false), MaxLights(1), - MaxAnisotropy(1), MaxUserClipPlanes(0), MaxAuxBuffers(0), MaxIndices(65535), + MaxAnisotropy(1), MaxAuxBuffers(0), MaxIndices(65535), MaxTextureSize(1), MaxGeometryVerticesOut(0), MaxTextureLODBias(0.f), Version(0), ShaderLanguageVersion(0), OcclusionQuerySupport(false), IsAtiRadeonX(false), pGlActiveTexture(0), pGlActiveTextureARB(0), pGlClientActiveTextureARB(0), @@ -428,8 +428,6 @@ void COpenGLExtensionHandler::initExtensions(video::IContextManager *cmgr, bool if (FeatureAvailable[IRR_EXT_texture_lod_bias]) glGetFloatv(GL_MAX_TEXTURE_LOD_BIAS_EXT, &MaxTextureLODBias); #endif - glGetIntegerv(GL_MAX_CLIP_PLANES, &num); - MaxUserClipPlanes = static_cast(num); glGetIntegerv(GL_AUX_BUFFERS, &num); MaxAuxBuffers = static_cast(num); #ifdef GL_ARB_draw_buffers diff --git a/irr/src/COpenGLExtensionHandler.h b/irr/src/COpenGLExtensionHandler.h index ada1c9a3a..cdff911b9 100644 --- a/irr/src/COpenGLExtensionHandler.h +++ b/irr/src/COpenGLExtensionHandler.h @@ -1019,8 +1019,6 @@ public: u8 MaxLights; //! Maximal Anisotropy u8 MaxAnisotropy; - //! Number of user clipplanes - u8 MaxUserClipPlanes; //! Number of auxiliary buffers u8 MaxAuxBuffers; //! Optimal number of indices per meshbuffer diff --git a/irr/src/CReadFile.h b/irr/src/CReadFile.h index 14f674772..c9231be7c 100644 --- a/irr/src/CReadFile.h +++ b/irr/src/CReadFile.h @@ -17,7 +17,7 @@ namespace io /*! Class for reading a real file from disk. */ -class CReadFile : public IReadFile +class CReadFile final : public IReadFile { public: CReadFile(const io::path &fileName); diff --git a/irr/src/CSceneCollisionManager.cpp b/irr/src/CSceneCollisionManager.cpp index 692f3c44f..77549a7dc 100644 --- a/irr/src/CSceneCollisionManager.cpp +++ b/irr/src/CSceneCollisionManager.cpp @@ -6,7 +6,6 @@ #include "ICameraSceneNode.h" #include "SViewFrustum.h" -#include "os.h" #include "irrMath.h" namespace irr diff --git a/irr/src/CSceneManager.cpp b/irr/src/CSceneManager.cpp index b20f6010a..c75a28a48 100644 --- a/irr/src/CSceneManager.cpp +++ b/irr/src/CSceneManager.cpp @@ -2,6 +2,8 @@ // This file is part of the "Irrlicht Engine". // For conditions of distribution and use, see copyright notice in irrlicht.h +#include + #include "CSceneManager.h" #include "IVideoDriver.h" #include "IFileSystem.h" @@ -18,6 +20,7 @@ #include "CXMeshFileLoader.h" #include "COBJMeshFileLoader.h" #include "CB3DMeshFileLoader.h" +#include "CGLTFMeshFileLoader.h" #include "CBillboardSceneNode.h" #include "CAnimatedMeshSceneNode.h" #include "CCameraSceneNode.h" @@ -76,6 +79,7 @@ CSceneManager::CSceneManager(video::IVideoDriver *driver, MeshLoaderList.push_back(new CXMeshFileLoader(this)); MeshLoaderList.push_back(new COBJMeshFileLoader(this)); MeshLoaderList.push_back(new CB3DMeshFileLoader(this)); + MeshLoaderList.push_back(new CGLTFMeshFileLoader()); } //! destructor @@ -95,9 +99,8 @@ CSceneManager::~CSceneManager() if (CollisionManager) CollisionManager->drop(); - u32 i; - for (i = 0; i < MeshLoaderList.size(); ++i) - MeshLoaderList[i]->drop(); + for (auto *loader : MeshLoaderList) + loader->drop(); if (ActiveCamera) ActiveCamera->drop(); @@ -140,12 +143,11 @@ IAnimatedMesh *CSceneManager::getUncachedMesh(io::IReadFile *file, const io::pat IAnimatedMesh *msh = 0; // iterate the list in reverse order so user-added loaders can override the built-in ones - s32 count = MeshLoaderList.size(); - for (s32 i = count - 1; i >= 0; --i) { - if (MeshLoaderList[i]->isALoadableFileExtension(filename)) { + for (auto it = MeshLoaderList.rbegin(); it != MeshLoaderList.rend(); it++) { + if ((*it)->isALoadableFileExtension(filename)) { // reset file to avoid side effects of previous calls to createMesh file->seek(0); - msh = MeshLoaderList[i]->createMesh(file); + msh = (*it)->createMesh(file); if (msh) { MeshCache->addMesh(cachename, msh); msh->drop(); @@ -388,14 +390,8 @@ u32 CSceneManager::registerNodeForRendering(ISceneNode *node, E_SCENE_NODE_RENDE switch (pass) { // take camera if it is not already registered case ESNRP_CAMERA: { - taken = 1; - for (u32 i = 0; i != CameraList.size(); ++i) { - if (CameraList[i] == node) { - taken = 0; - break; - } - } - if (taken) { + if (std::find(CameraList.begin(), CameraList.end(), node) == CameraList.end()) { + taken = 1; CameraList.push_back(node); } } break; @@ -405,19 +401,19 @@ u32 CSceneManager::registerNodeForRendering(ISceneNode *node, E_SCENE_NODE_RENDE break; case ESNRP_SOLID: if (!isCulled(node)) { - SolidNodeList.push_back(node); + SolidNodeList.emplace_back(node); taken = 1; } break; case ESNRP_TRANSPARENT: if (!isCulled(node)) { - TransparentNodeList.push_back(TransparentNodeEntry(node, camWorldPos)); + TransparentNodeList.emplace_back(node, camWorldPos); taken = 1; } break; case ESNRP_TRANSPARENT_EFFECT: if (!isCulled(node)) { - TransparentEffectNodeList.push_back(TransparentNodeEntry(node, camWorldPos)); + TransparentEffectNodeList.emplace_back(node, camWorldPos); taken = 1; } break; @@ -429,8 +425,7 @@ u32 CSceneManager::registerNodeForRendering(ISceneNode *node, E_SCENE_NODE_RENDE for (u32 i = 0; i < count; ++i) { if (Driver->needsTransparentRenderPass(node->getMaterial(i))) { // register as transparent node - TransparentNodeEntry e(node, camWorldPos); - TransparentNodeList.push_back(e); + TransparentNodeList.emplace_back(node, camWorldPos); taken = 1; break; } @@ -438,7 +433,7 @@ u32 CSceneManager::registerNodeForRendering(ISceneNode *node, E_SCENE_NODE_RENDE // not transparent, register as solid if (!taken) { - SolidNodeList.push_back(node); + SolidNodeList.emplace_back(node); taken = 1; } } @@ -509,10 +504,10 @@ void CSceneManager::drawAll() CurrentRenderPass = ESNRP_CAMERA; Driver->getOverrideMaterial().Enabled = ((Driver->getOverrideMaterial().EnablePasses & CurrentRenderPass) != 0); - for (i = 0; i < CameraList.size(); ++i) - CameraList[i]->render(); + for (auto *node : CameraList) + node->render(); - CameraList.set_used(0); + CameraList.clear(); } // render skyboxes @@ -520,10 +515,10 @@ void CSceneManager::drawAll() CurrentRenderPass = ESNRP_SKY_BOX; Driver->getOverrideMaterial().Enabled = ((Driver->getOverrideMaterial().EnablePasses & CurrentRenderPass) != 0); - for (i = 0; i < SkyBoxList.size(); ++i) - SkyBoxList[i]->render(); + for (auto *node : SkyBoxList) + node->render(); - SkyBoxList.set_used(0); + SkyBoxList.clear(); } // render default objects @@ -531,12 +526,12 @@ void CSceneManager::drawAll() CurrentRenderPass = ESNRP_SOLID; Driver->getOverrideMaterial().Enabled = ((Driver->getOverrideMaterial().EnablePasses & CurrentRenderPass) != 0); - SolidNodeList.sort(); // sort by textures + std::sort(SolidNodeList.begin(), SolidNodeList.end()); - for (i = 0; i < SolidNodeList.size(); ++i) - SolidNodeList[i].Node->render(); + for (auto &it : SolidNodeList) + it.Node->render(); - SolidNodeList.set_used(0); + SolidNodeList.clear(); } // render transparent objects. @@ -544,11 +539,12 @@ void CSceneManager::drawAll() CurrentRenderPass = ESNRP_TRANSPARENT; Driver->getOverrideMaterial().Enabled = ((Driver->getOverrideMaterial().EnablePasses & CurrentRenderPass) != 0); - TransparentNodeList.sort(); // sort by distance from camera - for (i = 0; i < TransparentNodeList.size(); ++i) - TransparentNodeList[i].Node->render(); + std::sort(TransparentNodeList.begin(), TransparentNodeList.end()); - TransparentNodeList.set_used(0); + for (auto &it : TransparentNodeList) + it.Node->render(); + + TransparentNodeList.clear(); } // render transparent effect objects. @@ -556,12 +552,12 @@ void CSceneManager::drawAll() CurrentRenderPass = ESNRP_TRANSPARENT_EFFECT; Driver->getOverrideMaterial().Enabled = ((Driver->getOverrideMaterial().EnablePasses & CurrentRenderPass) != 0); - TransparentEffectNodeList.sort(); // sort by distance from camera + std::sort(TransparentEffectNodeList.begin(), TransparentEffectNodeList.end()); - for (i = 0; i < TransparentEffectNodeList.size(); ++i) - TransparentEffectNodeList[i].Node->render(); + for (auto &it : TransparentEffectNodeList) + it.Node->render(); - TransparentEffectNodeList.set_used(0); + TransparentEffectNodeList.clear(); } // render custom gui nodes @@ -569,10 +565,10 @@ void CSceneManager::drawAll() CurrentRenderPass = ESNRP_GUI; Driver->getOverrideMaterial().Enabled = ((Driver->getOverrideMaterial().EnablePasses & CurrentRenderPass) != 0); - for (i = 0; i < GuiNodeList.size(); ++i) - GuiNodeList[i]->render(); + for (auto *node : GuiNodeList) + node->render(); - GuiNodeList.set_used(0); + GuiNodeList.clear(); } clearDeletionList(); @@ -592,7 +588,7 @@ void CSceneManager::addExternalMeshLoader(IMeshLoader *externalLoader) //! Returns the number of mesh loaders supported by Irrlicht at this time u32 CSceneManager::getMeshLoaderCount() const { - return MeshLoaderList.size(); + return static_cast(MeshLoaderList.size()); } //! Retrieve the given mesh loader @@ -629,12 +625,9 @@ void CSceneManager::addToDeletionQueue(ISceneNode *node) //! clears the deletion list void CSceneManager::clearDeletionList() { - if (DeletionList.empty()) - return; - - for (u32 i = 0; i < DeletionList.size(); ++i) { - DeletionList[i]->remove(); - DeletionList[i]->drop(); + for (auto *node : DeletionList) { + node->remove(); + node->drop(); } DeletionList.clear(); diff --git a/irr/src/CSceneManager.h b/irr/src/CSceneManager.h index 0e27290a8..4ef6d64b0 100644 --- a/irr/src/CSceneManager.h +++ b/irr/src/CSceneManager.h @@ -25,7 +25,7 @@ class IMeshCache; /*! The Scene Manager manages scene nodes, mesh resources, cameras and all the other stuff. */ -class CSceneManager : public ISceneManager, public ISceneNode +class CSceneManager final : public ISceneManager, public ISceneNode { public: //! constructor @@ -277,15 +277,15 @@ private: ISceneCollisionManager *CollisionManager; //! render pass lists - core::array CameraList; - core::array SkyBoxList; - core::array SolidNodeList; - core::array TransparentNodeList; - core::array TransparentEffectNodeList; - core::array GuiNodeList; + std::vector CameraList; + std::vector SkyBoxList; + std::vector SolidNodeList; + std::vector TransparentNodeList; + std::vector TransparentEffectNodeList; + std::vector GuiNodeList; - core::array MeshLoaderList; - core::array DeletionList; + std::vector MeshLoaderList; + std::vector DeletionList; //! current active camera ICameraSceneNode *ActiveCamera; diff --git a/irr/src/CSkinnedMesh.cpp b/irr/src/CSkinnedMesh.cpp index eb7317309..875fd8e7e 100644 --- a/irr/src/CSkinnedMesh.cpp +++ b/irr/src/CSkinnedMesh.cpp @@ -6,6 +6,7 @@ #include #include "CBoneSceneNode.h" #include "IAnimatedMeshSceneNode.h" +#include "SSkinMeshBuffer.h" #include "os.h" namespace @@ -110,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. @@ -132,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; } @@ -221,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() --- @@ -495,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; @@ -509,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; @@ -596,6 +598,15 @@ IMeshBuffer *CSkinnedMesh::getMeshBuffer(const video::SMaterial &material) const return 0; } +u32 CSkinnedMesh::getTextureSlot(u32 meshbufNr) const +{ + return TextureSlots.at(meshbufNr); +} + +void CSkinnedMesh::setTextureSlot(u32 meshbufNr, u32 textureSlot) { + TextureSlots.at(meshbufNr) = textureSlot; +} + //! returns an axis aligned bounding box const core::aabbox3d &CSkinnedMesh::getBoundingBox() const { @@ -754,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) @@ -1057,10 +1068,17 @@ void CSkinnedMesh::updateBoundingBox(void) scene::SSkinMeshBuffer *CSkinnedMesh::addMeshBuffer() { scene::SSkinMeshBuffer *buffer = new scene::SSkinMeshBuffer(); + TextureSlots.push_back(LocalBuffers.size()); LocalBuffers.push_back(buffer); return buffer; } +void CSkinnedMesh::addMeshBuffer(SSkinMeshBuffer *meshbuf) +{ + TextureSlots.push_back(LocalBuffers.size()); + LocalBuffers.push_back(meshbuf); +} + CSkinnedMesh::SJoint *CSkinnedMesh::addJoint(SJoint *parent) { SJoint *joint = new SJoint; diff --git a/irr/src/CSkinnedMesh.h b/irr/src/CSkinnedMesh.h index b0228c93b..1be6ee7bc 100644 --- a/irr/src/CSkinnedMesh.h +++ b/irr/src/CSkinnedMesh.h @@ -27,8 +27,8 @@ public: //! destructor virtual ~CSkinnedMesh(); - //! returns the amount of frames. If the amount is 1, it is a static (=non animated) mesh. - u32 getFrameCount() const override; + //! If the duration is 0, it is a static (=non animated) mesh. + f32 getMaxFrameNumber() const override; //! Gets the default animation speed of the animated mesh. /** \return Amount of frames per second. If the amount is 0, it is a static, non animated mesh. */ @@ -39,8 +39,8 @@ public: The actual speed is set in the scene node the mesh is instantiated in.*/ void setAnimationSpeed(f32 fps) override; - //! returns the animated mesh based on a detail level (which is ignored) - IMesh *getMesh(s32 frame, s32 detailLevel = 255, s32 startFrameLoop = -1, s32 endFrameLoop = -1) override; + //! returns the animated mesh for the given frame + IMesh *getMesh(f32) override; //! Animates this mesh's joints based on frame input //! blend: {0-old position, 1-New position} @@ -61,6 +61,10 @@ public: NULL if there is no such mesh buffer. */ IMeshBuffer *getMeshBuffer(const video::SMaterial &material) const override; + u32 getTextureSlot(u32 meshbufNr) const override; + + void setTextureSlot(u32 meshbufNr, u32 textureSlot); + //! returns an axis aligned bounding box const core::aabbox3d &getBoundingBox() const override; @@ -129,6 +133,9 @@ public: //! Adds a new meshbuffer to the mesh, access it as last one SSkinMeshBuffer *addMeshBuffer() override; + //! Adds a new meshbuffer to the mesh, access it as last one + void addMeshBuffer(SSkinMeshBuffer *meshbuf) override; + //! Adds a new joint to the mesh, access it as last one SJoint *addJoint(SJoint *parent = 0) override; @@ -184,6 +191,8 @@ private: core::array *SkinningBuffers; // Meshbuffer to skin, default is to skin localBuffers core::array LocalBuffers; + //! Mapping from meshbuffer number to bindable texture slot + std::vector TextureSlots; core::array AllJoints; core::array RootJoints; diff --git a/irr/src/CXMeshFileLoader.cpp b/irr/src/CXMeshFileLoader.cpp index 5978980f4..967fc367c 100644 --- a/irr/src/CXMeshFileLoader.cpp +++ b/irr/src/CXMeshFileLoader.cpp @@ -109,10 +109,6 @@ bool CXMeshFileLoader::load(io::IReadFile *file) // default material if nothing loaded if (!mesh->Materials.size()) { mesh->Materials.push_back(video::SMaterial()); - mesh->Materials[0].DiffuseColor.set(0xff777777); - mesh->Materials[0].Shininess = 0.f; - mesh->Materials[0].SpecularColor.set(0xff777777); - mesh->Materials[0].EmissiveColor.set(0xff000000); } u32 i; @@ -142,7 +138,7 @@ bool CXMeshFileLoader::load(io::IReadFile *file) if (!mesh->HasVertexColors) { for (u32 j = 0; j < mesh->FaceMaterialIndices.size(); ++j) { for (u32 id = j * 3 + 0; id <= j * 3 + 2; ++id) { - mesh->Vertices[mesh->Indices[id]].Color = mesh->Buffers[mesh->FaceMaterialIndices[j]]->Material.DiffuseColor; + mesh->Vertices[mesh->Indices[id]].Color = 0xff777777; } } } @@ -273,12 +269,12 @@ bool CXMeshFileLoader::load(io::IReadFile *file) } if (mesh->TCoords2.size()) { for (i = 0; i != mesh->Buffers.size(); ++i) { - mesh->Buffers[i]->Vertices_2TCoords.reallocate(vCountArray[i]); + mesh->Buffers[i]->Vertices_2TCoords->Data.reserve(vCountArray[i]); mesh->Buffers[i]->VertexType = video::EVT_2TCOORDS; } } else { for (i = 0; i != mesh->Buffers.size(); ++i) - mesh->Buffers[i]->Vertices_Standard.reallocate(vCountArray[i]); + mesh->Buffers[i]->Vertices_Standard->Data.reserve(vCountArray[i]); } verticesLinkIndex.set_used(mesh->Vertices.size()); @@ -290,14 +286,14 @@ bool CXMeshFileLoader::load(io::IReadFile *file) scene::SSkinMeshBuffer *buffer = mesh->Buffers[verticesLinkBuffer[i]]; if (mesh->TCoords2.size()) { - verticesLinkIndex[i] = buffer->Vertices_2TCoords.size(); - buffer->Vertices_2TCoords.push_back(mesh->Vertices[i]); + verticesLinkIndex[i] = buffer->Vertices_2TCoords->getCount(); + buffer->Vertices_2TCoords->Data.emplace_back(mesh->Vertices[i]); // We have a problem with correct tcoord2 handling here // crash fixed for now by checking the values - buffer->Vertices_2TCoords.getLast().TCoords2 = (i < mesh->TCoords2.size()) ? mesh->TCoords2[i] : mesh->Vertices[i].TCoords; + buffer->Vertices_2TCoords->Data.back().TCoords2 = (i < mesh->TCoords2.size()) ? mesh->TCoords2[i] : mesh->Vertices[i].TCoords; } else { - verticesLinkIndex[i] = buffer->Vertices_Standard.size(); - buffer->Vertices_Standard.push_back(mesh->Vertices[i]); + verticesLinkIndex[i] = buffer->Vertices_Standard->getCount(); + buffer->Vertices_Standard->Data.push_back(mesh->Vertices[i]); } } @@ -306,13 +302,13 @@ bool CXMeshFileLoader::load(io::IReadFile *file) for (i = 0; i < mesh->FaceMaterialIndices.size(); ++i) ++vCountArray[mesh->FaceMaterialIndices[i]]; for (i = 0; i != mesh->Buffers.size(); ++i) - mesh->Buffers[i]->Indices.reallocate(vCountArray[i]); + mesh->Buffers[i]->Indices->Data.reserve(vCountArray[i]); delete[] vCountArray; // create indices per buffer for (i = 0; i < mesh->FaceMaterialIndices.size(); ++i) { scene::SSkinMeshBuffer *buffer = mesh->Buffers[mesh->FaceMaterialIndices[i]]; for (u32 id = i * 3 + 0; id != i * 3 + 3; ++id) { - buffer->Indices.push_back(verticesLinkIndex[mesh->Indices[id]]); + buffer->Indices->Data.push_back(verticesLinkIndex[mesh->Indices[id]]); } } } @@ -994,9 +990,9 @@ bool CXMeshFileLoader::parseDataObjectSkinWeights(SXMesh &mesh) // transforms the mesh vertices to the space of the bone // When concatenated to the bone's transform, this provides the // world space coordinates of the mesh as affected by the bone - core::matrix4 &MatrixOffset = joint->GlobalInversedMatrix; - + core::matrix4 MatrixOffset; readMatrix(MatrixOffset); + joint->GlobalInversedMatrix = MatrixOffset; if (!checkForOneFollowingSemicolons()) { os::Printer::log("No finishing semicolon in Skin Weights found in x file", ELL_WARNING); diff --git a/irr/src/CZipReader.cpp b/irr/src/CZipReader.cpp index b761c72e8..036f6302a 100644 --- a/irr/src/CZipReader.cpp +++ b/irr/src/CZipReader.cpp @@ -191,8 +191,7 @@ bool CZipReader::scanGZipHeader() } } else { // no file name? - ZipFileName = Path; - core::deletePathFromFilename(ZipFileName); + ZipFileName = core::deletePathFromFilename(Path); // rename tgz to tar or remove gz extension if (core::hasFileExtension(ZipFileName, "tgz")) { @@ -325,7 +324,7 @@ bool CZipReader::scanZipHeader(bool ignoreGPBits) dirEnd.Offset = os::Byteswap::byteswap(dirEnd.Offset); dirEnd.CommentLength = os::Byteswap::byteswap(dirEnd.CommentLength); #endif - FileInfo.reallocate(dirEnd.TotalEntries); + FileInfo.reserve(dirEnd.TotalEntries); File->seek(dirEnd.Offset); while (scanCentralDirectoryHeader()) { } @@ -381,9 +380,10 @@ bool CZipReader::scanCentralDirectoryHeader() File->seek(entry.RelativeOffsetOfLocalHeader); scanZipHeader(true); File->seek(pos + entry.FilenameLength + entry.ExtraFieldLength + entry.FileCommentLength); - FileInfo.getLast().header.DataDescriptor.CompressedSize = entry.CompressedSize; - FileInfo.getLast().header.DataDescriptor.UncompressedSize = entry.UncompressedSize; - FileInfo.getLast().header.DataDescriptor.CRC32 = entry.CRC32; + auto &lastInfo = FileInfo.back(); + lastInfo.header.DataDescriptor.CompressedSize = entry.CompressedSize; + lastInfo.header.DataDescriptor.UncompressedSize = entry.UncompressedSize; + lastInfo.header.DataDescriptor.CRC32 = entry.CRC32; Files.getLast().Size = entry.UncompressedSize; return true; } diff --git a/irr/src/CZipReader.h b/irr/src/CZipReader.h index d9afd668a..b520c2030 100644 --- a/irr/src/CZipReader.h +++ b/irr/src/CZipReader.h @@ -4,8 +4,8 @@ #pragma once +#include #include "IReadFile.h" -#include "irrArray.h" #include "irrString.h" #include "IFileSystem.h" #include "CFileList.h" @@ -209,7 +209,7 @@ protected: IReadFile *File; // holds extended info about files - core::array FileInfo; + std::vector FileInfo; bool IsGZip; }; diff --git a/irr/src/IAttribute.h b/irr/src/IAttribute.h index b0dc76eee..23352a623 100644 --- a/irr/src/IAttribute.h +++ b/irr/src/IAttribute.h @@ -4,15 +4,8 @@ #pragma once -#include "IReferenceCounted.h" -#include "SColor.h" -#include "vector3d.h" -#include "vector2d.h" -#include "position2d.h" -#include "rect.h" -#include "dimension2d.h" +#include "irrTypes.h" #include "irrString.h" -#include "irrArray.h" #include "EAttributes.h" namespace irr @@ -20,16 +13,7 @@ namespace irr namespace io { -// All derived attribute types implement at least getter/setter for their own type (like CBoolAttribute will have setBool/getBool). -// Simple types will also implement getStringW and setString, but don't expect it to work for all types. -// String serialization makes no sense for some attribute-types (like stringw arrays or pointers), but is still useful for many types. -// (Note: I do _not_ know yet why the default string serialization is asymmetric with char* in set and wchar_t* in get). -// Additionally many attribute types will implement conversion functions like CBoolAttribute has p.E. getInt/setInt(). -// The reason for conversion functions is likely to make reading old formats easier which have changed in the meantime. For example -// an old xml can contain a bool attribute which is an int in a newer format. You can still call getInt() even thought the attribute has the wrong type. -// And please do _not_ confuse these attributes here with the ones used in the xml-reader (aka SAttribute which is just a key-value pair). - -class IAttribute : public virtual IReferenceCounted +class IAttribute { public: virtual ~IAttribute(){}; diff --git a/irr/src/Irrlicht.cpp b/irr/src/Irrlicht.cpp index d119584e3..eb94fa4eb 100644 --- a/irr/src/Irrlicht.cpp +++ b/irr/src/Irrlicht.cpp @@ -100,10 +100,6 @@ extern "C" IRRLICHT_API bool IRRCALLCONV isDriverSupported(E_DRIVER_TYPE driver) case EDT_OPENGL: return true; #endif -#ifdef _IRR_COMPILE_WITH_OGLES1_ - case EDT_OGLES1: - return true; -#endif #ifdef _IRR_COMPILE_WITH_OGLES2_ case EDT_OGLES2: return true; diff --git a/irr/src/OpenGL/Driver.cpp b/irr/src/OpenGL/Driver.cpp index 95d760548..fe9a24758 100644 --- a/irr/src/OpenGL/Driver.cpp +++ b/irr/src/OpenGL/Driver.cpp @@ -255,11 +255,7 @@ bool COpenGL3DriverBase::genericDriverInit(const core::dimension2d &screenS DriverAttributes->setAttribute("MaxTextures", (s32)Feature.MaxTextureUnits); DriverAttributes->setAttribute("MaxSupportedTextures", (s32)Feature.MaxTextureUnits); - // DriverAttributes->setAttribute("MaxLights", MaxLights); DriverAttributes->setAttribute("MaxAnisotropy", MaxAnisotropy); - // DriverAttributes->setAttribute("MaxUserClipPlanes", MaxUserClipPlanes); - // DriverAttributes->setAttribute("MaxAuxBuffers", MaxAuxBuffers); - // DriverAttributes->setAttribute("MaxMultipleRenderTargets", MaxMultipleRenderTargets); DriverAttributes->setAttribute("MaxIndices", (s32)MaxIndices); DriverAttributes->setAttribute("MaxTextureSize", (s32)MaxTextureSize); DriverAttributes->setAttribute("MaxTextureLODBias", MaxTextureLODBias); @@ -268,8 +264,6 @@ bool COpenGL3DriverBase::genericDriverInit(const core::dimension2d &screenS GL.PixelStorei(GL_PACK_ALIGNMENT, 1); - UserClipPlane.reallocate(0); - for (s32 i = 0; i < ETS_COUNT; ++i) setTransform(static_cast(i), core::IdentityMatrix); @@ -483,43 +477,36 @@ void COpenGL3DriverBase::setTransform(E_TRANSFORMATION_STATE state, const core:: Transformation3DChanged = true; } -bool COpenGL3DriverBase::updateVertexHardwareBuffer(SHWBufferLink_opengl *HWBuffer) +bool COpenGL3DriverBase::updateHardwareBuffer(SHWBufferLink_opengl *HWBuffer, + const void *buffer, size_t bufferSize, scene::E_HARDWARE_MAPPING hint) { - if (!HWBuffer) - return false; + assert(HWBuffer); - const scene::IMeshBuffer *mb = HWBuffer->MeshBuffer; - const void *vertices = mb->getVertices(); - const u32 vertexCount = mb->getVertexCount(); - const E_VERTEX_TYPE vType = mb->getVertexType(); - const u32 vertexSize = getVertexPitchFromType(vType); - - const void *buffer = vertices; - size_t bufferSize = vertexSize * vertexCount; + accountHWBufferUpload(bufferSize); // get or create buffer bool newBuffer = false; - if (!HWBuffer->vbo_verticesID) { - GL.GenBuffers(1, &HWBuffer->vbo_verticesID); - if (!HWBuffer->vbo_verticesID) + if (!HWBuffer->vbo_ID) { + GL.GenBuffers(1, &HWBuffer->vbo_ID); + if (!HWBuffer->vbo_ID) return false; newBuffer = true; - } else if (HWBuffer->vbo_verticesSize < bufferSize) { + } else if (HWBuffer->vbo_Size < bufferSize) { newBuffer = true; } - GL.BindBuffer(GL_ARRAY_BUFFER, HWBuffer->vbo_verticesID); + GL.BindBuffer(GL_ARRAY_BUFFER, HWBuffer->vbo_ID); // copy data to graphics card if (!newBuffer) GL.BufferSubData(GL_ARRAY_BUFFER, 0, bufferSize, buffer); else { - HWBuffer->vbo_verticesSize = bufferSize; + HWBuffer->vbo_Size = bufferSize; GLenum usage = GL_STATIC_DRAW; - if (HWBuffer->Mapped_Index == scene::EHM_STREAM) + if (hint == scene::EHM_STREAM) usage = GL_STREAM_DRAW; - else if (HWBuffer->Mapped_Index == scene::EHM_DYNAMIC) + else if (hint == scene::EHM_DYNAMIC) usage = GL_DYNAMIC_DRAW; GL.BufferData(GL_ARRAY_BUFFER, bufferSize, buffer, usage); } @@ -529,113 +516,83 @@ bool COpenGL3DriverBase::updateVertexHardwareBuffer(SHWBufferLink_opengl *HWBuff return (!TEST_GL_ERROR(this)); } +bool COpenGL3DriverBase::updateVertexHardwareBuffer(SHWBufferLink_opengl *HWBuffer) +{ + if (!HWBuffer) + return false; + + assert(HWBuffer->IsVertex); + const auto *vb = HWBuffer->VertexBuffer; + assert(vb); + + const u32 vertexSize = getVertexPitchFromType(vb->getType()); + const size_t bufferSize = vertexSize * vb->getCount(); + + return updateHardwareBuffer(HWBuffer, vb->getData(), bufferSize, vb->getHardwareMappingHint()); +} + bool COpenGL3DriverBase::updateIndexHardwareBuffer(SHWBufferLink_opengl *HWBuffer) { if (!HWBuffer) return false; - const scene::IMeshBuffer *mb = HWBuffer->MeshBuffer; + assert(!HWBuffer->IsVertex); + const auto *ib = HWBuffer->IndexBuffer; + assert(ib); - const void *indices = mb->getIndices(); - u32 indexCount = mb->getIndexCount(); - - GLenum indexSize; - switch (mb->getIndexType()) { - case (EIT_16BIT): { + u32 indexSize; + switch (ib->getType()) { + case EIT_16BIT: indexSize = sizeof(u16); break; - } - case (EIT_32BIT): { + case EIT_32BIT: indexSize = sizeof(u32); break; - } - default: { + default: return false; } - } - // get or create buffer - bool newBuffer = false; - if (!HWBuffer->vbo_indicesID) { - GL.GenBuffers(1, &HWBuffer->vbo_indicesID); - if (!HWBuffer->vbo_indicesID) - return false; - newBuffer = true; - } else if (HWBuffer->vbo_indicesSize < indexCount * indexSize) { - newBuffer = true; - } + const size_t bufferSize = ib->getCount() * indexSize; - GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, HWBuffer->vbo_indicesID); - - // copy data to graphics card - if (!newBuffer) - GL.BufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, indexCount * indexSize, indices); - else { - HWBuffer->vbo_indicesSize = indexCount * indexSize; - - GLenum usage = GL_STATIC_DRAW; - if (HWBuffer->Mapped_Index == scene::EHM_STREAM) - usage = GL_STREAM_DRAW; - else if (HWBuffer->Mapped_Index == scene::EHM_DYNAMIC) - usage = GL_DYNAMIC_DRAW; - GL.BufferData(GL_ELEMENT_ARRAY_BUFFER, indexCount * indexSize, indices, usage); - } - - GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); - - return (!TEST_GL_ERROR(this)); + return updateHardwareBuffer(HWBuffer, ib->getData(), bufferSize, ib->getHardwareMappingHint()); } -//! updates hardware buffer if needed bool COpenGL3DriverBase::updateHardwareBuffer(SHWBufferLink *HWBuffer) { if (!HWBuffer) return false; - if (HWBuffer->Mapped_Vertex != scene::EHM_NEVER) { - if (HWBuffer->ChangedID_Vertex != HWBuffer->MeshBuffer->getChangedID_Vertex() || !static_cast(HWBuffer)->vbo_verticesID) { + auto *b = static_cast(HWBuffer); - HWBuffer->ChangedID_Vertex = HWBuffer->MeshBuffer->getChangedID_Vertex(); - - if (!updateVertexHardwareBuffer(static_cast(HWBuffer))) + if (b->IsVertex) { + assert(b->VertexBuffer); + if (b->ChangedID != b->VertexBuffer->getChangedID() || !b->vbo_ID) { + if (!updateVertexHardwareBuffer(b)) return false; + b->ChangedID = b->VertexBuffer->getChangedID(); + } + } else { + assert(b->IndexBuffer); + if (b->ChangedID != b->IndexBuffer->getChangedID() || !b->vbo_ID) { + if (!updateIndexHardwareBuffer(b)) + return false; + b->ChangedID = b->IndexBuffer->getChangedID(); } } - - if (HWBuffer->Mapped_Index != scene::EHM_NEVER) { - if (HWBuffer->ChangedID_Index != HWBuffer->MeshBuffer->getChangedID_Index() || !static_cast(HWBuffer)->vbo_indicesID) { - - HWBuffer->ChangedID_Index = HWBuffer->MeshBuffer->getChangedID_Index(); - - if (!updateIndexHardwareBuffer((SHWBufferLink_opengl *)HWBuffer)) - return false; - } - } - return true; } -//! Create hardware buffer from meshbuffer -COpenGL3DriverBase::SHWBufferLink *COpenGL3DriverBase::createHardwareBuffer(const scene::IMeshBuffer *mb) +COpenGL3DriverBase::SHWBufferLink *COpenGL3DriverBase::createHardwareBuffer(const scene::IVertexBuffer *vb) { - if (!mb || (mb->getHardwareMappingHint_Index() == scene::EHM_NEVER && mb->getHardwareMappingHint_Vertex() == scene::EHM_NEVER)) + if (!vb || vb->getHardwareMappingHint() == scene::EHM_NEVER) return 0; - SHWBufferLink_opengl *HWBuffer = new SHWBufferLink_opengl(mb); + SHWBufferLink_opengl *HWBuffer = new SHWBufferLink_opengl(vb); // add to map HWBuffer->listPosition = HWBufferList.insert(HWBufferList.end(), HWBuffer); - HWBuffer->ChangedID_Vertex = HWBuffer->MeshBuffer->getChangedID_Vertex(); - HWBuffer->ChangedID_Index = HWBuffer->MeshBuffer->getChangedID_Index(); - HWBuffer->Mapped_Vertex = mb->getHardwareMappingHint_Vertex(); - HWBuffer->Mapped_Index = mb->getHardwareMappingHint_Index(); - HWBuffer->vbo_verticesID = 0; - HWBuffer->vbo_indicesID = 0; - HWBuffer->vbo_verticesSize = 0; - HWBuffer->vbo_indicesSize = 0; - - if (!updateHardwareBuffer(HWBuffer)) { + if (!updateVertexHardwareBuffer(HWBuffer)) { deleteHardwareBuffer(HWBuffer); return 0; } @@ -643,57 +600,70 @@ COpenGL3DriverBase::SHWBufferLink *COpenGL3DriverBase::createHardwareBuffer(cons return HWBuffer; } -void COpenGL3DriverBase::deleteHardwareBuffer(SHWBufferLink *_HWBuffer) +COpenGL3DriverBase::SHWBufferLink *COpenGL3DriverBase::createHardwareBuffer(const scene::IIndexBuffer *ib) { - if (!_HWBuffer) - return; + if (!ib || ib->getHardwareMappingHint() == scene::EHM_NEVER) + return 0; - SHWBufferLink_opengl *HWBuffer = static_cast(_HWBuffer); - if (HWBuffer->vbo_verticesID) { - GL.DeleteBuffers(1, &HWBuffer->vbo_verticesID); - HWBuffer->vbo_verticesID = 0; - } - if (HWBuffer->vbo_indicesID) { - GL.DeleteBuffers(1, &HWBuffer->vbo_indicesID); - HWBuffer->vbo_indicesID = 0; + SHWBufferLink_opengl *HWBuffer = new SHWBufferLink_opengl(ib); + + // add to map + HWBuffer->listPosition = HWBufferList.insert(HWBufferList.end(), HWBuffer); + + if (!updateIndexHardwareBuffer(HWBuffer)) { + deleteHardwareBuffer(HWBuffer); + return 0; } - CNullDriver::deleteHardwareBuffer(_HWBuffer); + return HWBuffer; } -//! Draw hardware buffer -void COpenGL3DriverBase::drawHardwareBuffer(SHWBufferLink *_HWBuffer) +void COpenGL3DriverBase::deleteHardwareBuffer(SHWBufferLink *HWBuffer) { - if (!_HWBuffer) + if (!HWBuffer) return; - SHWBufferLink_opengl *HWBuffer = static_cast(_HWBuffer); - - updateHardwareBuffer(HWBuffer); // check if update is needed - - const scene::IMeshBuffer *mb = HWBuffer->MeshBuffer; - const void *vertices = mb->getVertices(); - const void *indexList = mb->getIndices(); - - if (HWBuffer->Mapped_Vertex != scene::EHM_NEVER) { - GL.BindBuffer(GL_ARRAY_BUFFER, HWBuffer->vbo_verticesID); - vertices = 0; + auto *b = static_cast(HWBuffer); + if (b->vbo_ID) { + GL.DeleteBuffers(1, &b->vbo_ID); + b->vbo_ID = 0; } - if (HWBuffer->Mapped_Index != scene::EHM_NEVER) { - GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, HWBuffer->vbo_indicesID); - indexList = 0; + CNullDriver::deleteHardwareBuffer(HWBuffer); +} + +void COpenGL3DriverBase::drawBuffers(const scene::IVertexBuffer *vb, + const scene::IIndexBuffer *ib, u32 PrimitiveCount, + scene::E_PRIMITIVE_TYPE PrimitiveType) +{ + if (!vb || !ib) + return; + + auto *hwvert = static_cast(getBufferLink(vb)); + auto *hwidx = static_cast(getBufferLink(ib)); + updateHardwareBuffer(hwvert); + updateHardwareBuffer(hwidx); + + const void *vertices = vb->getData(); + if (hwvert) { + assert(hwvert->IsVertex); + GL.BindBuffer(GL_ARRAY_BUFFER, hwvert->vbo_ID); + vertices = nullptr; } - drawVertexPrimitiveList(vertices, mb->getVertexCount(), - indexList, mb->getPrimitiveCount(), - mb->getVertexType(), mb->getPrimitiveType(), - mb->getIndexType()); + const void *indexList = ib->getData(); + if (hwidx) { + assert(!hwidx->IsVertex); + GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, hwidx->vbo_ID); + indexList = nullptr; + } - if (HWBuffer->Mapped_Vertex != scene::EHM_NEVER) + drawVertexPrimitiveList(vertices, vb->getCount(), indexList, + PrimitiveCount, vb->getType(), PrimitiveType, ib->getType()); + + if (hwvert) GL.BindBuffer(GL_ARRAY_BUFFER, 0); - - if (HWBuffer->Mapped_Index != scene::EHM_NEVER) + if (hwidx) GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); } @@ -916,7 +886,8 @@ void COpenGL3DriverBase::draw2DImageBatch(const video::ITexture *texture, const irr::u32 drawCount = core::min_(positions.size(), sourceRects.size()); assert(6 * drawCount <= QuadIndexCount); // FIXME split the batch? or let it crash? - core::array vtx(drawCount * 4); + std::vector vtx; + vtx.reserve(drawCount * 4); for (u32 i = 0; i < drawCount; i++) { core::position2d targetPos = positions[i]; @@ -939,22 +910,22 @@ void COpenGL3DriverBase::draw2DImageBatch(const video::ITexture *texture, f32 down = 2.f - (f32)poss.LowerRightCorner.Y / (f32)renderTargetSize.Height * 2.f - 1.f; f32 top = 2.f - (f32)poss.UpperLeftCorner.Y / (f32)renderTargetSize.Height * 2.f - 1.f; - vtx.push_back(S3DVertex(left, top, 0.0f, + vtx.emplace_back(left, top, 0.0f, 0.0f, 0.0f, 0.0f, color, - tcoords.UpperLeftCorner.X, tcoords.UpperLeftCorner.Y)); - vtx.push_back(S3DVertex(right, top, 0.0f, + tcoords.UpperLeftCorner.X, tcoords.UpperLeftCorner.Y); + vtx.emplace_back(right, top, 0.0f, 0.0f, 0.0f, 0.0f, color, - tcoords.LowerRightCorner.X, tcoords.UpperLeftCorner.Y)); - vtx.push_back(S3DVertex(right, down, 0.0f, + tcoords.LowerRightCorner.X, tcoords.UpperLeftCorner.Y); + vtx.emplace_back(right, down, 0.0f, 0.0f, 0.0f, 0.0f, color, - tcoords.LowerRightCorner.X, tcoords.LowerRightCorner.Y)); - vtx.push_back(S3DVertex(left, down, 0.0f, + tcoords.LowerRightCorner.X, tcoords.LowerRightCorner.Y); + vtx.emplace_back(left, down, 0.0f, 0.0f, 0.0f, 0.0f, color, - tcoords.UpperLeftCorner.X, tcoords.LowerRightCorner.Y)); + tcoords.UpperLeftCorner.X, tcoords.LowerRightCorner.Y); } GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, QuadIndexBuffer); - drawElements(GL_TRIANGLES, vt2DImage, vtx.const_pointer(), vtx.size(), 0, 6 * drawCount); + drawElements(GL_TRIANGLES, vt2DImage, vtx.data(), vtx.size(), 0, 6 * drawCount); GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); if (clipRect) @@ -1104,15 +1075,14 @@ void COpenGL3DriverBase::endDraw(const VertexType &vertexType) ITexture *COpenGL3DriverBase::createDeviceDependentTexture(const io::path &name, IImage *image) { - core::array imageArray(1); - imageArray.push_back(image); + std::vector tmp { image }; - COpenGL3Texture *texture = new COpenGL3Texture(name, imageArray, ETT_2D, this); + COpenGL3Texture *texture = new COpenGL3Texture(name, tmp, ETT_2D, this); return texture; } -ITexture *COpenGL3DriverBase::createDeviceDependentTextureCubemap(const io::path &name, const core::array &image) +ITexture *COpenGL3DriverBase::createDeviceDependentTextureCubemap(const io::path &name, const std::vector &image) { COpenGL3Texture *texture = new COpenGL3Texture(name, image, ETT_CUBEMAP, this); @@ -1126,8 +1096,11 @@ void COpenGL3DriverBase::setMaterial(const SMaterial &material) OverrideMaterial.apply(Material); for (u32 i = 0; i < Feature.MaxTextureUnits; ++i) { - CacheHandler->getTextureCache().set(i, material.getTexture(i)); - setTransform((E_TRANSFORMATION_STATE)(ETS_TEXTURE_0 + i), material.getTextureMatrix(i)); + auto *texture = material.getTexture(i); + CacheHandler->getTextureCache().set(i, texture); + if (texture) { + setTransform((E_TRANSFORMATION_STATE)(ETS_TEXTURE_0 + i), material.getTextureMatrix(i)); + } } } @@ -1505,10 +1478,8 @@ void COpenGL3DriverBase::chooseMaterial2D() Material = InitMaterial2D; if (OverrideMaterial2DEnabled) { - OverrideMaterial2D.Lighting = false; OverrideMaterial2D.ZWriteEnable = EZW_OFF; OverrideMaterial2D.ZBuffer = ECFN_DISABLED; // it will be ECFN_DISABLED after merge - OverrideMaterial2D.Lighting = false; Material = OverrideMaterial2D; } @@ -1876,40 +1847,6 @@ void COpenGL3DriverBase::removeTexture(ITexture *texture) CNullDriver::removeTexture(texture); } -//! Set/unset a clipping plane. -bool COpenGL3DriverBase::setClipPlane(u32 index, const core::plane3df &plane, bool enable) -{ - if (index >= UserClipPlane.size()) - UserClipPlane.push_back(SUserClipPlane()); - - UserClipPlane[index].Plane = plane; - UserClipPlane[index].Enabled = enable; - return true; -} - -//! Enable/disable a clipping plane. -void COpenGL3DriverBase::enableClipPlane(u32 index, bool enable) -{ - UserClipPlane[index].Enabled = enable; -} - -//! Get the ClipPlane Count -u32 COpenGL3DriverBase::getClipPlaneCount() const -{ - return UserClipPlane.size(); -} - -const core::plane3df &COpenGL3DriverBase::getClipPlane(irr::u32 index) const -{ - if (index < UserClipPlane.size()) - return UserClipPlane[index].Plane; - else { - _IRR_DEBUG_BREAK_IF(true) // invalid index - static const core::plane3df dummy; - return dummy; - } -} - core::dimension2du COpenGL3DriverBase::getMaxTextureSize() const { return core::dimension2du(MaxTextureSize, MaxTextureSize); diff --git a/irr/src/OpenGL/Driver.h b/irr/src/OpenGL/Driver.h index 51554334b..3104b164d 100644 --- a/irr/src/OpenGL/Driver.h +++ b/irr/src/OpenGL/Driver.h @@ -7,7 +7,6 @@ #pragma once #include "SIrrCreationParameters.h" - #include "Common.h" #include "CNullDriver.h" #include "IMaterialRendererServices.h" @@ -47,16 +46,11 @@ public: struct SHWBufferLink_opengl : public SHWBufferLink { - SHWBufferLink_opengl(const scene::IMeshBuffer *meshBuffer) : - SHWBufferLink(meshBuffer), vbo_verticesID(0), vbo_indicesID(0), vbo_verticesSize(0), vbo_indicesSize(0) - { - } + SHWBufferLink_opengl(const scene::IVertexBuffer *vb) : SHWBufferLink(vb) {} + SHWBufferLink_opengl(const scene::IIndexBuffer *ib) : SHWBufferLink(ib) {} - u32 vbo_verticesID; // tmp - u32 vbo_indicesID; // tmp - - u32 vbo_verticesSize; // tmp - u32 vbo_indicesSize; // tmp + GLuint vbo_ID = 0; + u32 vbo_Size = 0; }; bool updateVertexHardwareBuffer(SHWBufferLink_opengl *HWBuffer); @@ -65,14 +59,18 @@ public: //! updates hardware buffer if needed bool updateHardwareBuffer(SHWBufferLink *HWBuffer) override; - //! Create hardware buffer from mesh - SHWBufferLink *createHardwareBuffer(const scene::IMeshBuffer *mb) override; + //! Create hardware buffer from vertex buffer + SHWBufferLink *createHardwareBuffer(const scene::IVertexBuffer *vb) override; + + //! Create hardware buffer from index buffer + SHWBufferLink *createHardwareBuffer(const scene::IIndexBuffer *ib) override; //! Delete hardware buffer (only some drivers can) void deleteHardwareBuffer(SHWBufferLink *HWBuffer) override; - //! Draw hardware buffer - void drawHardwareBuffer(SHWBufferLink *HWBuffer) override; + void drawBuffers(const scene::IVertexBuffer *vb, + const scene::IIndexBuffer *ib, u32 primCount, + scene::E_PRIMITIVE_TYPE pType = scene::EPT_TRIANGLES) override; IRenderTarget *addRenderTarget() override; @@ -227,18 +225,6 @@ public: // Does *nothing* unless in debug mode. bool testGLError(const char *file, int line); - //! Set/unset a clipping plane. - bool setClipPlane(u32 index, const core::plane3df &plane, bool enable = false) override; - - //! returns the current amount of user clip planes set. - u32 getClipPlaneCount() const; - - //! returns the 0 indexed Plane - const core::plane3df &getClipPlane(u32 index) const; - - //! Enable/disable a clipping plane. - void enableClipPlane(u32 index, bool enable) override; - //! Returns the graphics card vendor name. core::stringc getVendorInfo() override { @@ -278,7 +264,7 @@ protected: ITexture *createDeviceDependentTexture(const io::path &name, IImage *image) override; - ITexture *createDeviceDependentTextureCubemap(const io::path &name, const core::array &image) override; + ITexture *createDeviceDependentTextureCubemap(const io::path &name, const std::vector &image) override; //! Map Irrlicht wrap mode to OpenGL enum GLint getTextureWrapMode(u8 clamp) const; @@ -304,10 +290,7 @@ protected: LockRenderStateMode = false; } - void draw2D3DVertexPrimitiveList(const void *vertices, - u32 vertexCount, const void *indexList, u32 primitiveCount, - E_VERTEX_TYPE vType, scene::E_PRIMITIVE_TYPE pType, - E_INDEX_TYPE iType, bool is3D); + bool updateHardwareBuffer(SHWBufferLink_opengl *b, const void *buffer, size_t bufferSize, scene::E_HARDWARE_MAPPING hint); void createMaterialRenderers(); @@ -337,14 +320,6 @@ protected: bool LockRenderStateMode; u8 AntiAlias; - struct SUserClipPlane - { - core::plane3df Plane; - bool Enabled; - }; - - core::array UserClipPlane; - core::matrix4 TextureFlipMatrix; using FColorConverter = void (*)(const void *source, s32 count, void *dest); diff --git a/irr/src/OpenGL/FixedPipelineRenderer.cpp b/irr/src/OpenGL/FixedPipelineRenderer.cpp index 7c4adf719..cef446587 100644 --- a/irr/src/OpenGL/FixedPipelineRenderer.cpp +++ b/irr/src/OpenGL/FixedPipelineRenderer.cpp @@ -4,6 +4,7 @@ // For conditions of distribution and use, see copyright notice in Irrlicht.h #include "FixedPipelineRenderer.h" +#include "os.h" #include "IVideoDriver.h" @@ -15,24 +16,15 @@ namespace video // Base callback COpenGL3MaterialBaseCB::COpenGL3MaterialBaseCB() : - FirstUpdateBase(true), WVPMatrixID(-1), WVMatrixID(-1), NMatrixID(-1), + FirstUpdateBase(true), WVPMatrixID(-1), WVMatrixID(-1), FogEnableID(-1), FogTypeID(-1), FogColorID(-1), FogStartID(-1), - FogEndID(-1), FogDensityID(-1), ThicknessID(-1), LightEnable(false), MaterialAmbient(SColorf(0.f, 0.f, 0.f)), MaterialDiffuse(SColorf(0.f, 0.f, 0.f)), MaterialEmissive(SColorf(0.f, 0.f, 0.f)), MaterialSpecular(SColorf(0.f, 0.f, 0.f)), - MaterialShininess(0.f), FogEnable(0), FogType(1), FogColor(SColorf(0.f, 0.f, 0.f, 1.f)), FogStart(0.f), FogEnd(0.f), FogDensity(0.f), Thickness(1.f) + FogEndID(-1), FogDensityID(-1), ThicknessID(-1), Thickness(1.f), FogEnable(false) { } void COpenGL3MaterialBaseCB::OnSetMaterial(const SMaterial &material) { - LightEnable = material.Lighting; - MaterialAmbient = SColorf(material.AmbientColor); - MaterialDiffuse = SColorf(material.DiffuseColor); - MaterialEmissive = SColorf(material.EmissiveColor); - MaterialSpecular = SColorf(material.SpecularColor); - MaterialShininess = material.Shininess; - - FogEnable = material.FogEnable ? 1 : 0; - + FogEnable = material.FogEnable; Thickness = (material.Thickness > 0.f) ? material.Thickness : 1.f; } @@ -43,7 +35,6 @@ void COpenGL3MaterialBaseCB::OnSetConstants(IMaterialRendererServices *services, if (FirstUpdateBase) { WVPMatrixID = services->getVertexShaderConstantID("uWVPMatrix"); WVMatrixID = services->getVertexShaderConstantID("uWVMatrix"); - NMatrixID = services->getVertexShaderConstantID("uNMatrix"); FogEnableID = services->getVertexShaderConstantID("uFogEnable"); FogTypeID = services->getVertexShaderConstantID("uFogType"); @@ -56,31 +47,29 @@ void COpenGL3MaterialBaseCB::OnSetConstants(IMaterialRendererServices *services, FirstUpdateBase = false; } - const core::matrix4 W = driver->getTransform(ETS_WORLD); - const core::matrix4 V = driver->getTransform(ETS_VIEW); - const core::matrix4 P = driver->getTransform(ETS_PROJECTION); + const core::matrix4 &W = driver->getTransform(ETS_WORLD); + const core::matrix4 &V = driver->getTransform(ETS_VIEW); + const core::matrix4 &P = driver->getTransform(ETS_PROJECTION); - core::matrix4 Matrix = P * V * W; - services->setPixelShaderConstant(WVPMatrixID, Matrix.pointer(), 16); - - Matrix = V * W; + core::matrix4 Matrix = V * W; services->setPixelShaderConstant(WVMatrixID, Matrix.pointer(), 16); - Matrix.makeInverse(); - services->setPixelShaderConstant(NMatrixID, Matrix.getTransposed().pointer(), 16); + Matrix = P * Matrix; + services->setPixelShaderConstant(WVPMatrixID, Matrix.pointer(), 16); - services->setPixelShaderConstant(FogEnableID, &FogEnable, 1); + s32 TempEnable = FogEnable ? 1 : 0; + services->setPixelShaderConstant(FogEnableID, &TempEnable, 1); if (FogEnable) { SColor TempColor(0); E_FOG_TYPE TempType = EFT_FOG_LINEAR; - bool TempPerFragment = false; - bool TempRange = false; + f32 FogStart, FogEnd, FogDensity; + bool unused = false; - driver->getFog(TempColor, TempType, FogStart, FogEnd, FogDensity, TempPerFragment, TempRange); + driver->getFog(TempColor, TempType, FogStart, FogEnd, FogDensity, unused, unused); - FogType = (s32)TempType; - FogColor = SColorf(TempColor); + s32 FogType = (s32)TempType; + SColorf FogColor(TempColor); services->setPixelShaderConstant(FogTypeID, &FogType, 1); services->setPixelShaderConstant(FogColorID, reinterpret_cast(&FogColor), 4); diff --git a/irr/src/OpenGL/FixedPipelineRenderer.h b/irr/src/OpenGL/FixedPipelineRenderer.h index a07af1df9..89e251a35 100644 --- a/irr/src/OpenGL/FixedPipelineRenderer.h +++ b/irr/src/OpenGL/FixedPipelineRenderer.h @@ -26,7 +26,6 @@ protected: s32 WVPMatrixID; s32 WVMatrixID; - s32 NMatrixID; s32 FogEnableID; s32 FogTypeID; @@ -37,22 +36,8 @@ protected: s32 ThicknessID; - bool LightEnable; - SColorf GlobalAmbient; - SColorf MaterialAmbient; - SColorf MaterialDiffuse; - SColorf MaterialEmissive; - SColorf MaterialSpecular; - f32 MaterialShininess; - - s32 FogEnable; - s32 FogType; - SColorf FogColor; - f32 FogStart; - f32 FogEnd; - f32 FogDensity; - f32 Thickness; + bool FogEnable; }; class COpenGL3MaterialSolidCB : public COpenGL3MaterialBaseCB diff --git a/irr/src/OpenGL/MaterialRenderer.cpp b/irr/src/OpenGL/MaterialRenderer.cpp index 51a796d48..3c32ba318 100644 --- a/irr/src/OpenGL/MaterialRenderer.cpp +++ b/irr/src/OpenGL/MaterialRenderer.cpp @@ -262,35 +262,30 @@ bool COpenGL3MaterialRenderer::linkProgram() // seems that some implementations use an extra null terminator. ++maxlen; - c8 *buf = new c8[maxlen]; + std::vector buf(maxlen); UniformInfo.clear(); - UniformInfo.reallocate(num); + UniformInfo.reserve(num); for (GLint i = 0; i < num; ++i) { SUniformInfo ui; - memset(buf, 0, maxlen); + memset(buf.data(), 0, buf.size()); GLint size; - GL.GetActiveUniform(Program, i, maxlen, 0, &size, &ui.type, reinterpret_cast(buf)); - - core::stringc name = ""; + GL.GetActiveUniform(Program, i, maxlen, 0, &size, &ui.type, reinterpret_cast(buf.data())); // array support, workaround for some bugged drivers. for (s32 i = 0; i < maxlen; ++i) { if (buf[i] == '[' || buf[i] == '\0') break; - name += buf[i]; + ui.name += buf[i]; } - ui.name = name; - ui.location = GL.GetUniformLocation(Program, buf); + ui.location = GL.GetUniformLocation(Program, buf.data()); - UniformInfo.push_back(ui); + UniformInfo.push_back(std::move(ui)); } - - delete[] buf; } return true; diff --git a/irr/src/OpenGL/MaterialRenderer.h b/irr/src/OpenGL/MaterialRenderer.h index fa37cfff3..fca71478a 100644 --- a/irr/src/OpenGL/MaterialRenderer.h +++ b/irr/src/OpenGL/MaterialRenderer.h @@ -4,12 +4,12 @@ #pragma once +#include +#include #include "EMaterialTypes.h" #include "IMaterialRenderer.h" #include "IMaterialRendererServices.h" #include "IGPUProgrammingServices.h" -#include "irrArray.h" -#include "irrString.h" #include "Common.h" @@ -79,13 +79,13 @@ protected: struct SUniformInfo { - core::stringc name; + std::string name; GLenum type; GLint location; }; GLuint Program; - core::array UniformInfo; + std::vector UniformInfo; s32 UserData; }; diff --git a/irr/src/os.h b/irr/src/os.h index 58699ab3e..bcc096f95 100644 --- a/irr/src/os.h +++ b/irr/src/os.h @@ -52,7 +52,6 @@ public: // prints out a string to the console out stdout or debug log or whatever static void print(const c8 *message, ELOG_LEVEL ll = ELL_INFORMATION); static void log(const c8 *message, ELOG_LEVEL ll = ELL_INFORMATION); - static void log(const wchar_t *message, ELOG_LEVEL ll = ELL_INFORMATION); // The string ": " is added between message and hint static void log(const c8 *message, const c8 *hint, ELOG_LEVEL ll = ELL_INFORMATION); diff --git a/irr/test/CMakeLists.txt b/irr/test/CMakeLists.txt deleted file mode 100644 index 6518f69c1..000000000 --- a/irr/test/CMakeLists.txt +++ /dev/null @@ -1,32 +0,0 @@ -link_libraries(IrrlichtMt::IrrlichtMt) -add_executable(image_loader_test image_loader_test.cpp) - -function(test_image_loader format expected input) - string(TOLOWER ${format} suffix) - add_test(NAME ImageLoader${format}-${input} COMMAND image_loader_test ${expected} data/sample_${input}.${suffix} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) -endfunction() - -test_image_loader(BMP 16color-16bpp 4bpp_v3) -# test_image_loader(BMP 16color-16bpp 4bpp_v7) -# test_image_loader(BMP 16color-16bpp 4bpp_v3_rle) -# test_image_loader(BMP 16color-16bpp 4bpp_v7_rle) -test_image_loader(BMP 30color-16bpp 8bpp_v3) -# test_image_loader(BMP 30color-16bpp 8bpp_v7) -test_image_loader(BMP 30color-16bpp 8bpp_v3_rle) -# test_image_loader(BMP 30color-16bpp 8bpp_v7_rle) -# test_image_loader(BMP 30color-16bpp 16bpp_v3) -# test_image_loader(BMP 30color-16bpp 16bpp_v7) -test_image_loader(BMP 30color-24bpp 24bpp_v3) -test_image_loader(BMP 30color-24bpp 24bpp_v7) - -test_image_loader(PNG 30color-24bpp 8bpp) -test_image_loader(PNG 30color-24bpp 24bpp) - -test_image_loader(TGA 30color-32bpp 8bpp_up) -test_image_loader(TGA 30color-32bpp 8bpp_down) -# test_image_loader(TGA 30color-16bpp 8bpp_rle_up) -# test_image_loader(TGA 30color-16bpp 8bpp_rle_down) -test_image_loader(TGA 30color-24bpp 24bpp_up) -test_image_loader(TGA 30color-24bpp 24bpp_down) -test_image_loader(TGA 30color-24bpp 24bpp_rle_up) -test_image_loader(TGA 30color-24bpp 24bpp_rle_down) diff --git a/irr/test/data/sample_16bpp_v3.bmp b/irr/test/data/sample_16bpp_v3.bmp deleted file mode 100644 index 4b6fbcf50..000000000 Binary files a/irr/test/data/sample_16bpp_v3.bmp and /dev/null differ diff --git a/irr/test/data/sample_16bpp_v7.bmp b/irr/test/data/sample_16bpp_v7.bmp deleted file mode 100644 index 7a0b8a6ed..000000000 Binary files a/irr/test/data/sample_16bpp_v7.bmp and /dev/null differ diff --git a/irr/test/data/sample_24bpp.png b/irr/test/data/sample_24bpp.png deleted file mode 100644 index d38f5989c..000000000 Binary files a/irr/test/data/sample_24bpp.png and /dev/null differ diff --git a/irr/test/data/sample_24bpp_down.tga b/irr/test/data/sample_24bpp_down.tga deleted file mode 100644 index 031f88282..000000000 Binary files a/irr/test/data/sample_24bpp_down.tga and /dev/null differ diff --git a/irr/test/data/sample_24bpp_rle_down.tga b/irr/test/data/sample_24bpp_rle_down.tga deleted file mode 100644 index 24e765e19..000000000 Binary files a/irr/test/data/sample_24bpp_rle_down.tga and /dev/null differ diff --git a/irr/test/data/sample_24bpp_rle_up.tga b/irr/test/data/sample_24bpp_rle_up.tga deleted file mode 100644 index 4a5f9604d..000000000 Binary files a/irr/test/data/sample_24bpp_rle_up.tga and /dev/null differ diff --git a/irr/test/data/sample_24bpp_up.tga b/irr/test/data/sample_24bpp_up.tga deleted file mode 100644 index 1f1edc6e9..000000000 Binary files a/irr/test/data/sample_24bpp_up.tga and /dev/null differ diff --git a/irr/test/data/sample_24bpp_v3.bmp b/irr/test/data/sample_24bpp_v3.bmp deleted file mode 100644 index 4684f1466..000000000 Binary files a/irr/test/data/sample_24bpp_v3.bmp and /dev/null differ diff --git a/irr/test/data/sample_24bpp_v7.bmp b/irr/test/data/sample_24bpp_v7.bmp deleted file mode 100644 index 1fa343188..000000000 Binary files a/irr/test/data/sample_24bpp_v7.bmp and /dev/null differ diff --git a/irr/test/data/sample_4bpp_v3.bmp b/irr/test/data/sample_4bpp_v3.bmp deleted file mode 100644 index 0cc06f316..000000000 Binary files a/irr/test/data/sample_4bpp_v3.bmp and /dev/null differ diff --git a/irr/test/data/sample_8bpp.png b/irr/test/data/sample_8bpp.png deleted file mode 100644 index c81f816f4..000000000 Binary files a/irr/test/data/sample_8bpp.png and /dev/null differ diff --git a/irr/test/data/sample_8bpp_down.tga b/irr/test/data/sample_8bpp_down.tga deleted file mode 100644 index e46cad554..000000000 Binary files a/irr/test/data/sample_8bpp_down.tga and /dev/null differ diff --git a/irr/test/data/sample_8bpp_rle_down.tga b/irr/test/data/sample_8bpp_rle_down.tga deleted file mode 100644 index 3d34bff4c..000000000 Binary files a/irr/test/data/sample_8bpp_rle_down.tga and /dev/null differ diff --git a/irr/test/data/sample_8bpp_rle_up.tga b/irr/test/data/sample_8bpp_rle_up.tga deleted file mode 100644 index 0372bc34c..000000000 Binary files a/irr/test/data/sample_8bpp_rle_up.tga and /dev/null differ diff --git a/irr/test/data/sample_8bpp_up.tga b/irr/test/data/sample_8bpp_up.tga deleted file mode 100644 index a314e8689..000000000 Binary files a/irr/test/data/sample_8bpp_up.tga and /dev/null differ diff --git a/irr/test/data/sample_8bpp_v3.bmp b/irr/test/data/sample_8bpp_v3.bmp deleted file mode 100644 index a890e6465..000000000 Binary files a/irr/test/data/sample_8bpp_v3.bmp and /dev/null differ diff --git a/irr/test/data/sample_8bpp_v3_rle.bmp b/irr/test/data/sample_8bpp_v3_rle.bmp deleted file mode 100644 index d460d0fd6..000000000 Binary files a/irr/test/data/sample_8bpp_v3_rle.bmp and /dev/null differ diff --git a/irr/test/data/sample_8bpp_v7.bmp b/irr/test/data/sample_8bpp_v7.bmp deleted file mode 100644 index a31a342c2..000000000 Binary files a/irr/test/data/sample_8bpp_v7.bmp and /dev/null differ diff --git a/irr/test/data/sample_8bpp_v7_rle.bmp b/irr/test/data/sample_8bpp_v7_rle.bmp deleted file mode 100644 index d197b09a6..000000000 Binary files a/irr/test/data/sample_8bpp_v7_rle.bmp and /dev/null differ diff --git a/irr/test/image_loader_test.cpp b/irr/test/image_loader_test.cpp deleted file mode 100644 index 1dd4888f2..000000000 --- a/irr/test/image_loader_test.cpp +++ /dev/null @@ -1,162 +0,0 @@ -#include -#include -#include -#include -#include - -using namespace irr; - -struct ImageDesc -{ - const char *name; - video::ECOLOR_FORMAT format; - std::vector data; -}; - -template -std::vector to_byte_array(std::initializer_list in_data) -{ - std::vector data; - data.resize(sizeof(T) * in_data.size()); - memcpy(data.data(), in_data.begin(), data.size()); - return data; -} - -static const ImageDesc test_images[] = { - {"16color-16bpp", video::ECF_A1R5G5B5, to_byte_array({ - 0xb6df, 0xb6df, 0xb6df, 0xb6df, 0xc23f, 0xc23f, 0xd61f, 0xd61f, 0xea3f, 0xea3f, 0xfe5e, 0xfe5e, 0xfe7c, 0xfe7a, 0xfe7a, 0xfe97, 0xfeb5, 0xfeb5, 0xff16, - 0xb6df, 0xb6df, 0xff3f, 0xff3f, 0xc23f, 0xf2df, 0xf2df, 0xf2df, 0xea3f, 0xda7f, 0xda7f, 0xfe5e, 0xfe7a, 0xc23f, 0xbe7f, 0xbe7f, 0xfeb5, 0xfeb5, 0xfb76, - 0xb6df, 0xff9e, 0xc23f, 0xc23f, 0xc23f, 0xf2df, 0xd61f, 0xf2df, 0xea3f, 0xda7f, 0xfe5e, 0xda7f, 0xfe7a, 0xc23f, 0xfe97, 0xfeb5, 0xfeb5, 0xff16, 0xfb76, - 0xb6df, 0xff9e, 0xbe7f, 0xc23f, 0xd61f, 0xfe7c, 0xd61f, 0xf2df, 0xfe5e, 0xf2df, 0xfe7c, 0xda7f, 0xfe7a, 0xda7f, 0xbe7f, 0xfeb5, 0xff16, 0xff16, 0xfb76, - 0xb6df, 0xff9e, 0xc23f, 0xc23f, 0xd61f, 0xff3f, 0xd61f, 0xf2df, 0xfe5e, 0xda7f, 0xfe7c, 0xda7f, 0xfe7a, 0xc23f, 0xfeb5, 0xfeb5, 0xff16, 0xff16, 0xfb76, - 0xb6df, 0xbe7f, 0xff9e, 0xff3f, 0xd61f, 0xff3f, 0xf2df, 0xf2df, 0xfe5e, 0xf2df, 0xda7f, 0xfe97, 0xfe97, 0xda7f, 0xc23f, 0xda7f, 0xfb76, 0xfb76, 0xfb76, - 0xbe7f, 0xc23f, 0xc23f, 0xd61f, 0xd61f, 0xea3f, 0xea3f, 0xfe5e, 0xfe5e, 0xfe7c, 0xfe7c, 0xfe7a, 0xfe97, 0xfeb5, 0xfeb5, 0xfeb5, 0xfb76, 0xfb76, 0xfb76, - 0xbe7f, 0xc23f, 0xd61f, 0xd61f, 0xd61f, 0xea3f, 0xea3f, 0xfe5e, 0xfe5e, 0xfe7c, 0xfe97, 0xfe97, 0xfe97, 0xfeb5, 0xff16, 0xff16, 0xfb76, 0xfb76, 0xfb76, - 0xc23f, 0xff9e, 0xc23f, 0xd61f, 0xff3f, 0xea3f, 0xfe5e, 0xff3f, 0xfe7c, 0xfe7c, 0xf2df, 0xfe97, 0xfeb5, 0xfeb5, 0xda7f, 0xff16, 0xc23f, 0xc23f, 0xbe7f, - 0xc23f, 0xff9e, 0xff9e, 0xd61f, 0xff9e, 0xea3f, 0xff3f, 0xfe5e, 0xff3f, 0xfe7a, 0xf2df, 0xf2df, 0xfeb5, 0xf2df, 0xda7f, 0xfb76, 0xc23f, 0xfb76, 0xfb76, - 0xc23f, 0xff9e, 0xd61f, 0xff9e, 0xff9e, 0xfe5e, 0xff3f, 0xff3f, 0xff3f, 0xfe7a, 0xff3f, 0xfeb5, 0xf2df, 0xff16, 0xda7f, 0xfb76, 0xda7f, 0xda7f, 0xfb76, - 0xc23f, 0xff9e, 0xea3f, 0xea3f, 0xff9e, 0xfe5e, 0xff9e, 0xfe7c, 0xff3f, 0xfe97, 0xf2df, 0xfeb5, 0xfeb5, 0xff16, 0xf2df, 0xfb76, 0xda7f, 0xfb76, 0xfb76, - 0xd61f, 0xff9e, 0xd61f, 0xea3f, 0xff9e, 0xfe5e, 0xff9e, 0xfe7c, 0xff3f, 0xfe97, 0xff3f, 0xfeb5, 0xff16, 0xfb76, 0xda7f, 0xfb76, 0xda7f, 0xda7f, 0xc23f, - 0xd61f, 0xd61f, 0xea3f, 0xea3f, 0xea3f, 0xfe5e, 0xfe7c, 0xfe7a, 0xfe7a, 0xfe97, 0xfeb5, 0xff16, 0xff16, 0xfb76, 0xfb76, 0xfb76, 0xfb76, 0xfb76, 0xfb76, - 0xd61f, 0xd61f, 0xea3f, 0xfe5e, 0xfe5e, 0xfe7c, 0xfe7a, 0xfe7a, 0xfe97, 0xfeb5, 0xfeb5, 0xff16, 0xff16, 0xfb76, 0xfb76, 0xfb76, 0xfb76, 0xfb76, 0xfb76, - })}, - - {"30color-16bpp", video::ECF_A1R5G5B5, to_byte_array({ - 0xbabf, 0xbabf, 0xbabf, 0xbabf, 0xbe3f, 0xbdff, 0xc9ff, 0xda1f, 0xee3f, 0xee3f, 0xfa5f, 0xfe5e, 0xfe7c, 0xfe7b, 0xfe79, 0xfe97, 0xfeb5, 0xfed5, 0xff16, - 0xbabf, 0xbabf, 0xff1f, 0xff1f, 0xbdff, 0xfaff, 0xf2df, 0xeabf, 0xee3f, 0xe27f, 0xda7f, 0xfe5e, 0xfe7b, 0xc25f, 0xbe7f, 0xbe7f, 0xfeb5, 0xfed5, 0xff56, - 0xbabf, 0xff7e, 0xbe7f, 0xc25f, 0xbdff, 0xfaff, 0xda1f, 0xf2df, 0xee3f, 0xda7f, 0xfe5e, 0xce5f, 0xfe79, 0xce5f, 0xfe97, 0xfeb5, 0xfed5, 0xff16, 0xff56, - 0xbabf, 0xff7e, 0xbe7f, 0xbdff, 0xc9ff, 0xfaff, 0xda1f, 0xf2df, 0xfa5f, 0xeabf, 0xfe5e, 0xda7f, 0xfe79, 0xce5f, 0xc25f, 0xfeb5, 0xff16, 0xff16, 0xff76, - 0xbabf, 0xff7e, 0xc25f, 0xbdff, 0xc9ff, 0xff1f, 0xda1f, 0xf2df, 0xfa5f, 0xeabf, 0xfe7c, 0xda7f, 0xfe79, 0xce5f, 0xfeb5, 0xfed5, 0xff16, 0xff56, 0xff97, - 0xbabf, 0xbe7f, 0xff7e, 0xff5e, 0xda1f, 0xff1f, 0xfaff, 0xfaff, 0xfa5f, 0xf2df, 0xeabf, 0xfe79, 0xfe97, 0xda7f, 0xce5f, 0xce5f, 0xff56, 0xff76, 0xff97, - 0xbe7f, 0xc25f, 0xbdff, 0xc9ff, 0xda1f, 0xda1f, 0xee3f, 0xfa5f, 0xfa5f, 0xfe7c, 0xfe7b, 0xfe79, 0xfe97, 0xfeb5, 0xfed5, 0xfed5, 0xff76, 0xff76, 0xff97, - 0xbe7f, 0xbe3f, 0xc9ff, 0xc9ff, 0xda1f, 0xee3f, 0xee3f, 0xfa5f, 0xfe5e, 0xfe7c, 0xfe79, 0xfe97, 0xfe97, 0xfeb5, 0xff16, 0xff16, 0xff76, 0xff97, 0xff97, - 0xbe3f, 0xffbe, 0xc9ff, 0xda1f, 0xff5e, 0xee3f, 0xfa5f, 0xff1f, 0xfe7c, 0xfe7b, 0xf2df, 0xfe97, 0xfeb5, 0xfeb5, 0xda7f, 0xff76, 0xce5f, 0xbe3f, 0xc25f, - 0xbe3f, 0xffbe, 0xffbe, 0xda1f, 0xff7e, 0xfa5f, 0xff5e, 0xfe5e, 0xff1f, 0xfe79, 0xf2df, 0xf2df, 0xfeb5, 0xeabf, 0xda7f, 0xff76, 0xce5f, 0xff97, 0xff97, - 0xbdff, 0xffbe, 0xda1f, 0xff7e, 0xff7e, 0xfa5f, 0xff5e, 0xff1f, 0xff1f, 0xfe79, 0xfaff, 0xfeb5, 0xf2df, 0xff16, 0xe27f, 0xff97, 0xda7f, 0xce5f, 0xff97, - 0xbdff, 0xffbe, 0xda1f, 0xee3f, 0xffbe, 0xfa5f, 0xff7e, 0xfe7b, 0xff1f, 0xfe97, 0xfaff, 0xfeb5, 0xfed5, 0xff56, 0xeabf, 0xff97, 0xda7f, 0xff97, 0xff97, - 0xc9ff, 0xffbe, 0xda1f, 0xee3f, 0xff7e, 0xfa5f, 0xff7e, 0xfe7c, 0xff5e, 0xfe97, 0xfaff, 0xfed5, 0xff16, 0xff56, 0xeabf, 0xff97, 0xda7f, 0xda7f, 0xce5f, - 0xda1f, 0xda1f, 0xee3f, 0xee3f, 0xfa5f, 0xfe5e, 0xfe7c, 0xfe79, 0xfe79, 0xfe97, 0xfeb5, 0xfed5, 0xff16, 0xff56, 0xff97, 0xff97, 0xff97, 0xff97, 0xff97, - 0xda1f, 0xda1f, 0xee3f, 0xfa5f, 0xfe5e, 0xfe7c, 0xfe7b, 0xfe79, 0xfe97, 0xfeb5, 0xfed5, 0xff16, 0xff56, 0xff97, 0xff97, 0xff97, 0xff97, 0xff97, 0xff97, - })}, - - {"30color-24bpp", video::ECF_R8G8B8, { - 0x71, 0xaf, 0xff, 0x71, 0xaf, 0xff, 0x71, 0xaf, 0xff, 0x71, 0xaf, 0xff, 0x7b, 0x8b, 0xff, 0x7a, 0x7f, 0xff, 0x90, 0x7f, 0xff, 0xb6, 0x85, 0xff, 0xd9, 0x8c, 0xff, 0xd9, 0x8c, 0xff, 0xf4, 0x92, 0xfe, 0xfe, 0x96, 0xf0, 0xfd, 0x99, 0xe3, 0xfd, 0x9b, 0xda, 0xfc, 0x9f, 0xca, 0xfc, 0xa4, 0xbc, 0xfb, 0xa9, 0xab, 0xfa, 0xb6, 0xad, 0xf9, 0xc6, 0xb1, - 0x71, 0xaf, 0xff, 0x71, 0xaf, 0xff, 0xff, 0xc6, 0xfc, 0xff, 0xc6, 0xfc, 0x7a, 0x7f, 0xff, 0xf7, 0xbe, 0xff, 0xe4, 0xb2, 0xfe, 0xd4, 0xa9, 0xff, 0xd9, 0x8c, 0xff, 0xc3, 0x98, 0xff, 0xb2, 0x9a, 0xff, 0xfe, 0x96, 0xf0, 0xfd, 0x9b, 0xda, 0x86, 0x93, 0xfe, 0x78, 0x9b, 0xff, 0x78, 0x9b, 0xff, 0xfb, 0xa9, 0xab, 0xfa, 0xb6, 0xad, 0xf8, 0xd2, 0xb3, - 0x71, 0xaf, 0xff, 0xfe, 0xdc, 0xf3, 0x78, 0x9b, 0xff, 0x86, 0x93, 0xfe, 0x7a, 0x7f, 0xff, 0xf7, 0xbe, 0xff, 0xb6, 0x85, 0xff, 0xe4, 0xb2, 0xfe, 0xd9, 0x8c, 0xff, 0xb2, 0x9a, 0xff, 0xfe, 0x96, 0xf0, 0x9a, 0x91, 0xff, 0xfc, 0x9f, 0xca, 0x9a, 0x91, 0xff, 0xfc, 0xa4, 0xbc, 0xfb, 0xa9, 0xab, 0xfa, 0xb6, 0xad, 0xf9, 0xc6, 0xb1, 0xf8, 0xd2, 0xb3, - 0x71, 0xaf, 0xff, 0xfe, 0xdc, 0xf3, 0x78, 0x9b, 0xff, 0x7a, 0x7f, 0xff, 0x90, 0x7f, 0xff, 0xf7, 0xbe, 0xff, 0xb6, 0x85, 0xff, 0xe4, 0xb2, 0xfe, 0xf4, 0x92, 0xfe, 0xd4, 0xa9, 0xff, 0xfe, 0x96, 0xf0, 0xb2, 0x9a, 0xff, 0xfc, 0x9f, 0xca, 0x9a, 0x91, 0xff, 0x86, 0x93, 0xfe, 0xfb, 0xa9, 0xab, 0xf9, 0xc6, 0xb1, 0xf9, 0xc6, 0xb1, 0xf8, 0xd8, 0xb5, - 0x71, 0xaf, 0xff, 0xfe, 0xdc, 0xf3, 0x86, 0x93, 0xfe, 0x7a, 0x7f, 0xff, 0x90, 0x7f, 0xff, 0xff, 0xc6, 0xfc, 0xb6, 0x85, 0xff, 0xe4, 0xb2, 0xfe, 0xf4, 0x92, 0xfe, 0xd4, 0xa9, 0xff, 0xfd, 0x99, 0xe3, 0xb2, 0x9a, 0xff, 0xfc, 0x9f, 0xca, 0x9a, 0x91, 0xff, 0xfb, 0xa9, 0xab, 0xfa, 0xb6, 0xad, 0xf9, 0xc6, 0xb1, 0xf8, 0xd2, 0xb3, 0xf8, 0xe4, 0xb9, - 0x71, 0xaf, 0xff, 0x78, 0x9b, 0xff, 0xfe, 0xdc, 0xf3, 0xff, 0xd0, 0xf7, 0xb6, 0x85, 0xff, 0xff, 0xc6, 0xfc, 0xf7, 0xbe, 0xff, 0xf7, 0xbe, 0xff, 0xf4, 0x92, 0xfe, 0xe4, 0xb2, 0xfe, 0xd4, 0xa9, 0xff, 0xfc, 0x9f, 0xca, 0xfc, 0xa4, 0xbc, 0xb2, 0x9a, 0xff, 0x9a, 0x91, 0xff, 0x9a, 0x91, 0xff, 0xf8, 0xd2, 0xb3, 0xf8, 0xd8, 0xb5, 0xf8, 0xe4, 0xb9, - 0x78, 0x9b, 0xff, 0x86, 0x93, 0xfe, 0x7a, 0x7f, 0xff, 0x90, 0x7f, 0xff, 0xb6, 0x85, 0xff, 0xb6, 0x85, 0xff, 0xd9, 0x8c, 0xff, 0xf4, 0x92, 0xfe, 0xf4, 0x92, 0xfe, 0xfd, 0x99, 0xe3, 0xfd, 0x9b, 0xda, 0xfc, 0x9f, 0xca, 0xfc, 0xa4, 0xbc, 0xfb, 0xa9, 0xab, 0xfa, 0xb6, 0xad, 0xfa, 0xb6, 0xad, 0xf8, 0xd8, 0xb5, 0xf8, 0xd8, 0xb5, 0xf8, 0xe4, 0xb9, - 0x78, 0x9b, 0xff, 0x7b, 0x8b, 0xff, 0x90, 0x7f, 0xff, 0x90, 0x7f, 0xff, 0xb6, 0x85, 0xff, 0xd9, 0x8c, 0xff, 0xd9, 0x8c, 0xff, 0xf4, 0x92, 0xfe, 0xfe, 0x96, 0xf0, 0xfd, 0x99, 0xe3, 0xfc, 0x9f, 0xca, 0xfc, 0xa4, 0xbc, 0xfc, 0xa4, 0xbc, 0xfb, 0xa9, 0xab, 0xf9, 0xc6, 0xb1, 0xf9, 0xc6, 0xb1, 0xf8, 0xd8, 0xb5, 0xf8, 0xe4, 0xb9, 0xf8, 0xe4, 0xb9, - 0x7b, 0x8b, 0xff, 0xff, 0xeb, 0xf2, 0x90, 0x7f, 0xff, 0xb6, 0x85, 0xff, 0xff, 0xd0, 0xf7, 0xd9, 0x8c, 0xff, 0xf4, 0x92, 0xfe, 0xff, 0xc6, 0xfc, 0xfd, 0x99, 0xe3, 0xfd, 0x9b, 0xda, 0xe4, 0xb2, 0xfe, 0xfc, 0xa4, 0xbc, 0xfb, 0xa9, 0xab, 0xfb, 0xa9, 0xab, 0xb2, 0x9a, 0xff, 0xf8, 0xd8, 0xb5, 0x9a, 0x91, 0xff, 0x7b, 0x8b, 0xff, 0x86, 0x93, 0xfe, - 0x7b, 0x8b, 0xff, 0xff, 0xeb, 0xf2, 0xff, 0xeb, 0xf2, 0xb6, 0x85, 0xff, 0xfe, 0xdc, 0xf3, 0xf4, 0x92, 0xfe, 0xff, 0xd0, 0xf7, 0xfe, 0x96, 0xf0, 0xff, 0xc6, 0xfc, 0xfc, 0x9f, 0xca, 0xe4, 0xb2, 0xfe, 0xe4, 0xb2, 0xfe, 0xfb, 0xa9, 0xab, 0xd4, 0xa9, 0xff, 0xb2, 0x9a, 0xff, 0xf8, 0xd8, 0xb5, 0x9a, 0x91, 0xff, 0xf8, 0xe4, 0xb9, 0xf8, 0xe4, 0xb9, - 0x7a, 0x7f, 0xff, 0xff, 0xeb, 0xf2, 0xb6, 0x85, 0xff, 0xfe, 0xdc, 0xf3, 0xfe, 0xdc, 0xf3, 0xf4, 0x92, 0xfe, 0xff, 0xd0, 0xf7, 0xff, 0xc6, 0xfc, 0xff, 0xc6, 0xfc, 0xfc, 0x9f, 0xca, 0xf7, 0xbe, 0xff, 0xfb, 0xa9, 0xab, 0xe4, 0xb2, 0xfe, 0xf9, 0xc6, 0xb1, 0xc3, 0x98, 0xff, 0xf8, 0xe4, 0xb9, 0xb2, 0x9a, 0xff, 0x9a, 0x91, 0xff, 0xf8, 0xe4, 0xb9, - 0x7a, 0x7f, 0xff, 0xff, 0xeb, 0xf2, 0xb6, 0x85, 0xff, 0xd9, 0x8c, 0xff, 0xff, 0xeb, 0xf2, 0xf4, 0x92, 0xfe, 0xfe, 0xdc, 0xf3, 0xfd, 0x9b, 0xda, 0xff, 0xc6, 0xfc, 0xfc, 0xa4, 0xbc, 0xf7, 0xbe, 0xff, 0xfb, 0xa9, 0xab, 0xfa, 0xb6, 0xad, 0xf8, 0xd2, 0xb3, 0xd4, 0xa9, 0xff, 0xf8, 0xe4, 0xb9, 0xb2, 0x9a, 0xff, 0xf8, 0xe4, 0xb9, 0xf8, 0xe4, 0xb9, - 0x90, 0x7f, 0xff, 0xff, 0xeb, 0xf2, 0xb6, 0x85, 0xff, 0xd9, 0x8c, 0xff, 0xfe, 0xdc, 0xf3, 0xf4, 0x92, 0xfe, 0xfe, 0xdc, 0xf3, 0xfd, 0x99, 0xe3, 0xff, 0xd0, 0xf7, 0xfc, 0xa4, 0xbc, 0xf7, 0xbe, 0xff, 0xfa, 0xb6, 0xad, 0xf9, 0xc6, 0xb1, 0xf8, 0xd2, 0xb3, 0xd4, 0xa9, 0xff, 0xf8, 0xe4, 0xb9, 0xb2, 0x9a, 0xff, 0xb2, 0x9a, 0xff, 0x9a, 0x91, 0xff, - 0xb6, 0x85, 0xff, 0xb6, 0x85, 0xff, 0xd9, 0x8c, 0xff, 0xd9, 0x8c, 0xff, 0xf4, 0x92, 0xfe, 0xfe, 0x96, 0xf0, 0xfd, 0x99, 0xe3, 0xfc, 0x9f, 0xca, 0xfc, 0x9f, 0xca, 0xfc, 0xa4, 0xbc, 0xfb, 0xa9, 0xab, 0xfa, 0xb6, 0xad, 0xf9, 0xc6, 0xb1, 0xf8, 0xd2, 0xb3, 0xf8, 0xe4, 0xb9, 0xf8, 0xe4, 0xb9, 0xf8, 0xe4, 0xb9, 0xf8, 0xe4, 0xb9, 0xf8, 0xe4, 0xb9, - 0xb6, 0x85, 0xff, 0xb6, 0x85, 0xff, 0xd9, 0x8c, 0xff, 0xf4, 0x92, 0xfe, 0xfe, 0x96, 0xf0, 0xfd, 0x99, 0xe3, 0xfd, 0x9b, 0xda, 0xfc, 0x9f, 0xca, 0xfc, 0xa4, 0xbc, 0xfb, 0xa9, 0xab, 0xfa, 0xb6, 0xad, 0xf9, 0xc6, 0xb1, 0xf8, 0xd2, 0xb3, 0xf8, 0xe4, 0xb9, 0xf8, 0xe4, 0xb9, 0xf8, 0xe4, 0xb9, 0xf8, 0xe4, 0xb9, 0xf8, 0xe4, 0xb9, 0xf8, 0xe4, 0xb9, - }}, - - {"30color-32bpp", video::ECF_A8R8G8B8, to_byte_array({ - 0xff71afff, 0xff71afff, 0xff71afff, 0xff71afff, 0xff7b8bff, 0xff7a7fff, 0xff907fff, 0xffb685ff, 0xffd98cff, 0xffd98cff, 0xfff492fe, 0xfffe96f0, 0xfffd99e3, 0xfffd9bda, 0xfffc9fca, 0xfffca4bc, 0xfffba9ab, 0xfffab6ad, 0xfff9c6b1, - 0xff71afff, 0xff71afff, 0xffffc6fc, 0xffffc6fc, 0xff7a7fff, 0xfff7beff, 0xffe4b2fe, 0xffd4a9ff, 0xffd98cff, 0xffc398ff, 0xffb29aff, 0xfffe96f0, 0xfffd9bda, 0xff8693fe, 0xff789bff, 0xff789bff, 0xfffba9ab, 0xfffab6ad, 0xfff8d2b3, - 0xff71afff, 0xfffedcf3, 0xff789bff, 0xff8693fe, 0xff7a7fff, 0xfff7beff, 0xffb685ff, 0xffe4b2fe, 0xffd98cff, 0xffb29aff, 0xfffe96f0, 0xff9a91ff, 0xfffc9fca, 0xff9a91ff, 0xfffca4bc, 0xfffba9ab, 0xfffab6ad, 0xfff9c6b1, 0xfff8d2b3, - 0xff71afff, 0xfffedcf3, 0xff789bff, 0xff7a7fff, 0xff907fff, 0xfff7beff, 0xffb685ff, 0xffe4b2fe, 0xfff492fe, 0xffd4a9ff, 0xfffe96f0, 0xffb29aff, 0xfffc9fca, 0xff9a91ff, 0xff8693fe, 0xfffba9ab, 0xfff9c6b1, 0xfff9c6b1, 0xfff8d8b5, - 0xff71afff, 0xfffedcf3, 0xff8693fe, 0xff7a7fff, 0xff907fff, 0xffffc6fc, 0xffb685ff, 0xffe4b2fe, 0xfff492fe, 0xffd4a9ff, 0xfffd99e3, 0xffb29aff, 0xfffc9fca, 0xff9a91ff, 0xfffba9ab, 0xfffab6ad, 0xfff9c6b1, 0xfff8d2b3, 0xfff8e4b9, - 0xff71afff, 0xff789bff, 0xfffedcf3, 0xffffd0f7, 0xffb685ff, 0xffffc6fc, 0xfff7beff, 0xfff7beff, 0xfff492fe, 0xffe4b2fe, 0xffd4a9ff, 0xfffc9fca, 0xfffca4bc, 0xffb29aff, 0xff9a91ff, 0xff9a91ff, 0xfff8d2b3, 0xfff8d8b5, 0xfff8e4b9, - 0xff789bff, 0xff8693fe, 0xff7a7fff, 0xff907fff, 0xffb685ff, 0xffb685ff, 0xffd98cff, 0xfff492fe, 0xfff492fe, 0xfffd99e3, 0xfffd9bda, 0xfffc9fca, 0xfffca4bc, 0xfffba9ab, 0xfffab6ad, 0xfffab6ad, 0xfff8d8b5, 0xfff8d8b5, 0xfff8e4b9, - 0xff789bff, 0xff7b8bff, 0xff907fff, 0xff907fff, 0xffb685ff, 0xffd98cff, 0xffd98cff, 0xfff492fe, 0xfffe96f0, 0xfffd99e3, 0xfffc9fca, 0xfffca4bc, 0xfffca4bc, 0xfffba9ab, 0xfff9c6b1, 0xfff9c6b1, 0xfff8d8b5, 0xfff8e4b9, 0xfff8e4b9, - 0xff7b8bff, 0xffffebf2, 0xff907fff, 0xffb685ff, 0xffffd0f7, 0xffd98cff, 0xfff492fe, 0xffffc6fc, 0xfffd99e3, 0xfffd9bda, 0xffe4b2fe, 0xfffca4bc, 0xfffba9ab, 0xfffba9ab, 0xffb29aff, 0xfff8d8b5, 0xff9a91ff, 0xff7b8bff, 0xff8693fe, - 0xff7b8bff, 0xffffebf2, 0xffffebf2, 0xffb685ff, 0xfffedcf3, 0xfff492fe, 0xffffd0f7, 0xfffe96f0, 0xffffc6fc, 0xfffc9fca, 0xffe4b2fe, 0xffe4b2fe, 0xfffba9ab, 0xffd4a9ff, 0xffb29aff, 0xfff8d8b5, 0xff9a91ff, 0xfff8e4b9, 0xfff8e4b9, - 0xff7a7fff, 0xffffebf2, 0xffb685ff, 0xfffedcf3, 0xfffedcf3, 0xfff492fe, 0xffffd0f7, 0xffffc6fc, 0xffffc6fc, 0xfffc9fca, 0xfff7beff, 0xfffba9ab, 0xffe4b2fe, 0xfff9c6b1, 0xffc398ff, 0xfff8e4b9, 0xffb29aff, 0xff9a91ff, 0xfff8e4b9, - 0xff7a7fff, 0xffffebf2, 0xffb685ff, 0xffd98cff, 0xffffebf2, 0xfff492fe, 0xfffedcf3, 0xfffd9bda, 0xffffc6fc, 0xfffca4bc, 0xfff7beff, 0xfffba9ab, 0xfffab6ad, 0xfff8d2b3, 0xffd4a9ff, 0xfff8e4b9, 0xffb29aff, 0xfff8e4b9, 0xfff8e4b9, - 0xff907fff, 0xffffebf2, 0xffb685ff, 0xffd98cff, 0xfffedcf3, 0xfff492fe, 0xfffedcf3, 0xfffd99e3, 0xffffd0f7, 0xfffca4bc, 0xfff7beff, 0xfffab6ad, 0xfff9c6b1, 0xfff8d2b3, 0xffd4a9ff, 0xfff8e4b9, 0xffb29aff, 0xffb29aff, 0xff9a91ff, - 0xffb685ff, 0xffb685ff, 0xffd98cff, 0xffd98cff, 0xfff492fe, 0xfffe96f0, 0xfffd99e3, 0xfffc9fca, 0xfffc9fca, 0xfffca4bc, 0xfffba9ab, 0xfffab6ad, 0xfff9c6b1, 0xfff8d2b3, 0xfff8e4b9, 0xfff8e4b9, 0xfff8e4b9, 0xfff8e4b9, 0xfff8e4b9, - 0xffb685ff, 0xffb685ff, 0xffd98cff, 0xfff492fe, 0xfffe96f0, 0xfffd99e3, 0xfffd9bda, 0xfffc9fca, 0xfffca4bc, 0xfffba9ab, 0xfffab6ad, 0xfff9c6b1, 0xfff8d2b3, 0xfff8e4b9, 0xfff8e4b9, 0xfff8e4b9, 0xfff8e4b9, 0xfff8e4b9, 0xfff8e4b9, - })}, - -}; - -void printImageBytes(const video::IImage *img) -{ - const auto *data = (u8 *)img->getData(); - const auto w = img->getPitch(); - const auto h = img->getDimension().Height; - for (int y = 0; y < h; y++) { - for (int k = 0; k < w; k++) { - std::printf("0x%02x, ", *data++); - } - std::printf("\n"); - } -} - -int main(int argc, char *argv[]) -try { - if (argc != 3) - throw std::runtime_error("Invalid arguments. Expected sample ID and image file name"); - - const ImageDesc *sample = nullptr; - for (auto &&image : test_images) { - if (strcmp(argv[1], image.name) == 0) - sample = ℑ - } - if (!sample) - throw std::runtime_error("Sample not found"); - - SIrrlichtCreationParameters p; - p.DriverType = video::EDT_NULL; - p.WindowSize = core::dimension2du(640, 480); - p.LoggingLevel = ELL_DEBUG; - - auto *device = createDeviceEx(p); - if (!device) - throw std::runtime_error("Failed to create device"); - - auto *driver = device->getVideoDriver(); - - auto *img = driver->createImageFromFile(argv[2]); - if (!img) - throw std::runtime_error("Failed to load image"); - - if (img->getDimension() != core::dimension2du{19, 15}) - throw std::runtime_error("Wrong image dimensions"); - - if (img->getColorFormat() != sample->format) - throw std::runtime_error("Wrong image format"); - - if (img->getImageDataSizeInBytes() != sample->data.size()) - throw std::logic_error("Image data size not equal to sample size"); - - if (memcmp(img->getData(), sample->data.data(), sample->data.size()) != 0) { - printImageBytes(img); - throw std::runtime_error("Wrong image contents"); - } - - img->drop(); - device->drop(); - - return 0; -} catch (const std::exception &e) { - std::printf("Test failed: %s\n", e.what()); - return 1; -} diff --git a/lib/gmp/mini-gmp.c b/lib/gmp/mini-gmp.c index 69a72bfd4..ef9010be2 100644 --- a/lib/gmp/mini-gmp.c +++ b/lib/gmp/mini-gmp.c @@ -55,7 +55,7 @@ see https://www.gnu.org/licenses/. */ #include #endif - + /* Macros */ #define GMP_LIMB_BITS (sizeof(mp_limb_t) * CHAR_BIT) @@ -283,7 +283,7 @@ see https://www.gnu.org/licenses/. */ const int mp_bits_per_limb = GMP_LIMB_BITS; - + /* Memory allocation and other helper functions. */ static void gmp_die (const char *msg) @@ -384,7 +384,7 @@ gmp_free_limbs (mp_ptr old, mp_size_t size) gmp_free (old, size * sizeof (mp_limb_t)); } - + /* MPN interface */ void @@ -777,7 +777,7 @@ mpn_neg (mp_ptr rp, mp_srcptr up, mp_size_t n) return 1; } - + /* MPN division interface. */ /* The 3/2 inverse is defined as @@ -1169,7 +1169,7 @@ mpn_div_qr (mp_ptr qp, mp_ptr np, mp_size_t nn, mp_srcptr dp, mp_size_t dn) gmp_free_limbs (tp, dn); } - + /* MPN base conversion. */ static unsigned mpn_base_power_of_two_p (unsigned b) @@ -1425,7 +1425,7 @@ mpn_set_str (mp_ptr rp, const unsigned char *sp, size_t sn, int base) } } - + /* MPZ interface */ void mpz_init (mpz_t r) @@ -1480,7 +1480,7 @@ mpz_realloc (mpz_t r, mp_size_t size) #define MPZ_REALLOC(z,n) ((n) > (z)->_mp_alloc \ ? mpz_realloc(z,n) \ : (z)->_mp_d) - + /* MPZ assignment and basic conversions. */ void mpz_set_si (mpz_t r, signed long int x) @@ -1704,7 +1704,7 @@ mpz_roinit_n (mpz_t x, mp_srcptr xp, mp_size_t xs) return x; } - + /* Conversions and comparison to double. */ void mpz_set_d (mpz_t r, double x) @@ -1862,7 +1862,7 @@ mpz_cmp_d (const mpz_t x, double d) } } - + /* MPZ comparisons and the like. */ int mpz_sgn (const mpz_t u) @@ -1950,7 +1950,7 @@ mpz_swap (mpz_t u, mpz_t v) MPN_PTR_SWAP (u->_mp_d, u->_mp_size, v->_mp_d, v->_mp_size); } - + /* MPZ addition and subtraction */ @@ -2050,7 +2050,7 @@ mpz_sub (mpz_t r, const mpz_t a, const mpz_t b) r->_mp_size = a->_mp_size >= 0 ? rn : - rn; } - + /* MPZ multiplication */ void mpz_mul_si (mpz_t r, const mpz_t u, long int v) @@ -2186,7 +2186,7 @@ mpz_submul (mpz_t r, const mpz_t u, const mpz_t v) mpz_clear (t); } - + /* MPZ division */ enum mpz_div_round_mode { GMP_DIV_FLOOR, GMP_DIV_CEIL, GMP_DIV_TRUNC }; @@ -2661,7 +2661,7 @@ mpz_divisible_ui_p (const mpz_t n, unsigned long d) return mpz_div_qr_ui (NULL, NULL, n, d, GMP_DIV_TRUNC) == 0; } - + /* GCD */ static mp_limb_t mpn_gcd_11 (mp_limb_t u, mp_limb_t v) @@ -3054,7 +3054,7 @@ mpz_invert (mpz_t r, const mpz_t u, const mpz_t m) return invertible; } - + /* Higher level operations (sqrt, pow and root) */ void @@ -3334,7 +3334,7 @@ mpn_sqrtrem (mp_ptr sp, mp_ptr rp, mp_srcptr p, mp_size_t n) mpz_clear (r); return res; } - + /* Combinatorics */ void @@ -3378,7 +3378,7 @@ mpz_bin_uiui (mpz_t r, unsigned long n, unsigned long k) mpz_clear (t); } - + /* Primality testing */ /* Computes Kronecker (a/b) with odd b, a!=0 and GCD(a,b) = 1 */ @@ -3646,7 +3646,7 @@ mpz_probab_prime_p (const mpz_t n, int reps) return is_prime; } - + /* Logical operations and bit manipulation. */ /* Numbers are treated as if represented in two's complement (and @@ -4183,7 +4183,7 @@ mpz_scan0 (const mpz_t u, mp_bitcnt_t starting_bit) return mpn_common_scan (limb, i, up, un, ux); } - + /* MPZ base conversion. */ size_t @@ -4443,7 +4443,7 @@ mpz_out_str (FILE *stream, int base, const mpz_t x) return n; } - + static int gmp_detect_endian (void) { diff --git a/lib/jsoncpp/json/UPDATING b/lib/jsoncpp/json/UPDATING index c96ade51c..f299a33ba 100644 --- a/lib/jsoncpp/json/UPDATING +++ b/lib/jsoncpp/json/UPDATING @@ -1,6 +1,6 @@ #!/bin/sh cd .. -git clone https://github.com/open-source-parsers/jsoncpp -b 1.9.5 --depth 1 +git clone https://github.com/open-source-parsers/jsoncpp -b 1.9.6 --depth 1 cd jsoncpp ./amalgamate.py cp -R dist/json ../json diff --git a/lib/jsoncpp/json/json-forwards.h b/lib/jsoncpp/json/json-forwards.h index dda924f83..6800f787f 100644 --- a/lib/jsoncpp/json/json-forwards.h +++ b/lib/jsoncpp/json/json-forwards.h @@ -1,4 +1,4 @@ -/// Json-cpp amalgamated forward header (http://jsoncpp.sourceforge.net/). +/// Json-cpp amalgamated forward header (https://github.com/open-source-parsers/jsoncpp/). /// It is intended to be used with #include "json/json-forwards.h" /// This header provides forward declaration for all JsonCpp types. @@ -94,19 +94,18 @@ license you like. // 3. /CMakeLists.txt // IMPORTANT: also update the SOVERSION!! -#define JSONCPP_VERSION_STRING "1.9.5" +#define JSONCPP_VERSION_STRING "1.9.7" #define JSONCPP_VERSION_MAJOR 1 #define JSONCPP_VERSION_MINOR 9 -#define JSONCPP_VERSION_PATCH 5 +#define JSONCPP_VERSION_PATCH 7 #define JSONCPP_VERSION_QUALIFIER #define JSONCPP_VERSION_HEXA \ ((JSONCPP_VERSION_MAJOR << 24) | (JSONCPP_VERSION_MINOR << 16) | \ (JSONCPP_VERSION_PATCH << 8)) -#ifdef JSONCPP_USING_SECURE_MEMORY -#undef JSONCPP_USING_SECURE_MEMORY +#if !defined(JSONCPP_USE_SECURE_MEMORY) +#define JSONCPP_USE_SECURE_MEMORY 0 #endif -#define JSONCPP_USING_SECURE_MEMORY 0 // If non-zero, the library zeroes any memory that it has allocated before // it frees its memory. @@ -133,10 +132,12 @@ license you like. #ifndef JSON_ALLOCATOR_H_INCLUDED #define JSON_ALLOCATOR_H_INCLUDED +#include #include #include -#pragma pack(push, 8) +#pragma pack(push) +#pragma pack() namespace Json { template class SecureAllocator { @@ -164,8 +165,16 @@ public: * The memory block is filled with zeroes before being released. */ void deallocate(pointer p, size_type n) { - // memset_s is used because memset may be optimized away by the compiler + // These constructs will not be removed by the compiler during optimization, + // unlike memset. +#if defined(HAVE_MEMSET_S) memset_s(p, n * sizeof(T), 0, n * sizeof(T)); +#elif defined(_WIN32) + RtlSecureZeroMemory(p, n * sizeof(T)); +#else + std::fill_n(reinterpret_cast(p), n, 0); +#endif + // free using "global operator delete" ::operator delete(p); } @@ -195,7 +204,9 @@ public: // Boilerplate SecureAllocator() {} template SecureAllocator(const SecureAllocator&) {} - template struct rebind { using other = SecureAllocator; }; + template struct rebind { + using other = SecureAllocator; + }; }; template @@ -356,7 +367,7 @@ using LargestUInt = UInt64; template using Allocator = - typename std::conditional, + typename std::conditional, std::allocator>::type; using String = std::basic_string, Allocator>; using IStringStream = diff --git a/lib/jsoncpp/json/json.h b/lib/jsoncpp/json/json.h index b280790a4..d81f7dc0f 100644 --- a/lib/jsoncpp/json/json.h +++ b/lib/jsoncpp/json/json.h @@ -1,4 +1,4 @@ -/// Json-cpp amalgamated header (http://jsoncpp.sourceforge.net/). +/// Json-cpp amalgamated header (https://github.com/open-source-parsers/jsoncpp/). /// It is intended to be used with #include "json/json.h" // ////////////////////////////////////////////////////////////////////// @@ -93,19 +93,18 @@ license you like. // 3. /CMakeLists.txt // IMPORTANT: also update the SOVERSION!! -#define JSONCPP_VERSION_STRING "1.9.5" +#define JSONCPP_VERSION_STRING "1.9.7" #define JSONCPP_VERSION_MAJOR 1 #define JSONCPP_VERSION_MINOR 9 -#define JSONCPP_VERSION_PATCH 5 +#define JSONCPP_VERSION_PATCH 7 #define JSONCPP_VERSION_QUALIFIER #define JSONCPP_VERSION_HEXA \ ((JSONCPP_VERSION_MAJOR << 24) | (JSONCPP_VERSION_MINOR << 16) | \ (JSONCPP_VERSION_PATCH << 8)) -#ifdef JSONCPP_USING_SECURE_MEMORY -#undef JSONCPP_USING_SECURE_MEMORY +#if !defined(JSONCPP_USE_SECURE_MEMORY) +#define JSONCPP_USE_SECURE_MEMORY 0 #endif -#define JSONCPP_USING_SECURE_MEMORY 0 // If non-zero, the library zeroes any memory that it has allocated before // it frees its memory. @@ -132,10 +131,12 @@ license you like. #ifndef JSON_ALLOCATOR_H_INCLUDED #define JSON_ALLOCATOR_H_INCLUDED +#include #include #include -#pragma pack(push, 8) +#pragma pack(push) +#pragma pack() namespace Json { template class SecureAllocator { @@ -163,8 +164,16 @@ public: * The memory block is filled with zeroes before being released. */ void deallocate(pointer p, size_type n) { - // memset_s is used because memset may be optimized away by the compiler + // These constructs will not be removed by the compiler during optimization, + // unlike memset. +#if defined(HAVE_MEMSET_S) memset_s(p, n * sizeof(T), 0, n * sizeof(T)); +#elif defined(_WIN32) + RtlSecureZeroMemory(p, n * sizeof(T)); +#else + std::fill_n(reinterpret_cast(p), n, 0); +#endif + // free using "global operator delete" ::operator delete(p); } @@ -194,7 +203,9 @@ public: // Boilerplate SecureAllocator() {} template SecureAllocator(const SecureAllocator&) {} - template struct rebind { using other = SecureAllocator; }; + template struct rebind { + using other = SecureAllocator; + }; }; template @@ -355,7 +366,7 @@ using LargestUInt = UInt64; template using Allocator = - typename std::conditional, + typename std::conditional, std::allocator>::type; using String = std::basic_string, Allocator>; using IStringStream = @@ -459,7 +470,8 @@ class ValueConstIterator; #include "forwards.h" #endif // if !defined(JSON_IS_AMALGAMATION) -#pragma pack(push, 8) +#pragma pack(push) +#pragma pack() namespace Json { @@ -527,8 +539,8 @@ public: // recognized in your jurisdiction. // See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE -#ifndef JSON_H_INCLUDED -#define JSON_H_INCLUDED +#ifndef JSON_VALUE_H_INCLUDED +#define JSON_VALUE_H_INCLUDED #if !defined(JSON_IS_AMALGAMATION) #include "forwards.h" @@ -577,7 +589,8 @@ public: #pragma warning(disable : 4251 4275) #endif // if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) -#pragma pack(push, 8) +#pragma pack(push) +#pragma pack() /** \brief JSON (JavaScript Object Notation). */ @@ -898,7 +911,7 @@ public: int compare(const Value& other) const; const char* asCString() const; ///< Embedded zeroes could cause you trouble! -#if JSONCPP_USING_SECURE_MEMORY +#if JSONCPP_USE_SECURE_MEMORY unsigned getCStringLength() const; // Allows you to understand the length of // the CString #endif @@ -960,7 +973,7 @@ public: /// \post type() is arrayValue void resize(ArrayIndex newSize); - //@{ + ///@{ /// Access an array element (zero based index). If the array contains less /// than index element, then null value are inserted in the array so that /// its size is index+1. @@ -968,15 +981,15 @@ public: /// this from the operator[] which takes a string.) Value& operator[](ArrayIndex index); Value& operator[](int index); - //@} + ///@} - //@{ + ///@{ /// Access an array element (zero based index). /// (You may need to say 'value[0u]' to get your compiler to distinguish /// this from the operator[] which takes a string.) const Value& operator[](ArrayIndex index) const; const Value& operator[](int index) const; - //@} + ///@} /// If the array contains at least index+1 elements, returns the element /// value, otherwise returns defaultValue. @@ -1036,6 +1049,9 @@ public: /// and operator[]const /// \note As stated elsewhere, behavior is undefined if (end-begin) >= 2^30 Value const* find(char const* begin, char const* end) const; + /// Most general and efficient version of isMember()const, get()const, + /// and operator[]const + Value const* find(const String& key) const; /// Most general and efficient version of object-mutators. /// \note As stated elsewhere, behavior is undefined if (end-begin) >= 2^30 /// \return non-zero, but JSON_ASSERT if this is neither object nor nullValue. @@ -1108,6 +1124,26 @@ public: iterator begin(); iterator end(); + /// \brief Returns a reference to the first element in the `Value`. + /// Requires that this value holds an array or json object, with at least one + /// element. + const Value& front() const; + + /// \brief Returns a reference to the first element in the `Value`. + /// Requires that this value holds an array or json object, with at least one + /// element. + Value& front(); + + /// \brief Returns a reference to the last element in the `Value`. + /// Requires that value holds an array or json object, with at least one + /// element. + const Value& back() const; + + /// \brief Returns a reference to the last element in the `Value`. + /// Requires that this value holds an array or json object, with at least one + /// element. + Value& back(); + // Accessors for the [start, limit) range of bytes within the JSON text from // which this value was parsed, if any. void setOffsetStart(ptrdiff_t start); @@ -1448,6 +1484,14 @@ public: inline void swap(Value& a, Value& b) { a.swap(b); } +inline const Value& Value::front() const { return *begin(); } + +inline Value& Value::front() { return *begin(); } + +inline const Value& Value::back() const { return *(--end()); } + +inline Value& Value::back() { return *(--end()); } + } // namespace Json #pragma pack(pop) @@ -1496,7 +1540,8 @@ inline void swap(Value& a, Value& b) { a.swap(b); } #pragma warning(disable : 4251) #endif // if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) -#pragma pack(push, 8) +#pragma pack(push) +#pragma pack() namespace Json { @@ -1523,12 +1568,12 @@ public: }; /** \brief Constructs a Reader allowing all features for parsing. - * \deprecated Use CharReader and CharReaderBuilder. + * \deprecated Use CharReader and CharReaderBuilder. */ Reader(); /** \brief Constructs a Reader allowing the specified feature set for parsing. - * \deprecated Use CharReader and CharReaderBuilder. + * \deprecated Use CharReader and CharReaderBuilder. */ Reader(const Features& features); @@ -1662,6 +1707,7 @@ private: using Errors = std::deque; bool readToken(Token& token); + bool readTokenSkippingComments(Token& token); void skipSpaces(); bool match(const Char* pattern, int patternLength); bool readComment(); @@ -1693,7 +1739,6 @@ private: int& column) const; String getLocationLineAndColumn(Location location) const; void addComment(Location begin, Location end, CommentPlacement placement); - void skipCommentTokens(Token& token); static bool containsNewLine(Location begin, Location end); static String normalizeEOL(Location begin, Location end); @@ -1716,6 +1761,12 @@ private: */ class JSON_API CharReader { public: + struct JSON_API StructuredError { + ptrdiff_t offset_start; + ptrdiff_t offset_limit; + String message; + }; + virtual ~CharReader() = default; /** \brief Read a Value from a JSON * document. The document must be a UTF-8 encoded string containing the @@ -1734,7 +1785,12 @@ public: * error occurred. */ virtual bool parse(char const* beginDoc, char const* endDoc, Value* root, - String* errs) = 0; + String* errs); + + /** \brief Returns a vector of structured errors encountered while parsing. + * Each parse call resets the stored list of errors. + */ + std::vector getStructuredErrors() const; class JSON_API Factory { public: @@ -1744,7 +1800,21 @@ public: */ virtual CharReader* newCharReader() const = 0; }; // Factory -}; // CharReader + +protected: + class Impl { + public: + virtual ~Impl() = default; + virtual bool parse(char const* beginDoc, char const* endDoc, Value* root, + String* errs) = 0; + virtual std::vector getStructuredErrors() const = 0; + }; + + explicit CharReader(std::unique_ptr impl) : _impl(std::move(impl)) {} + +private: + std::unique_ptr _impl; +}; // CharReader /** \brief Build a CharReader implementation. * @@ -1832,6 +1902,12 @@ public: * \snippet src/lib_json/json_reader.cpp CharReaderBuilderStrictMode */ static void strictMode(Json::Value* settings); + /** ECMA-404 mode. + * \pre 'settings' != NULL (but Json::null is fine) + * \remark Defaults: + * \snippet src/lib_json/json_reader.cpp CharReaderBuilderECMA404Mode + */ + static void ecma404Mode(Json::Value* settings); }; /** Consume entire stream and use its begin/end. @@ -1912,7 +1988,8 @@ JSON_API IStream& operator>>(IStream&, Value&); #pragma warning(disable : 4251) #endif // if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) -#pragma pack(push, 8) +#pragma pack(push) +#pragma pack() namespace Json { @@ -1955,7 +2032,7 @@ public: */ virtual StreamWriter* newStreamWriter() const = 0; }; // Factory -}; // StreamWriter +}; // StreamWriter /** \brief Write into stringstream, then return string, for convenience. * A StreamWriter will be created from the factory, used, and then deleted. @@ -2059,8 +2136,7 @@ public: #pragma warning(push) #pragma warning(disable : 4996) // Deriving from deprecated class #endif -class JSON_API FastWriter - : public Writer { +class JSON_API FastWriter : public Writer { public: FastWriter(); ~FastWriter() override = default; @@ -2109,7 +2185,7 @@ private: * - otherwise, it the values do not fit on one line, or the array contains * object or non empty array, then print one value per line. * - * If the Value have comments then they are outputed according to their + * If the Value have comments then they are outputted according to their *#CommentPlacement. * * \sa Reader, Value, Value::setComment() @@ -2119,8 +2195,7 @@ private: #pragma warning(push) #pragma warning(disable : 4996) // Deriving from deprecated class #endif -class JSON_API - StyledWriter : public Writer { +class JSON_API StyledWriter : public Writer { public: StyledWriter(); ~StyledWriter() override = default; @@ -2178,7 +2253,7 @@ private: * - otherwise, it the values do not fit on one line, or the array contains * object or non empty array, then print one value per line. * - * If the Value have comments then they are outputed according to their + * If the Value have comments then they are outputted according to their #CommentPlacement. * * \sa Reader, Value, Value::setComment() @@ -2188,8 +2263,7 @@ private: #pragma warning(push) #pragma warning(disable : 4996) // Deriving from deprecated class #endif -class JSON_API - StyledStreamWriter { +class JSON_API StyledStreamWriter { public: /** * \param indentation Each level will be indented by this amount extra. @@ -2245,6 +2319,7 @@ String JSON_API valueToString( PrecisionType precisionType = PrecisionType::significantDigits); String JSON_API valueToString(bool value); String JSON_API valueToQuotedString(const char* value); +String JSON_API valueToQuotedString(const char* value, size_t length); /// \brief Output using the StyledStreamWriter. /// \see Json::operator>>() diff --git a/lib/jsoncpp/jsoncpp.cpp b/lib/jsoncpp/jsoncpp.cpp index 2ec052c94..758f451c6 100644 --- a/lib/jsoncpp/jsoncpp.cpp +++ b/lib/jsoncpp/jsoncpp.cpp @@ -1,4 +1,4 @@ -/// Json-cpp amalgamated source (http://jsoncpp.sourceforge.net/). +/// Json-cpp amalgamated source (https://github.com/open-source-parsers/jsoncpp/). /// It is intended to be used with #include "json/json.h" // ////////////////////////////////////////////////////////////////////// @@ -250,6 +250,7 @@ Iter fixZerosInTheEnd(Iter begin, Iter end, unsigned int precision) { #endif // if !defined(JSON_IS_AMALGAMATION) #include #include +#include #include #include #include @@ -366,7 +367,7 @@ bool Reader::parse(const char* beginDoc, const char* endDoc, Value& root, bool successful = readValue(); Token token; - skipCommentTokens(token); + readTokenSkippingComments(token); if (collectComments_ && !commentsBefore_.empty()) root.setComment(commentsBefore_, commentAfter); if (features_.strictRoot_) { @@ -394,7 +395,7 @@ bool Reader::readValue() { throwRuntimeError("Exceeded stackLimit in readValue()."); Token token; - skipCommentTokens(token); + readTokenSkippingComments(token); bool successful = true; if (collectComments_ && !commentsBefore_.empty()) { @@ -462,14 +463,14 @@ bool Reader::readValue() { return successful; } -void Reader::skipCommentTokens(Token& token) { +bool Reader::readTokenSkippingComments(Token& token) { + bool success = readToken(token); if (features_.allowComments_) { - do { - readToken(token); - } while (token.type_ == tokenComment); - } else { - readToken(token); + while (success && token.type_ == tokenComment) { + success = readToken(token); + } } + return success; } bool Reader::readToken(Token& token) { @@ -683,12 +684,7 @@ bool Reader::readObject(Token& token) { Value init(objectValue); currentValue().swapPayload(init); currentValue().setOffsetStart(token.start_ - begin_); - while (readToken(tokenName)) { - bool initialTokenOk = true; - while (tokenName.type_ == tokenComment && initialTokenOk) - initialTokenOk = readToken(tokenName); - if (!initialTokenOk) - break; + while (readTokenSkippingComments(tokenName)) { if (tokenName.type_ == tokenObjectEnd && name.empty()) // empty object return true; name.clear(); @@ -717,15 +713,11 @@ bool Reader::readObject(Token& token) { return recoverFromError(tokenObjectEnd); Token comma; - if (!readToken(comma) || - (comma.type_ != tokenObjectEnd && comma.type_ != tokenArraySeparator && - comma.type_ != tokenComment)) { + if (!readTokenSkippingComments(comma) || + (comma.type_ != tokenObjectEnd && comma.type_ != tokenArraySeparator)) { return addErrorAndRecover("Missing ',' or '}' in object declaration", comma, tokenObjectEnd); } - bool finalizeTokenOk = true; - while (comma.type_ == tokenComment && finalizeTokenOk) - finalizeTokenOk = readToken(comma); if (comma.type_ == tokenObjectEnd) return true; } @@ -755,10 +747,7 @@ bool Reader::readArray(Token& token) { Token currentToken; // Accept Comment after last item in the array. - ok = readToken(currentToken); - while (currentToken.type_ == tokenComment && ok) { - ok = readToken(currentToken); - } + ok = readTokenSkippingComments(currentToken); bool badTokenType = (currentToken.type_ != tokenArraySeparator && currentToken.type_ != tokenArrayEnd); if (!ok || badTokenType) { @@ -836,11 +825,16 @@ bool Reader::decodeDouble(Token& token) { bool Reader::decodeDouble(Token& token, Value& decoded) { double value = 0; - String buffer(token.start_, token.end_); - IStringStream is(buffer); - if (!(is >> value)) - return addError( - "'" + String(token.start_, token.end_) + "' is not a number.", token); + IStringStream is(String(token.start_, token.end_)); + if (!(is >> value)) { + if (value == std::numeric_limits::max()) + value = std::numeric_limits::infinity(); + else if (value == std::numeric_limits::lowest()) + value = -std::numeric_limits::infinity(); + else if (!std::isinf(value)) + return addError( + "'" + String(token.start_, token.end_) + "' is not a number.", token); + } decoded = value; return true; } @@ -1004,7 +998,7 @@ void Reader::getLocationLineAndColumn(Location location, int& line, while (current < location && current != end_) { Char c = *current++; if (c == '\r') { - if (*current == '\n') + if (current != end_ && *current == '\n') ++current; lastLineStart = current; ++line; @@ -1121,17 +1115,12 @@ class OurReader { public: using Char = char; using Location = const Char*; - struct StructuredError { - ptrdiff_t offset_start; - ptrdiff_t offset_limit; - String message; - }; explicit OurReader(OurFeatures const& features); bool parse(const char* beginDoc, const char* endDoc, Value& root, bool collectComments = true); String getFormattedErrorMessages() const; - std::vector getStructuredErrors() const; + std::vector getStructuredErrors() const; private: OurReader(OurReader const&); // no impl @@ -1174,6 +1163,7 @@ private: using Errors = std::deque; bool readToken(Token& token); + bool readTokenSkippingComments(Token& token); void skipSpaces(); void skipBom(bool skipBom); bool match(const Char* pattern, int patternLength); @@ -1207,7 +1197,6 @@ private: int& column) const; String getLocationLineAndColumn(Location location) const; void addComment(Location begin, Location end, CommentPlacement placement); - void skipCommentTokens(Token& token); static String normalizeEOL(Location begin, Location end); static bool containsNewLine(Location begin, Location end); @@ -1261,7 +1250,7 @@ bool OurReader::parse(const char* beginDoc, const char* endDoc, Value& root, bool successful = readValue(); nodes_.pop(); Token token; - skipCommentTokens(token); + readTokenSkippingComments(token); if (features_.failIfExtra_ && (token.type_ != tokenEndOfStream)) { addError("Extra non-whitespace after JSON value.", token); return false; @@ -1289,7 +1278,7 @@ bool OurReader::readValue() { if (nodes_.size() > features_.stackLimit_) throwRuntimeError("Exceeded stackLimit in readValue()."); Token token; - skipCommentTokens(token); + readTokenSkippingComments(token); bool successful = true; if (collectComments_ && !commentsBefore_.empty()) { @@ -1376,14 +1365,14 @@ bool OurReader::readValue() { return successful; } -void OurReader::skipCommentTokens(Token& token) { +bool OurReader::readTokenSkippingComments(Token& token) { + bool success = readToken(token); if (features_.allowComments_) { - do { - readToken(token); - } while (token.type_ == tokenComment); - } else { - readToken(token); + while (success && token.type_ == tokenComment) { + success = readToken(token); + } } + return success; } bool OurReader::readToken(Token& token) { @@ -1680,12 +1669,7 @@ bool OurReader::readObject(Token& token) { Value init(objectValue); currentValue().swapPayload(init); currentValue().setOffsetStart(token.start_ - begin_); - while (readToken(tokenName)) { - bool initialTokenOk = true; - while (tokenName.type_ == tokenComment && initialTokenOk) - initialTokenOk = readToken(tokenName); - if (!initialTokenOk) - break; + while (readTokenSkippingComments(tokenName)) { if (tokenName.type_ == tokenObjectEnd && (name.empty() || features_.allowTrailingCommas_)) // empty object or trailing comma @@ -1722,15 +1706,11 @@ bool OurReader::readObject(Token& token) { return recoverFromError(tokenObjectEnd); Token comma; - if (!readToken(comma) || - (comma.type_ != tokenObjectEnd && comma.type_ != tokenArraySeparator && - comma.type_ != tokenComment)) { + if (!readTokenSkippingComments(comma) || + (comma.type_ != tokenObjectEnd && comma.type_ != tokenArraySeparator)) { return addErrorAndRecover("Missing ',' or '}' in object declaration", comma, tokenObjectEnd); } - bool finalizeTokenOk = true; - while (comma.type_ == tokenComment && finalizeTokenOk) - finalizeTokenOk = readToken(comma); if (comma.type_ == tokenObjectEnd) return true; } @@ -1764,10 +1744,7 @@ bool OurReader::readArray(Token& token) { Token currentToken; // Accept Comment after last item in the array. - ok = readToken(currentToken); - while (currentToken.type_ == tokenComment && ok) { - ok = readToken(currentToken); - } + ok = readTokenSkippingComments(currentToken); bool badTokenType = (currentToken.type_ != tokenArraySeparator && currentToken.type_ != tokenArrayEnd); if (!ok || badTokenType) { @@ -1845,7 +1822,7 @@ bool OurReader::decodeNumber(Token& token, Value& decoded) { const auto digit(static_cast(c - '0')); if (value >= threshold) { // We've hit or exceeded the max value divided by 10 (rounded down). If - // a) we've only just touched the limit, meaing value == threshold, + // a) we've only just touched the limit, meaning value == threshold, // b) this is the last digit, or // c) it's small enough to fit in that rounding delta, we're okay. // Otherwise treat this number as a double to avoid overflow. @@ -1882,11 +1859,15 @@ bool OurReader::decodeDouble(Token& token) { bool OurReader::decodeDouble(Token& token, Value& decoded) { double value = 0; - const String buffer(token.start_, token.end_); - IStringStream is(buffer); + IStringStream is(String(token.start_, token.end_)); if (!(is >> value)) { - return addError( - "'" + String(token.start_, token.end_) + "' is not a number.", token); + if (value == std::numeric_limits::max()) + value = std::numeric_limits::infinity(); + else if (value == std::numeric_limits::lowest()) + value = -std::numeric_limits::infinity(); + else if (!std::isinf(value)) + return addError( + "'" + String(token.start_, token.end_) + "' is not a number.", token); } decoded = value; return true; @@ -2051,7 +2032,7 @@ void OurReader::getLocationLineAndColumn(Location location, int& line, while (current < location && current != end_) { Char c = *current++; if (c == '\r') { - if (*current == '\n') + if (current != end_ && *current == '\n') ++current; lastLineStart = current; ++line; @@ -2086,10 +2067,11 @@ String OurReader::getFormattedErrorMessages() const { return formattedMessage; } -std::vector OurReader::getStructuredErrors() const { - std::vector allErrors; +std::vector +OurReader::getStructuredErrors() const { + std::vector allErrors; for (const auto& error : errors_) { - OurReader::StructuredError structured; + CharReader::StructuredError structured; structured.offset_start = error.token_.start_ - begin_; structured.offset_limit = error.token_.end_ - begin_; structured.message = error.message_; @@ -2099,20 +2081,36 @@ std::vector OurReader::getStructuredErrors() const { } class OurCharReader : public CharReader { - bool const collectComments_; - OurReader reader_; public: OurCharReader(bool collectComments, OurFeatures const& features) - : collectComments_(collectComments), reader_(features) {} - bool parse(char const* beginDoc, char const* endDoc, Value* root, - String* errs) override { - bool ok = reader_.parse(beginDoc, endDoc, *root, collectComments_); - if (errs) { - *errs = reader_.getFormattedErrorMessages(); + : CharReader( + std::unique_ptr(new OurImpl(collectComments, features))) {} + +protected: + class OurImpl : public Impl { + public: + OurImpl(bool collectComments, OurFeatures const& features) + : collectComments_(collectComments), reader_(features) {} + + bool parse(char const* beginDoc, char const* endDoc, Value* root, + String* errs) override { + bool ok = reader_.parse(beginDoc, endDoc, *root, collectComments_); + if (errs) { + *errs = reader_.getFormattedErrorMessages(); + } + return ok; } - return ok; - } + + std::vector + getStructuredErrors() const override { + return reader_.getStructuredErrors(); + } + + private: + bool const collectComments_; + OurReader reader_; + }; }; CharReaderBuilder::CharReaderBuilder() { setDefaults(&settings_); } @@ -2201,6 +2199,32 @@ void CharReaderBuilder::setDefaults(Json::Value* settings) { (*settings)["skipBom"] = true; //! [CharReaderBuilderDefaults] } +// static +void CharReaderBuilder::ecma404Mode(Json::Value* settings) { + //! [CharReaderBuilderECMA404Mode] + (*settings)["allowComments"] = false; + (*settings)["allowTrailingCommas"] = false; + (*settings)["strictRoot"] = false; + (*settings)["allowDroppedNullPlaceholders"] = false; + (*settings)["allowNumericKeys"] = false; + (*settings)["allowSingleQuotes"] = false; + (*settings)["stackLimit"] = 1000; + (*settings)["failIfExtra"] = true; + (*settings)["rejectDupKeys"] = false; + (*settings)["allowSpecialFloats"] = false; + (*settings)["skipBom"] = false; + //! [CharReaderBuilderECMA404Mode] +} + +std::vector +CharReader::getStructuredErrors() const { + return _impl->getStructuredErrors(); +} + +bool CharReader::parse(char const* beginDoc, char const* endDoc, Value* root, + String* errs) { + return _impl->parse(beginDoc, endDoc, root, errs); +} ////////////////////////////////// // global functions @@ -2209,7 +2233,7 @@ bool parseFromStream(CharReader::Factory const& fact, IStream& sin, Value* root, String* errs) { OStringStream ssin; ssin << sin.rdbuf(); - String doc = ssin.str(); + String doc = std::move(ssin).str(); char const* begin = doc.data(); char const* end = begin + doc.size(); // Note that we do not actually need a null-terminator. @@ -2501,7 +2525,8 @@ template static inline bool InRange(double d, T min, U max) { // The casts can lose precision, but we are looking only for // an approximate range. Might fail on edge cases though. ~cdunn - return d >= static_cast(min) && d <= static_cast(max); + return d >= static_cast(min) && d <= static_cast(max) && + !(static_cast(d) == min && d != static_cast(min)); } #else // if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) static inline double integerToDouble(Json::UInt64 value) { @@ -2515,7 +2540,8 @@ template static inline double integerToDouble(T value) { template static inline bool InRange(double d, T min, U max) { - return d >= integerToDouble(min) && d <= integerToDouble(max); + return d >= integerToDouble(min) && d <= integerToDouble(max) && + !(static_cast(d) == min && d != integerToDouble(min)); } #endif // if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) @@ -2577,7 +2603,7 @@ inline static void decodePrefixedString(bool isPrefixed, char const* prefixed, /** Free the string duplicated by * duplicateStringValue()/duplicateAndPrefixStringValue(). */ -#if JSONCPP_USING_SECURE_MEMORY +#if JSONCPP_USE_SECURE_MEMORY static inline void releasePrefixedStringValue(char* value) { unsigned length = 0; char const* valueDecoded; @@ -2592,10 +2618,10 @@ static inline void releaseStringValue(char* value, unsigned length) { memset(value, 0, size); free(value); } -#else // !JSONCPP_USING_SECURE_MEMORY +#else // !JSONCPP_USE_SECURE_MEMORY static inline void releasePrefixedStringValue(char* value) { free(value); } static inline void releaseStringValue(char* value, unsigned) { free(value); } -#endif // JSONCPP_USING_SECURE_MEMORY +#endif // JSONCPP_USE_SECURE_MEMORY } // namespace Json @@ -3013,7 +3039,7 @@ const char* Value::asCString() const { return this_str; } -#if JSONCPP_USING_SECURE_MEMORY +#if JSONCPP_USE_SECURE_MEMORY unsigned Value::getCStringLength() const { JSON_ASSERT_MESSAGE(type() == stringValue, "in Json::Value::asCString(): requires stringValue"); @@ -3119,6 +3145,11 @@ Value::Int64 Value::asInt64() const { JSON_ASSERT_MESSAGE(isInt64(), "LargestUInt out of Int64 range"); return Int64(value_.uint_); case realValue: + // If the double value is in proximity to minInt64, it will be rounded to + // minInt64. The correct value in this scenario is indeterminable + JSON_ASSERT_MESSAGE( + value_.real_ != minInt64, + "Double value is minInt64, precise value cannot be determined"); JSON_ASSERT_MESSAGE(InRange(value_.real_, minInt64, maxInt64), "double out of Int64 range"); return Int64(value_.real_); @@ -3506,6 +3537,9 @@ Value const* Value::find(char const* begin, char const* end) const { return nullptr; return &(*it).second; } +Value const* Value::find(const String& key) const { + return find(key.data(), key.data() + key.length()); +} Value* Value::demand(char const* begin, char const* end) { JSON_ASSERT_MESSAGE(type() == nullValue || type() == objectValue, "in Json::Value::demand(begin, end): requires " @@ -3519,7 +3553,7 @@ const Value& Value::operator[](const char* key) const { return *found; } Value const& Value::operator[](const String& key) const { - Value const* found = find(key.data(), key.data() + key.length()); + Value const* found = find(key); if (!found) return nullSingleton(); return *found; @@ -3619,7 +3653,7 @@ bool Value::removeIndex(ArrayIndex index, Value* removed) { return false; } if (removed) - *removed = it->second; + *removed = std::move(it->second); ArrayIndex oldSize = size(); // shift left all items left, into the place of the "removed" for (ArrayIndex i = index; i < (oldSize - 1); ++i) { @@ -3722,8 +3756,12 @@ bool Value::isInt64() const { // Note that maxInt64 (= 2^63 - 1) is not exactly representable as a // double, so double(maxInt64) will be rounded up to 2^63. Therefore we // require the value to be strictly less than the limit. - return value_.real_ >= double(minInt64) && - value_.real_ < double(maxInt64) && IsIntegral(value_.real_); + // minInt64 is -2^63 which can be represented as a double, but since double + // values in its proximity are also rounded to -2^63, we require the value + // to be strictly greater than the limit to avoid returning 'true' for + // values that are not in the range + return value_.real_ > double(minInt64) && value_.real_ < double(maxInt64) && + IsIntegral(value_.real_); default: break; } @@ -3761,7 +3799,11 @@ bool Value::isIntegral() const { // Note that maxUInt64 (= 2^64 - 1) is not exactly representable as a // double, so double(maxUInt64) will be rounded up to 2^64. Therefore we // require the value to be strictly less than the limit. - return value_.real_ >= double(minInt64) && + // minInt64 is -2^63 which can be represented as a double, but since double + // values in its proximity are also rounded to -2^63, we require the value + // to be strictly greater than the limit to avoid returning 'true' for + // values that are not in the range + return value_.real_ > double(minInt64) && value_.real_ < maxUInt64AsDouble && IsIntegral(value_.real_); #else return value_.real_ >= minInt && value_.real_ <= maxUInt && @@ -3824,9 +3866,8 @@ void Value::setComment(String comment, CommentPlacement placement) { // Always discard trailing newline, to aid indentation. comment.pop_back(); } - JSON_ASSERT(!comment.empty()); JSON_ASSERT_MESSAGE( - comment[0] == '\0' || comment[0] == '/', + comment.empty() || comment[0] == '/', "in Json::Value::setComment(): Comments must start with /"); comments_.set(placement, std::move(comment)); } @@ -4194,8 +4235,9 @@ String valueToString(double value, bool useSpecialFloats, if (!isfinite(value)) { static const char* const reps[2][3] = {{"NaN", "-Infinity", "Infinity"}, {"null", "-1e+9999", "1e+9999"}}; - return reps[useSpecialFloats ? 0 : 1] - [isnan(value) ? 0 : (value < 0) ? 1 : 2]; + return reps[useSpecialFloats ? 0 : 1][isnan(value) ? 0 + : (value < 0) ? 1 + : 2]; } String buffer(size_t(36), '\0'); @@ -4415,6 +4457,10 @@ String valueToQuotedString(const char* value) { return valueToQuotedStringN(value, strlen(value)); } +String valueToQuotedString(const char* value, size_t length) { + return valueToQuotedStringN(value, length); +} + // Class Writer // ////////////////////////////////////////////////////////////////// Writer::~Writer() = default; @@ -4552,7 +4598,7 @@ void StyledWriter::writeValue(const Value& value) { const String& name = *it; const Value& childValue = value[name]; writeCommentBeforeValue(childValue); - writeWithIndent(valueToQuotedString(name.c_str())); + writeWithIndent(valueToQuotedString(name.c_str(), name.size())); document_ += " : "; writeValue(childValue); if (++it == members.end()) { @@ -4770,7 +4816,7 @@ void StyledStreamWriter::writeValue(const Value& value) { const String& name = *it; const Value& childValue = value[name]; writeCommentBeforeValue(childValue); - writeWithIndent(valueToQuotedString(name.c_str())); + writeWithIndent(valueToQuotedString(name.c_str(), name.size())); *document_ << " : "; writeValue(childValue); if (++it == members.end()) { @@ -5308,7 +5354,7 @@ String writeString(StreamWriter::Factory const& factory, Value const& root) { OStringStream sout; StreamWriterPtr const writer(factory.newStreamWriter()); writer->write(root, &sout); - return sout.str(); + return std::move(sout).str(); } OStream& operator<<(OStream& sout, Value const& root) { diff --git a/lib/lua/CMakeLists.txt b/lib/lua/CMakeLists.txt index 2de4840cb..869a3c320 100644 --- a/lib/lua/CMakeLists.txt +++ b/lib/lua/CMakeLists.txt @@ -22,6 +22,10 @@ else() set(DEFAULT_ANSI TRUE) endif() +if(MSVC) + set(COMMON_CFLAGS "${COMMON_CFLAGS} -D_CRT_SECURE_NO_WARNINGS") +endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") set(COMMON_LDFLAGS "${COMMON_LDFLAGS} -Wl,-E -lm") set(DEFAULT_DLOPEN ON) diff --git a/lib/tiniergltf/.gitignore b/lib/tiniergltf/.gitignore new file mode 100644 index 000000000..6d105f52b --- /dev/null +++ b/lib/tiniergltf/.gitignore @@ -0,0 +1,6 @@ +cmake +CMakeCache.txt +CMakeFiles +.cache +compile_commands.json +build \ No newline at end of file diff --git a/lib/tiniergltf/CMakeLists.txt b/lib/tiniergltf/CMakeLists.txt new file mode 100644 index 000000000..889203e49 --- /dev/null +++ b/lib/tiniergltf/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.12) + +project(tiniergltf + VERSION 1.0.0 + LANGUAGES CXX +) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_library(tiniergltf OBJECT tiniergltf.hpp) +add_library(tiniergltf::tiniergltf ALIAS tiniergltf) +set_target_properties(tiniergltf PROPERTIES LINKER_LANGUAGE CXX) + +target_include_directories(tiniergltf + INTERFACE + "$" + "${JSON_INCLUDE_DIR}" # Set in FindJson.cmake + "${CMAKE_SOURCE_DIR}/src" # util/base64.h +) + +target_link_libraries(tiniergltf) diff --git a/lib/tiniergltf/Readme.md b/lib/tiniergltf/Readme.md new file mode 100644 index 000000000..a8cc65c93 --- /dev/null +++ b/lib/tiniergltf/Readme.md @@ -0,0 +1,39 @@ +# TinierGLTF + +A safe, modern, tiny glTF loader for C++ 17. + +What this is: + +* A tiny glTF deserializer which maps JSON objects to appropriate C++ structures. +* Intended to be safe for loading untrusted input. +* Slightly tailored to the needs of [Minetest](https://github.com/minetest/minetest). + +What this doesn't and shouldn't do: + +* Serialization +* Loading images +* Resolving resources +* Support glTF extensions + +## TODOs + +- [ ] Add GLB support. +- [ ] Add further checks according to the specification. + - Everything in the JSON schema (+ indices and misc. stuff) is already validated. + Much of the code was generated by a Lua script from the JSON schemata. + +## License + +`tiniergltf.hpp` was written by Lars Müller and is licensed under the MIT license: + +> Copyright 2024 Lars Müller +> +> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## Bug Bounty + +I offer a reward of one (1) virtual headpat per valid bug report. diff --git a/lib/tiniergltf/tiniergltf.hpp b/lib/tiniergltf/tiniergltf.hpp new file mode 100644 index 000000000..35440f5dd --- /dev/null +++ b/lib/tiniergltf/tiniergltf.hpp @@ -0,0 +1,1482 @@ +#pragma once + +#include +#include "util/base64.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace tiniergltf { + +static inline void check(bool cond) { + if (!cond) + throw std::runtime_error("invalid glTF"); +} + +template +static inline void checkIndex(const std::optional> &vec, + const std::optional &i) { + if (!i.has_value()) return; + check(vec.has_value()); + check(i < vec->size()); +} + +template +static inline void checkIndex(const std::vector &vec, + const std::optional &i) { + if (!i.has_value()) return; + check(i < vec.size()); +} + +template +static inline void checkForall(const std::optional> &vec, const F &cond) { + if (!vec.has_value()) + return; + for (const T &v : vec.value()) + cond(v); +} + +template +static inline void checkDuplicateFree(const std::vector &vec) { + check(std::unordered_set(vec.begin(), vec.end()).size() == vec.size()); +} + +template +static inline T as(const Json::Value &o); + +template<> +bool as(const Json::Value &o) { + check(o.isBool()); + return o.asBool(); +} + +template<> +double as (const Json::Value &o) { + check(o.isDouble()); + return o.asDouble(); +} + +template<> +std::size_t as(const Json::Value &o) { + check(o.isUInt64()); + auto u = o.asUInt64(); + check(u <= std::numeric_limits::max()); + return u; +} + +template<> +std::string as(const Json::Value &o) { + check(o.isString()); + return o.asString(); +} + +template +std::vector asVec(const Json::Value &o) { + check(o.isArray()); + std::vector res; + res.reserve(o.size()); + for (Json::ArrayIndex i = 0; i < o.size(); ++i) { + res.push_back(as(o[i])); + } + return res; +} + +template +std::array asArr(const Json::Value &o) { + check(o.isArray()); + check(o.size() == n); + std::array res; + for (Json::ArrayIndex i = 0; i < n; ++i) { + res[i] = as(o[i]); + } + return res; +} + +struct AccessorSparseIndices { + std::size_t bufferView; + std::size_t byteOffset; + // as defined in the glTF specification + enum class ComponentType { + UNSIGNED_BYTE, + UNSIGNED_SHORT, + UNSIGNED_INT, + }; + ComponentType componentType; + std::size_t componentSize() const { + switch (componentType) { + case ComponentType::UNSIGNED_BYTE: + return 1; + case ComponentType::UNSIGNED_SHORT: + return 2; + case ComponentType::UNSIGNED_INT: + return 4; + } + throw std::logic_error("invalid component type"); + } + std::size_t elementSize() const { + return componentSize(); + } + AccessorSparseIndices(const Json::Value &o) + : bufferView(as(o["bufferView"])) + , byteOffset(0) + { + check(o.isObject()); + if (o.isMember("byteOffset")) { + byteOffset = as(o["byteOffset"]); + check(byteOffset >= 0); + } + { + static std::unordered_map map = { + {5121, ComponentType::UNSIGNED_BYTE}, + {5123, ComponentType::UNSIGNED_SHORT}, + {5125, ComponentType::UNSIGNED_INT}, + }; + const auto &v = o["componentType"]; check(v.isUInt64()); + componentType = map.at(v.asUInt64()); + } + } +}; +template<> AccessorSparseIndices as(const Json::Value &o) { return o; } + +struct AccessorSparseValues { + std::size_t bufferView; + std::size_t byteOffset; + AccessorSparseValues(const Json::Value &o) + : bufferView(as(o["bufferView"])) + , byteOffset(0) + { + check(o.isObject()); + if (o.isMember("byteOffset")) { + byteOffset = as(o["byteOffset"]); + check(byteOffset >= 0); + } + } +}; +template<> AccessorSparseValues as(const Json::Value &o) { return o; } + +struct AccessorSparse { + std::size_t count; + AccessorSparseIndices indices; + AccessorSparseValues values; + AccessorSparse(const Json::Value &o) + : count(as(o["count"])) + , indices(as(o["indices"])) + , values(as(o["values"])) + { + check(o.isObject()); + check(count >= 1); + } +}; +template<> AccessorSparse as(const Json::Value &o) { return o; } + +struct Accessor { + std::optional bufferView; + std::size_t byteOffset; + // as defined in the glTF specification + enum class ComponentType { + BYTE, + UNSIGNED_BYTE, + SHORT, + UNSIGNED_SHORT, + UNSIGNED_INT, + FLOAT, + }; + ComponentType componentType; + std::size_t componentSize() const { + switch (componentType) { + case ComponentType::BYTE: + case ComponentType::UNSIGNED_BYTE: + return 1; + case ComponentType::SHORT: + case ComponentType::UNSIGNED_SHORT: + return 2; + case ComponentType::UNSIGNED_INT: + case ComponentType::FLOAT: + return 4; + } + throw std::logic_error("invalid component type"); + } + std::size_t count; + std::optional> max; + std::optional> min; + std::optional name; + bool normalized; + std::optional sparse; + enum class Type { + MAT2, + MAT3, + MAT4, + SCALAR, + VEC2, + VEC3, + VEC4, + }; + std::size_t typeCount() const { + switch (type) { + case Type::SCALAR: + return 1; + case Type::VEC2: + return 2; + case Type::VEC3: + return 3; + case Type::MAT2: + case Type::VEC4: + return 4; + case Type::MAT3: + return 9; + case Type::MAT4: + return 16; + } + throw std::logic_error("invalid type"); + } + Type type; + std::size_t elementSize() const { + return componentSize() * typeCount(); + } + Accessor(const Json::Value &o) + : byteOffset(0) + , count(as(o["count"])) + , normalized(false) + { + check(o.isObject()); + if (o.isMember("bufferView")) { + bufferView = as(o["bufferView"]); + } + { + static std::unordered_map map = { + {5120, ComponentType::BYTE}, + {5121, ComponentType::UNSIGNED_BYTE}, + {5122, ComponentType::SHORT}, + {5123, ComponentType::UNSIGNED_SHORT}, + {5125, ComponentType::UNSIGNED_INT}, + {5126, ComponentType::FLOAT}, + }; + const auto &v = o["componentType"]; check(v.isUInt64()); + componentType = map.at(v.asUInt64()); + } + if (o.isMember("byteOffset")) { + byteOffset = as(o["byteOffset"]); + check(byteOffset >= 0); + check(byteOffset % componentSize() == 0); + } + check(count >= 1); + if (o.isMember("name")) { + name = as(o["name"]); + } + if (o.isMember("normalized")) { + normalized = as(o["normalized"]); + } + if (o.isMember("sparse")) { + sparse = as(o["sparse"]); + } + { + static std::unordered_map map = { + {"MAT2", Type::MAT2}, + {"MAT3", Type::MAT3}, + {"MAT4", Type::MAT4}, + {"SCALAR", Type::SCALAR}, + {"VEC2", Type::VEC2}, + {"VEC3", Type::VEC3}, + {"VEC4", Type::VEC4}, + }; + const auto &v = o["type"]; check(v.isString()); + type = map.at(v.asString()); + } + if (o.isMember("max")) { + max = asVec(o["max"]); + check(max->size() == typeCount()); + } + if (o.isMember("min")) { + min = asVec(o["min"]); + check(min->size() == typeCount()); + } + } +}; +template<> Accessor as(const Json::Value &o) { return o; } + +struct AnimationChannelTarget { + std::optional node; + enum class Path { + ROTATION, + SCALE, + TRANSLATION, + WEIGHTS, + }; + Path path; + AnimationChannelTarget(const Json::Value &o) + { + check(o.isObject()); + if (o.isMember("node")) { + node = as(o["node"]); + } + { + static std::unordered_map map = { + {"rotation", Path::ROTATION}, + {"scale", Path::SCALE}, + {"translation", Path::TRANSLATION}, + {"weights", Path::WEIGHTS}, + }; + const auto &v = o["path"]; check(v.isString()); + path = map.at(v.asString()); + } + } +}; +template<> AnimationChannelTarget as(const Json::Value &o) { return o; } + +struct AnimationChannel { + std::size_t sampler; + AnimationChannelTarget target; + AnimationChannel(const Json::Value &o) + : sampler(as(o["sampler"])) + , target(as(o["target"])) + { + check(o.isObject()); + } +}; +template<> AnimationChannel as(const Json::Value &o) { return o; } + +struct AnimationSampler { + std::size_t input; + enum class Interpolation { + CUBICSPLINE, + LINEAR, + STEP, + }; + Interpolation interpolation; + std::size_t output; + AnimationSampler(const Json::Value &o) + : input(as(o["input"])) + , interpolation(Interpolation::LINEAR) + , output(as(o["output"])) + { + check(o.isObject()); + if (o.isMember("interpolation")) { + static std::unordered_map map = { + {"CUBICSPLINE", Interpolation::CUBICSPLINE}, + {"LINEAR", Interpolation::LINEAR}, + {"STEP", Interpolation::STEP}, + }; + const auto &v = o["interpolation"]; check(v.isString()); + interpolation = map.at(v.asString()); + } + } +}; +template<> AnimationSampler as(const Json::Value &o) { return o; } + +struct Animation { + std::vector channels; + std::optional name; + std::vector samplers; + Animation(const Json::Value &o) + : channels(asVec(o["channels"])) + , samplers(asVec(o["samplers"])) + { + check(o.isObject()); + check(channels.size() >= 1); + if (o.isMember("name")) { + name = as(o["name"]); + } + check(samplers.size() >= 1); + } +}; +template<> Animation as(const Json::Value &o) { return o; } + +struct Asset { + std::optional copyright; + std::optional generator; + std::optional minVersion; + std::string version; + Asset(const Json::Value &o) + : version(as(o["version"])) + { + check(o.isObject()); + if (o.isMember("copyright")) { + copyright = as(o["copyright"]); + } + if (o.isMember("generator")) { + generator = as(o["generator"]); + } + if (o.isMember("minVersion")) { + minVersion = as(o["minVersion"]); + } + } +}; +template<> Asset as(const Json::Value &o) { return o; } + +struct BufferView { + std::size_t buffer; + std::size_t byteLength; + std::size_t byteOffset; + std::optional byteStride; + std::optional name; + enum class Target { + ARRAY_BUFFER, + ELEMENT_ARRAY_BUFFER, + }; + std::optional target; + BufferView(const Json::Value &o) + : buffer(as(o["buffer"])) + , byteLength(as(o["byteLength"])) + , byteOffset(0) + { + check(o.isObject()); + check(byteLength >= 1); + if (o.isMember("byteOffset")) { + byteOffset = as(o["byteOffset"]); + check(byteOffset >= 0); + } + if (o.isMember("byteStride")) { + byteStride = as(o["byteStride"]); + check(byteStride.value() >= 4); + check(byteStride.value() <= 252); + check(byteStride.value() % 4 == 0); + } + if (o.isMember("name")) { + name = as(o["name"]); + } + if (o.isMember("target")) { + static std::unordered_map map = { + {34962, Target::ARRAY_BUFFER}, + {34963, Target::ELEMENT_ARRAY_BUFFER}, + }; + const auto &v = o["target"]; check(v.isUInt64()); + target = map.at(v.asUInt64()); + } + } +}; +template<> BufferView as(const Json::Value &o) { return o; } + +struct Buffer { + std::size_t byteLength; + std::optional name; + std::string data; + Buffer(const Json::Value &o, + const std::function &resolveURI, + std::optional &&glbData = std::nullopt) + : byteLength(as(o["byteLength"])) + { + check(o.isObject()); + check(byteLength >= 1); + if (o.isMember("name")) { + name = as(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(o["uri"]); + for (auto &prefix : std::array { + "data:application/octet-stream;base64,", + "data:application/gltf-buffer;base64," + }) { + if (std::string_view(uri).substr(0, prefix.length()) == prefix) { + auto view = std::string_view(uri).substr(prefix.length()); + check(base64_is_valid(view)); + data = base64_decode(view); + dataURI = true; + break; + } + } + if (!dataURI) + data = resolveURI(uri); + check(data.size() >= byteLength); + } + data.resize(byteLength); + } +}; + +struct CameraOrthographic { + double xmag; + double ymag; + double zfar; + double znear; + CameraOrthographic(const Json::Value &o) + : xmag(as(o["xmag"])) + , ymag(as(o["ymag"])) + , zfar(as(o["zfar"])) + , znear(as(o["znear"])) + { + check(o.isObject()); + check(zfar > 0); + check(znear >= 0); + } +}; +template<> CameraOrthographic as(const Json::Value &o) { return o; } + +struct CameraPerspective { + std::optional aspectRatio; + double yfov; + std::optional zfar; + double znear; + CameraPerspective(const Json::Value &o) + : yfov(as(o["yfov"])) + , znear(as(o["znear"])) + { + check(o.isObject()); + if (o.isMember("aspectRatio")) { + aspectRatio = as(o["aspectRatio"]); + check(aspectRatio.value() > 0); + } + check(yfov > 0); + if (o.isMember("zfar")) { + zfar = as(o["zfar"]); + check(zfar.value() > 0); + } + check(znear > 0); + } +}; +template<> CameraPerspective as(const Json::Value &o) { return o; } + +struct Camera { + std::optional name; + std::optional orthographic; + std::optional perspective; + enum class Type { + ORTHOGRAPHIC, + PERSPECTIVE, + }; + Type type; + Camera(const Json::Value &o) + { + check(o.isObject()); + if (o.isMember("name")) { + name = as(o["name"]); + } + if (o.isMember("orthographic")) { + orthographic = as(o["orthographic"]); + } + if (o.isMember("perspective")) { + perspective = as(o["perspective"]); + } + { + static std::unordered_map map = { + {"orthographic", Type::ORTHOGRAPHIC}, + {"perspective", Type::PERSPECTIVE}, + }; + const auto &v = o["type"]; check(v.isString()); + type = map.at(v.asString()); + } + } +}; +template<> Camera as(const Json::Value &o) { return o; } + +struct Image { + std::optional bufferView; + enum class MimeType { + IMAGE_JPEG, + IMAGE_PNG, + }; + std::optional mimeType; + std::optional name; + std::optional uri; + Image(const Json::Value &o) + { + check(o.isObject()); + if (o.isMember("bufferView")) { + bufferView = as(o["bufferView"]); + } + if (o.isMember("mimeType")) { + static std::unordered_map map = { + {"image/jpeg", MimeType::IMAGE_JPEG}, + {"image/png", MimeType::IMAGE_PNG}, + }; + const auto &v = o["mimeType"]; check(v.isString()); + mimeType = map.at(v.asString()); + } + if (o.isMember("name")) { + name = as(o["name"]); + } + if (o.isMember("uri")) { + uri = as(o["uri"]); + } + } +}; +template<> Image as(const Json::Value &o) { return o; } + +struct TextureInfo { + std::size_t index; + std::size_t texCoord; + TextureInfo(const Json::Value &o) + : index(as(o["index"])) + , texCoord(0) + { + check(o.isObject()); + if (o.isMember("texCoord")) { + texCoord = as(o["texCoord"]); + check(texCoord >= 0); + } + } +}; +template<> TextureInfo as(const Json::Value &o) { return o; } + +struct MaterialNormalTextureInfo { + std::size_t index; + double scale; + std::size_t texCoord; + MaterialNormalTextureInfo(const Json::Value &o) + : index(as(o["index"])) + , scale(1) + , texCoord(0) + { + check(o.isObject()); + if (o.isMember("scale")) { + scale = as(o["scale"]); + } + if (o.isMember("texCoord")) { + texCoord = as(o["texCoord"]); + } + } +}; +template<> MaterialNormalTextureInfo as(const Json::Value &o) { return o; } + +struct MaterialOcclusionTextureInfo { + std::size_t index; + double strength; + std::size_t texCoord; + MaterialOcclusionTextureInfo(const Json::Value &o) + : index(as(o["index"])) + , strength(1) + , texCoord(0) + { + check(o.isObject()); + if (o.isMember("strength")) { + strength = as(o["strength"]); + check(strength >= 0); + check(strength <= 1); + } + if (o.isMember("texCoord")) { + texCoord = as(o["texCoord"]); + } + } +}; +template<> MaterialOcclusionTextureInfo as(const Json::Value &o) { return o; } + +struct MaterialPbrMetallicRoughness { + std::array baseColorFactor; + std::optional baseColorTexture; + double metallicFactor; + std::optional metallicRoughnessTexture; + double roughnessFactor; + MaterialPbrMetallicRoughness(const Json::Value &o) + : baseColorFactor{1, 1, 1, 1} + , metallicFactor(1) + , roughnessFactor(1) + { + check(o.isObject()); + if (o.isMember("baseColorFactor")) { + baseColorFactor = asArr(o["baseColorFactor"]); + for (auto v: baseColorFactor) { + check(v >= 0); + check(v <= 1); + } + } + if (o.isMember("baseColorTexture")) { + baseColorTexture = as(o["baseColorTexture"]); + } + if (o.isMember("metallicFactor")) { + metallicFactor = as(o["metallicFactor"]); + check(metallicFactor >= 0); + check(metallicFactor <= 1); + } + if (o.isMember("metallicRoughnessTexture")) { + metallicRoughnessTexture = as(o["metallicRoughnessTexture"]); + } + if (o.isMember("roughnessFactor")) { + roughnessFactor = as(o["roughnessFactor"]); + check(roughnessFactor >= 0); + check(roughnessFactor <= 1); + } + } +}; +template<> MaterialPbrMetallicRoughness as(const Json::Value &o) { return o; } + +struct Material { + double alphaCutoff; + enum class AlphaMode { + BLEND, + MASK, + OPAQUE, + }; + AlphaMode alphaMode; + bool doubleSided; + std::array emissiveFactor; + std::optional emissiveTexture; + std::optional name; + std::optional normalTexture; + std::optional occlusionTexture; + std::optional pbrMetallicRoughness; + Material(const Json::Value &o) + : alphaCutoff(0.5) + , alphaMode(AlphaMode::OPAQUE) + , doubleSided(false) + , emissiveFactor{0, 0, 0} + { + check(o.isObject()); + if (o.isMember("alphaCutoff")) { + alphaCutoff = as(o["alphaCutoff"]); + check(alphaCutoff >= 0); + } + if (o.isMember("alphaMode")){ + static std::unordered_map map = { + {"BLEND", AlphaMode::BLEND}, + {"MASK", AlphaMode::MASK}, + {"OPAQUE", AlphaMode::OPAQUE}, + }; + const auto &v = o["alphaMode"]; check(v.isString()); + alphaMode = map.at(v.asString()); + } + if (o.isMember("doubleSided")) { + doubleSided = as(o["doubleSided"]); + } + if (o.isMember("emissiveFactor")) { + emissiveFactor = asArr(o["emissiveFactor"]); + for (const auto &v: emissiveFactor) { + check(v >= 0); + check(v <= 1); + } + } + if (o.isMember("emissiveTexture")) { + emissiveTexture = as(o["emissiveTexture"]); + } + if (o.isMember("name")) { + name = as(o["name"]); + } + if (o.isMember("normalTexture")) { + normalTexture = as(o["normalTexture"]); + } + if (o.isMember("occlusionTexture")) { + occlusionTexture = as(o["occlusionTexture"]); + } + if (o.isMember("pbrMetallicRoughness")) { + pbrMetallicRoughness = as(o["pbrMetallicRoughness"]); + } + } +}; +template<> Material as(const Json::Value &o) { return o; } + +struct MeshPrimitive { + static void enumeratedProps(const Json::Value &o, const std::string &name, std::optional> &attr) { + for (std::size_t i = 0;; ++i) { + const std::string s = name + "_" + std::to_string(i); + if (!o.isMember(s)) break; + if (i == 0) { + attr = std::vector(); + } + attr->push_back(as(o[s])); + } + } + struct Attributes { + std::optional position, normal, tangent; + std::optional> texcoord, color, joints, weights; + Attributes(const Json::Value &o) { + if (o.isMember("POSITION")) + position = as(o["POSITION"]); + if (o.isMember("NORMAL")) + normal = as(o["NORMAL"]); + if (o.isMember("TANGENT")) + tangent = as(o["TANGENT"]); + enumeratedProps(o, "TEXCOORD", texcoord); + enumeratedProps(o, "COLOR", color); + enumeratedProps(o, "JOINTS", joints); + enumeratedProps(o, "WEIGHTS", weights); + check(joints.has_value() == weights.has_value()); + if (joints.has_value()) { + check(joints->size() == weights->size()); + } + check(position.has_value() + || normal.has_value() + || tangent.has_value() + || texcoord.has_value() + || color.has_value() + || joints.has_value() + || weights.has_value()); + } + }; + Attributes attributes; + std::optional indices; + std::optional material; + enum class Mode { + POINTS, + LINES, + LINE_LOOP, + LINE_STRIP, + TRIANGLES, + TRIANGLE_STRIP, + TRIANGLE_FAN, + }; + Mode mode; + struct MorphTargets { + std::optional position, normal, tangent; + std::optional> texcoord, color; + MorphTargets(const Json::Value &o) { + if (o.isMember("POSITION")) + position = as(o["POSITION"]); + if (o.isMember("NORMAL")) + normal = as(o["NORMAL"]); + if (o.isMember("TANGENT")) + tangent = as(o["TANGENT"]); + enumeratedProps(o, "TEXCOORD", texcoord); + enumeratedProps(o, "COLOR", color); + check(position.has_value() + || normal.has_value() + || tangent.has_value() + || texcoord.has_value() + || color.has_value()); + } + }; + std::optional> targets; + MeshPrimitive(const Json::Value &o) + : attributes(Attributes(o["attributes"])) + , mode(Mode::TRIANGLES) + { + check(o.isObject()); + if (o.isMember("indices")) { + indices = as(o["indices"]); + } + if (o.isMember("material")) { + material = as(o["material"]); + } + if (o.isMember("mode")) { + static std::unordered_map map = { + {0, Mode::POINTS}, + {1, Mode::LINES}, + {2, Mode::LINE_LOOP}, + {3, Mode::LINE_STRIP}, + {4, Mode::TRIANGLES}, + {5, Mode::TRIANGLE_STRIP}, + {6, Mode::TRIANGLE_FAN}, + }; + const auto &v = o["mode"]; check(v.isUInt64()); + mode = map.at(v.asUInt64()); + } + if (o.isMember("targets")) { + targets = asVec(o["targets"]); + check(targets->size() >= 1); + } + } +}; +template<> MeshPrimitive::MorphTargets as(const Json::Value &o) { return o; } +template<> MeshPrimitive as(const Json::Value &o) { return o; } + +struct Mesh { + std::optional name; + std::vector primitives; + std::optional> weights; + Mesh(const Json::Value &o) + : primitives(asVec(o["primitives"])) + { + check(o.isObject()); + if (o.isMember("name")) { + name = as(o["name"]); + } + check(primitives.size() >= 1); + if (o.isMember("weights")) { + weights = asVec(o["weights"]); + check(weights->size() >= 1); + } + } +}; +template<> Mesh as(const Json::Value &o) { return o; } + +struct Node { + std::optional camera; + std::optional> children; + typedef std::array Matrix; + struct TRS { + std::array translation = {0, 0, 0}; + std::array rotation = {0, 0, 0, 1}; + std::array scale = {1, 1, 1}; + }; + std::variant transform; + std::optional mesh; + std::optional name; + std::optional skin; + std::optional> weights; + Node(const Json::Value &o) + : transform(Matrix { + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + }) + { + check(o.isObject()); + if (o.isMember("camera")) { + camera = as(o["camera"]); + } + if (o.isMember("children")) { + children = asVec(o["children"]); + check(children->size() >= 1); + checkDuplicateFree(*children); + } + bool hasTRS = o.isMember("translation") || o.isMember("rotation") || o.isMember("scale"); + if (o.isMember("matrix")) { + check(!hasTRS); + transform = asArr(o["matrix"]); + } else if (hasTRS) { + TRS trs; + if (o.isMember("translation")) { + trs.translation = asArr(o["translation"]); + } + if (o.isMember("rotation")) { + trs.rotation = asArr(o["rotation"]); + for (auto v: trs.rotation) { + check(v >= -1); + check(v <= 1); + } + } + if (o.isMember("scale")) { + trs.scale = asArr(o["scale"]); + } + transform = trs; + } + if (o.isMember("mesh")) { + mesh = as(o["mesh"]); + } + if (o.isMember("name")) { + name = as(o["name"]); + } + if (o.isMember("skin")) { + check(mesh.has_value()); + skin = as(o["skin"]); + } + if (o.isMember("weights")) { + weights = asVec(o["weights"]); + check(weights->size() >= 1); + } + } +}; +template<> Node as(const Json::Value &o) { return o; } + +struct Sampler { + enum class MagFilter { + NEAREST, + LINEAR, + }; + std::optional magFilter; + enum class MinFilter { + NEAREST, + LINEAR, + NEAREST_MIPMAP_NEAREST, + LINEAR_MIPMAP_NEAREST, + NEAREST_MIPMAP_LINEAR, + LINEAR_MIPMAP_LINEAR, + }; + std::optional minFilter; + std::optional name; + enum class Wrap { + REPEAT, + CLAMP_TO_EDGE, + MIRRORED_REPEAT, + }; + Wrap wrapS; + Wrap wrapT; + Sampler(const Json::Value &o) + : wrapS(Wrap::REPEAT) + , wrapT(Wrap::REPEAT) + { + check(o.isObject()); + if (o.isMember("magFilter")) { + static std::unordered_map map = { + {9728, MagFilter::NEAREST}, + {9729, MagFilter::LINEAR}, + }; + const auto &v = o["magFilter"]; check(v.isUInt64()); + magFilter = map.at(v.asUInt64()); + } + if (o.isMember("minFilter")) { + static std::unordered_map map = { + {9728, MinFilter::NEAREST}, + {9729, MinFilter::LINEAR}, + {9984, MinFilter::NEAREST_MIPMAP_NEAREST}, + {9985, MinFilter::LINEAR_MIPMAP_NEAREST}, + {9986, MinFilter::NEAREST_MIPMAP_LINEAR}, + {9987, MinFilter::LINEAR_MIPMAP_LINEAR}, + }; + const auto &v = o["minFilter"]; check(v.isUInt64()); + minFilter = map.at(v.asUInt64()); + } + if (o.isMember("name")) { + name = as(o["name"]); + } + static std::unordered_map map = { + {10497, Wrap::REPEAT}, + {33071, Wrap::CLAMP_TO_EDGE}, + {33648, Wrap::MIRRORED_REPEAT}, + }; + if (o.isMember("wrapS")) { + const auto &v = o["wrapS"]; check(v.isUInt64()); + wrapS = map.at(v.asUInt64()); + } + if (o.isMember("wrapT")) { + const auto &v = o["wrapT"]; check(v.isUInt64()); + wrapT = map.at(v.asUInt64()); + } + } +}; +template<> Sampler as(const Json::Value &o) { return o; } + +struct Scene { + std::optional name; + std::optional> nodes; + Scene(const Json::Value &o) + { + check(o.isObject()); + if (o.isMember("name")) { + name = as(o["name"]); + } + if (o.isMember("nodes")) { + nodes = asVec(o["nodes"]); + check(nodes->size() >= 1); + checkDuplicateFree(*nodes); + } + } +}; +template<> Scene as(const Json::Value &o) { return o; } + +struct Skin { + std::optional inverseBindMatrices; + std::vector joints; + std::optional name; + std::optional skeleton; + Skin(const Json::Value &o) + : joints(asVec(o["joints"])) + { + check(o.isObject()); + if (o.isMember("inverseBindMatrices")) { + inverseBindMatrices = as(o["inverseBindMatrices"]); + } + check(joints.size() >= 1); + checkDuplicateFree(joints); + if (o.isMember("name")) { + name = as(o["name"]); + } + if (o.isMember("skeleton")) { + skeleton = as(o["skeleton"]); + } + } +}; +template<> Skin as(const Json::Value &o) { return o; } + +struct Texture { + std::optional name; + std::optional sampler; + std::optional source; + Texture(const Json::Value &o) + { + check(o.isObject()); + if (o.isMember("name")) { + name = as(o["name"]); + } + if (o.isMember("sampler")) { + sampler = as(o["sampler"]); + } + if (o.isMember("source")) { + source = as(o["source"]); + } + } +}; +template<> Texture as(const Json::Value &o) { return o; } + +using UriResolver = std::function; +static inline std::string uriError(const std::string &uri) { + // only base64 data URI support by default + throw std::runtime_error("unsupported URI: " + uri); +} + +struct GlTF { + std::optional> accessors; + std::optional> animations; + Asset asset; + std::optional> bufferViews; + std::optional> buffers; + std::optional> cameras; + std::optional> extensionsRequired; + std::optional> extensionsUsed; + std::optional> images; + std::optional> materials; + std::optional> meshes; + std::optional> nodes; + std::optional> samplers; + std::optional scene; + std::optional> scenes; + std::optional> skins; + std::optional> textures; + + GlTF(const Json::Value &o, + const UriResolver &resolveUri = uriError, + std::optional &&glbData = std::nullopt) + : asset(as(o["asset"])) + { + check(o.isObject()); + if (o.isMember("accessors")) { + accessors = asVec(o["accessors"]); + check(accessors->size() >= 1); + } + if (o.isMember("animations")) { + animations = asVec(o["animations"]); + check(animations->size() >= 1); + } + if (o.isMember("bufferViews")) { + bufferViews = asVec(o["bufferViews"]); + check(bufferViews->size() >= 1); + } + if (o.isMember("buffers")) { + auto b = o["buffers"]; + check(b.isArray()); + std::vector bufs; + bufs.reserve(b.size()); + for (Json::ArrayIndex i = 0; i < b.size(); ++i) { + bufs.emplace_back(b[i], resolveUri, + i == 0 ? std::move(glbData) : std::nullopt); + } + check(bufs.size() >= 1); + buffers = std::move(bufs); + } + if (o.isMember("cameras")) { + cameras = asVec(o["cameras"]); + check(cameras->size() >= 1); + } + if (o.isMember("extensionsRequired")) { + extensionsRequired = asVec(o["extensionsRequired"]); + check(extensionsRequired->size() >= 1); + checkDuplicateFree(*extensionsRequired); + } + if (o.isMember("extensionsUsed")) { + extensionsUsed = asVec(o["extensionsUsed"]); + check(extensionsUsed->size() >= 1); + checkDuplicateFree(*extensionsUsed); + } + if (o.isMember("images")) { + images = asVec(o["images"]); + check(images->size() >= 1); + } + if (o.isMember("materials")) { + materials = asVec(o["materials"]); + check(materials->size() >= 1); + } + if (o.isMember("meshes")) { + meshes = asVec(o["meshes"]); + check(meshes->size() >= 1); + } + if (o.isMember("nodes")) { + nodes = asVec(o["nodes"]); + check(nodes->size() >= 1); + // Nodes must be a forest: + // 1. Each node should have indegree 0 or 1: + std::vector indeg(nodes->size()); + for (std::size_t i = 0; i < nodes->size(); ++i) { + auto children = nodes->at(i).children; + if (!children.has_value()) continue; + for (auto child : children.value()) { + ++indeg.at(child); + } + } + for (const auto deg : indeg) { + check(deg <= 1); + } + // 2. There should be no cycles: + std::vector visited(nodes->size()); + std::stack> toVisit; + for (std::size_t i = 0; i < nodes->size(); ++i) { + // Only start DFS in roots. + if (indeg[i] > 0) + continue; + + toVisit.push(i); + do { + std::size_t j = toVisit.top(); + check(!visited.at(j)); + visited[j] = true; + toVisit.pop(); + auto children = nodes->at(j).children; + if (!children.has_value()) + continue; + for (auto child : *children) { + toVisit.push(child); + } + } while (!toVisit.empty()); + } + } + if (o.isMember("samplers")) { + samplers = asVec(o["samplers"]); + check(samplers->size() >= 1); + } + if (o.isMember("scene")) { + scene = as(o["scene"]); + } + if (o.isMember("scenes")) { + scenes = asVec(o["scenes"]); + check(scenes->size() >= 1); + } + if (o.isMember("skins")) { + skins = asVec(o["skins"]); + check(skins->size() >= 1); + } + if (o.isMember("textures")) { + textures = asVec(o["textures"]); + check(textures->size() >= 1); + } + + // Validation + + checkForall(bufferViews, [&](const BufferView &view) { + check(buffers.has_value()); + const Buffer &buf = buffers->at(view.buffer); + // Be careful because of possible integer overflows. + check(view.byteOffset < buf.byteLength); + check(view.byteLength <= buf.byteLength); + check(view.byteOffset <= buf.byteLength - view.byteLength); + }); + + const auto checkAccessor = [&](const auto &accessor, + std::size_t bufferView, std::size_t byteOffset, std::size_t count) { + const BufferView &view = bufferViews->at(bufferView); + if (view.byteStride.has_value()) + check(*view.byteStride % accessor.componentSize() == 0); + check(byteOffset < view.byteLength); + // Use division to avoid overflows. + const auto effective_byte_stride = view.byteStride.value_or(accessor.elementSize()); + check(count <= (view.byteLength - byteOffset) / effective_byte_stride); + }; + checkForall(accessors, [&](const Accessor &accessor) { + if (accessor.bufferView.has_value()) + checkAccessor(accessor, *accessor.bufferView, accessor.byteOffset, accessor.count); + if (accessor.sparse.has_value()) { + const auto &indices = accessor.sparse->indices; + checkAccessor(indices, indices.bufferView, indices.byteOffset, accessor.sparse->count); + const auto &values = accessor.sparse->values; + checkAccessor(accessor, values.bufferView, values.byteOffset, accessor.sparse->count); + } + }); + + checkForall(images, [&](const Image &image) { + checkIndex(bufferViews, image.bufferView); + }); + + checkForall(meshes, [&](const Mesh &mesh) { + for (const auto &primitive : mesh.primitives) { + checkIndex(accessors, primitive.indices); + checkIndex(materials, primitive.material); + checkIndex(accessors, primitive.attributes.normal); + checkIndex(accessors, primitive.attributes.position); + checkIndex(accessors, primitive.attributes.tangent); + checkForall(primitive.attributes.texcoord, [&](const std::size_t &i) { + checkIndex(accessors, i); + }); + checkForall(primitive.attributes.color, [&](const std::size_t &i) { + checkIndex(accessors, i); + }); + checkForall(primitive.attributes.joints, [&](const std::size_t &i) { + checkIndex(accessors, i); + }); + checkForall(primitive.attributes.weights, [&](const std::size_t &i) { + checkIndex(accessors, i); + }); + if (primitive.material.has_value()) { + const Material &material = materials->at(primitive.material.value()); + if (material.emissiveTexture.has_value()) { + check(primitive.attributes.texcoord.has_value()); + check(material.emissiveTexture->texCoord < primitive.attributes.texcoord->size()); + } + if (material.normalTexture.has_value()) { + check(primitive.attributes.texcoord.has_value()); + check(material.normalTexture->texCoord < primitive.attributes.texcoord->size()); + } + if (material.occlusionTexture.has_value()) { + check(primitive.attributes.texcoord.has_value()); + check(material.occlusionTexture->texCoord < primitive.attributes.texcoord->size()); + } + } + checkForall(primitive.targets, [&](const MeshPrimitive::MorphTargets &target) { + checkIndex(accessors, target.normal); + checkIndex(accessors, target.position); + checkIndex(accessors, target.tangent); + checkForall(target.texcoord, [&](const std::size_t &i) { + checkIndex(accessors, i); + }); + checkForall(target.color, [&](const std::size_t &i) { + checkIndex(accessors, i); + }); + }); + } + }); + + checkForall(nodes, [&](const Node &node) { + checkIndex(cameras, node.camera); + checkIndex(meshes, node.mesh); + checkIndex(skins, node.skin); + }); + + checkForall(scenes, [&](const Scene &scene) { + checkForall(scene.nodes, [&](const size_t &i) { + checkIndex(nodes, i); + }); + }); + + checkForall(skins, [&](const Skin &skin) { + checkIndex(accessors, skin.inverseBindMatrices); + for (const std::size_t &i : skin.joints) + checkIndex(nodes, i); + checkIndex(nodes, skin.skeleton); + }); + + checkForall(textures, [&](const Texture &texture) { + checkIndex(samplers, texture.sampler); + checkIndex(images, texture.source); + }); + + checkForall(animations, [&](const Animation &animation) { + for (const auto &sampler : animation.samplers) { + checkIndex(accessors, sampler.input); + const auto &accessor = accessors->at(sampler.input); + check(accessor.type == Accessor::Type::SCALAR); + check(accessor.componentType == Accessor::ComponentType::FLOAT); + checkIndex(accessors, sampler.output); + } + for (const auto &channel : animation.channels) { + checkIndex(nodes, channel.target.node); + checkIndex(animation.samplers, channel.sampler); + } + }); + + checkIndex(scenes, scene); + } +}; + +// std::span is C++ 20, so we roll our own little struct here. +template +struct Span { + T *ptr; + uint32_t len; + bool empty() const { + return len == 0; + } + T *end() const { + return ptr + len; + } + template + Span cast() const { + return {(U *) ptr, len}; + } +}; + +static Json::Value readJson(Span span) { + Json::CharReaderBuilder builder; + const std::unique_ptr reader(builder.newCharReader()); + Json::Value json; + JSONCPP_STRING err; + if (!reader->parse(span.ptr, span.end(), &json, &err)) + throw std::runtime_error(std::string("invalid JSON: ") + err); + return json; +} + +inline GlTF readGlb(const char *data, std::size_t len, const UriResolver &resolveUri = uriError) { + struct Chunk { + uint32_t type; + Span span; + }; + + struct Stream { + Span span; + + bool eof() const { + return span.empty(); + } + + void advance(uint32_t n) { + span.len -= n; + span.ptr += n; + } + + uint32_t readUint32() { + if (span.len < 4) + throw std::runtime_error("premature EOF"); + uint32_t res = 0; + for (int i = 0; i < 4; ++i) + res += span.ptr[i] << (i * 8); + advance(4); + return res; + } + + Chunk readChunk() { + const auto chunkLen = readUint32(); + if (chunkLen % 4 != 0) + throw std::runtime_error("chunk length must be multiple of 4"); + const auto chunkType = readUint32(); + + auto chunkPtr = span.ptr; + if (span.len < chunkLen) + throw std::runtime_error("premature EOF"); + advance(chunkLen); + return {chunkType, {chunkPtr, chunkLen}}; + } + }; + + constexpr uint32_t MAGIC_GLTF = 0x46546C67; + constexpr uint32_t MAGIC_JSON = 0x4E4F534A; + constexpr uint32_t MAGIC_BIN = 0x004E4942; + + if (len > std::numeric_limits::max()) + throw std::runtime_error("too large"); + + Stream is{{(const uint8_t *) data, static_cast(len)}}; + + const auto magic = is.readUint32(); + if (magic != MAGIC_GLTF) + throw std::runtime_error("wrong magic number"); + const auto version = is.readUint32(); + if (version != 2) + throw std::runtime_error("wrong version"); + const auto length = is.readUint32(); + if (length != len) + throw std::runtime_error("wrong length"); + + const auto json = is.readChunk(); + if (json.type != MAGIC_JSON) + throw std::runtime_error("expected JSON chunk"); + + std::optional buffer; + if (!is.eof()) { + const auto chunk = is.readChunk(); + if (chunk.type == MAGIC_BIN) + buffer = std::string((const char *) chunk.span.ptr, chunk.span.len); + else if (chunk.type == MAGIC_JSON) + throw std::runtime_error("unexpected chunk"); + // Ignore all other chunks. We still want to validate that + // 1. These chunks are valid; + // 2. These chunks are *not* JSON or BIN chunks + while (!is.eof()) { + const auto type = is.readChunk().type; + if (type == MAGIC_JSON || type == MAGIC_BIN) + throw std::runtime_error("unexpected chunk"); + } + } + + return GlTF(readJson(json.span.cast()), resolveUri, std::move(buffer)); +} + +inline GlTF readGlTF(const char *data, std::size_t len, const UriResolver &resolveUri = uriError) { + if (len > std::numeric_limits::max()) + throw std::runtime_error("too large"); + + return GlTF(readJson({data, static_cast(len)}), resolveUri); +} + +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 955bf05f2..6dd4c05d2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -408,6 +408,7 @@ set(common_SRCS face_position_cache.cpp filesys.cpp gettext.cpp + gettext_plural_form.cpp httpfetch.cpp hud.cpp inventory.cpp @@ -648,6 +649,9 @@ if(BUILD_CLIENT) if(BUILD_UNITTESTS OR BUILD_BENCHMARKS) target_link_libraries(${PROJECT_NAME} Catch2::Catch2) endif() + if(BUILD_WITH_TRACY) + target_link_libraries(${PROJECT_NAME} Tracy::TracyClient) + endif() if(PRECOMPILE_HEADERS) target_precompile_headers(${PROJECT_NAME} PRIVATE ${PRECOMPILED_HEADERS_LIST}) @@ -715,6 +719,9 @@ if(BUILD_SERVER) if(BUILD_UNITTESTS OR BUILD_BENCHMARKS) target_link_libraries(${PROJECT_NAME}server Catch2::Catch2) endif() + if(BUILD_WITH_TRACY) + target_link_libraries(${PROJECT_NAME}server Tracy::TracyClient) + endif() if(PRECOMPILE_HEADERS) target_precompile_headers(${PROJECT_NAME}server PRIVATE ${PRECOMPILED_HEADERS_LIST}) @@ -781,7 +788,7 @@ endif() if(MSVC) # Visual Studio - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /D _WIN32_WINNT=0x0601 /D WIN32_LEAN_AND_MEAN") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /D _WIN32_WINNT=0x0601 /D WIN32_LEAN_AND_MEAN /D _CRT_SECURE_NO_WARNINGS") # EHa enables SEH exceptions (used for catching segfaults) set(CMAKE_CXX_FLAGS_RELEASE "/EHa /Ox /MD /GS- /Zi /fp:fast /D NDEBUG /D _HAS_ITERATOR_DEBUGGING=0") if(CMAKE_SIZEOF_VOID_P EQUAL 4) diff --git a/src/activeobject.h b/src/activeobject.h index 989b48e91..cb868346a 100644 --- a/src/activeobject.h +++ b/src/activeobject.h @@ -96,6 +96,9 @@ struct BoneOverride { core::quaternion previous; core::quaternion next; + // Redundantly store the euler angles serverside + // so that we can return them in the appropriate getters + v3f next_radians; bool absolute = false; f32 interp_timer = 0; } rotation; diff --git a/src/client/camera.cpp b/src/client/camera.cpp index 5e724d05e..615d30c87 100644 --- a/src/client/camera.cpp +++ b/src/client/camera.cpp @@ -38,6 +38,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "script/scripting_client.h" #include "gettext.h" #include +#include #define CAMERA_OFFSET_STEP 200 #define WIELDMESH_OFFSET_X 55.0f @@ -62,7 +63,7 @@ Camera::Camera(MapDrawControl &draw_control, Client *client, RenderingEngine *re // all other 3D scene nodes and before the GUI. m_wieldmgr = smgr->createNewSceneManager(); m_wieldmgr->addCameraSceneNode(); - m_wieldnode = new WieldMeshSceneNode(m_wieldmgr, -1, false); + m_wieldnode = new WieldMeshSceneNode(m_wieldmgr, -1); m_wieldnode->setItem(ItemStack(), m_client); m_wieldnode->drop(); // m_wieldmgr grabbed it @@ -404,10 +405,11 @@ void Camera::update(LocalPlayer* player, f32 frametime, f32 tool_reload_ratio) // Compute absolute camera position and target m_headnode->getAbsoluteTransformation().transformVect(m_camera_position, rel_cam_pos); - m_headnode->getAbsoluteTransformation().rotateVect(m_camera_direction, rel_cam_target - rel_cam_pos); + m_camera_direction = m_headnode->getAbsoluteTransformation() + .rotateAndScaleVect(rel_cam_target - rel_cam_pos); - v3f abs_cam_up; - m_headnode->getAbsoluteTransformation().rotateVect(abs_cam_up, rel_cam_up); + v3f abs_cam_up = m_headnode->getAbsoluteTransformation() + .rotateAndScaleVect(rel_cam_up); // Separate camera position for calculation v3f my_cp = m_camera_position; diff --git a/src/client/camera.h b/src/client/camera.h index 88533181b..8f85da0e1 100644 --- a/src/client/camera.h +++ b/src/client/camera.h @@ -19,7 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once -#include "irrlichttypes_extrabloated.h" +#include "irrlichttypes.h" #include "inventory.h" #include "util/numeric.h" #include "client/localplayer.h" diff --git a/src/client/client.cpp b/src/client/client.cpp index af32b6db8..7feb2212d 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -52,6 +52,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "profiler.h" #include "shader.h" #include "gettext.h" +#include "clientdynamicinfo.h" #include "clientmap.h" #include "clientmedia.h" #include "version.h" @@ -119,7 +120,7 @@ Client::Client( m_rendering_engine(rendering_engine), m_mesh_update_manager(std::make_unique(this)), m_env( - new ClientMap(this, rendering_engine, control, 666), + make_irr(this, rendering_engine, control, 666), tsrc, this ), m_particle_manager(std::make_unique(&m_env)), @@ -392,15 +393,12 @@ void Client::connect(const Address &address, const std::string &address_name, } m_address_name = address_name; - m_con.reset(new con::Connection(PROTOCOL_ID, 512, CONNECTION_TIMEOUT, - address.isIPv6(), this)); + m_con.reset(con::createMTP(CONNECTION_TIMEOUT, address.isIPv6(), this)); infostream << "Connecting to server at "; address.print(infostream); infostream << std::endl; - // Since we use TryReceive() a timeout here would be ineffective anyway - m_con->SetTimeoutMs(0); m_con->Connect(address); initLocalMapSaving(address, m_address_name, is_local_server); @@ -439,20 +437,16 @@ void Client::step(float dtime) } } - // UGLY hack to fix 2 second startup delay caused by non existent - // server client startup synchronization in local server or singleplayer mode - static bool initial_step = true; - if (initial_step) { - initial_step = false; - } - else if(m_state == LC_Created) { + // The issue that made this workaround necessary was fixed in August 2024, but + // it's not like we can remove this code - ever. + if (m_state == LC_Created) { float &counter = m_connection_reinit_timer; counter -= dtime; - if(counter <= 0.0) { - counter = 2.0; + if (counter <= 0) { + counter = 1.5f; LocalPlayer *myplayer = m_env.getLocalPlayer(); - FATAL_ERROR_IF(myplayer == NULL, "Local player not found in environment."); + FATAL_ERROR_IF(!myplayer, "Local player not found in environment"); sendInit(myplayer->getName()); } @@ -833,7 +827,7 @@ bool Client::loadMedia(const std::string &data, const std::string &filename, } const char *model_ext[] = { - ".x", ".b3d", ".obj", + ".x", ".b3d", ".obj", ".gltf", ".glb", NULL }; name = removeStringEnd(filename, model_ext); @@ -847,16 +841,12 @@ bool Client::loadMedia(const std::string &data, const std::string &filename, return true; } - const char *translate_ext[] = { - ".tr", NULL - }; - name = removeStringEnd(filename, translate_ext); - if (!name.empty()) { + if (Translations::isTranslationFile(filename)) { if (from_media_push) return false; TRACESTREAM(<< "Client: Loading translation: " << "\"" << filename << "\"" << std::endl); - g_client_translations->loadTranslation(data); + g_client_translations->loadTranslation(filename, data); return true; } @@ -866,13 +856,13 @@ bool Client::loadMedia(const std::string &data, const std::string &filename, } // Virtual methods from con::PeerHandler -void Client::peerAdded(con::Peer *peer) +void Client::peerAdded(con::IPeer *peer) { infostream << "Client::peerAdded(): peer->id=" << peer->id << std::endl; } -void Client::deletingPeer(con::Peer *peer, bool timeout) +void Client::deletingPeer(con::IPeer *peer, bool timeout) { infostream << "Client::deletingPeer(): " "Server Peer is getting deleted " @@ -1040,7 +1030,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; @@ -1052,6 +1042,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); @@ -1066,10 +1058,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) @@ -1147,11 +1142,8 @@ void Client::sendInit(const std::string &playerName) { NetworkPacket pkt(TOSERVER_INIT, 1 + 2 + 2 + (1 + playerName.size())); - // we don't support network compression yet - u16 supp_comp_modes = NETPROTO_COMPRESSION_NONE; - - pkt << (u8) SER_FMT_VER_HIGHEST_READ << (u16) supp_comp_modes; - pkt << (u16) CLIENT_PROTOCOL_VERSION_MIN << (u16) CLIENT_PROTOCOL_VERSION_MAX; + pkt << (u8) SER_FMT_VER_HIGHEST_READ << (u16) 0; + pkt << CLIENT_PROTOCOL_VERSION_MIN << LATEST_PROTOCOL_VERSION; pkt << playerName; Send(&pkt); @@ -1368,9 +1360,9 @@ void Client::sendDamage(u16 damage) Send(&pkt); } -void Client::sendRespawn() +void Client::sendRespawnLegacy() { - NetworkPacket pkt(TOSERVER_RESPAWN, 0); + NetworkPacket pkt(TOSERVER_RESPAWN_LEGACY, 0); Send(&pkt); } @@ -1406,6 +1398,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() && @@ -1415,7 +1409,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(); @@ -1426,8 +1422,10 @@ void Client::sendPlayerPos() player->last_camera_fov = camera_fov; player->last_camera_inverted = camera_inverted; player->last_wanted_range = wanted_range; + player->last_movement_speed = movement_speed; + player->last_movement_dir = movement_dir; - NetworkPacket pkt(TOSERVER_PLAYERPOS, 12 + 12 + 4 + 4 + 4 + 1 + 1 + 1); + NetworkPacket pkt(TOSERVER_PLAYERPOS, 12 + 12 + 4 + 4 + 4 + 1 + 1 + 1 + 4 + 4); writePlayerPos(player, &map, &pkt, camera_inverted); diff --git a/src/client/client.h b/src/client/client.h index b2ff9a0da..0b26ff94d 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -20,23 +20,22 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once #include "clientenvironment.h" -#include "irrlichttypes_extrabloated.h" +#include "irrlichttypes.h" #include #include #include #include #include #include -#include "clientobject.h" #include "gamedef.h" #include "inventorymanager.h" -#include "client/hud.h" -#include "tileanimation.h" #include "network/address.h" +#include "network/networkprotocol.h" // multiple enums #include "network/peerhandler.h" #include "gameparams.h" -#include "clientdynamicinfo.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 @@ -44,34 +43,35 @@ with this program; if not, write to the Free Software Foundation, Inc., #define CLIENT_CHAT_MESSAGE_LIMIT_PER_10S 10.0f -struct ClientEvent; -struct MeshMakeData; -struct ChatMessage; -class MapBlockMesh; -class RenderingEngine; -class IWritableTextureSource; -class IWritableShaderSource; -class IWritableItemDefManager; -class ISoundManager; -class NodeDefManager; -//class IWritableCraftDefManager; +class Camera; class ClientMediaDownloader; -class SingleMediaDownloader; -struct MapDrawControl; +class ISoundManager; +class IWritableItemDefManager; +class IWritableShaderSource; +class IWritableTextureSource; +class MapBlockMesh; +class MapDatabase; +class MeshUpdateManager; +class Minimap; class ModChannelMgr; class MtEventManager; -struct PointedThing; -struct MapNode; -class MapDatabase; -class Minimap; -struct MinimapMapblock; -class MeshUpdateManager; -class ParticleManager; -class Camera; -struct PlayerControl; class NetworkPacket; +class NodeDefManager; +class ParticleManager; +class RenderingEngine; +class SingleMediaDownloader; +struct ChatMessage; +struct ClientDynamicInfo; +struct ClientEvent; +struct MapDrawControl; +struct MapNode; +struct MeshMakeData; +struct MinimapMapblock; +struct PlayerControl; +struct PointedThing; + namespace con { -class Connection; +class IConnection; } using sound_handle_t = int; @@ -195,7 +195,7 @@ public: void handleCommand_Breath(NetworkPacket* pkt); void handleCommand_MovePlayer(NetworkPacket* pkt); void handleCommand_MovePlayerRel(NetworkPacket* pkt); - void handleCommand_DeathScreen(NetworkPacket* pkt); + void handleCommand_DeathScreenLegacy(NetworkPacket* pkt); void handleCommand_AnnounceMedia(NetworkPacket* pkt); void handleCommand_Media(NetworkPacket* pkt); void handleCommand_NodeDef(NetworkPacket* pkt); @@ -250,7 +250,7 @@ public: void sendChangePassword(const std::string &oldpassword, const std::string &newpassword); void sendDamage(u16 damage); - void sendRespawn(); + void sendRespawnLegacy(); void sendReady(); void sendHaveMedia(const std::vector &tokens); void sendUpdateClientInfo(const ClientDynamicInfo &info); @@ -452,8 +452,8 @@ private: void loadMods(); // Virtual methods from con::PeerHandler - void peerAdded(con::Peer *peer) override; - void deletingPeer(con::Peer *peer, bool timeout) override; + void peerAdded(con::IPeer *peer) override; + void deletingPeer(con::IPeer *peer, bool timeout) override; void initLocalMapSaving(const Address &address, const std::string &hostname, @@ -493,7 +493,7 @@ private: std::unique_ptr m_mesh_update_manager; ClientEnvironment m_env; std::unique_ptr m_particle_manager; - std::unique_ptr m_con; + std::unique_ptr m_con; std::string m_address_name; ELoginRegister m_allow_login_or_register = ELoginRegister::Any; Camera *m_camera = nullptr; diff --git a/src/client/clientenvironment.cpp b/src/client/clientenvironment.cpp index f1ec14d3b..08c5586ea 100644 --- a/src/client/clientenvironment.cpp +++ b/src/client/clientenvironment.cpp @@ -43,10 +43,10 @@ with this program; if not, write to the Free Software Foundation, Inc., ClientEnvironment */ -ClientEnvironment::ClientEnvironment(ClientMap *map, +ClientEnvironment::ClientEnvironment(irr_ptr map, ITextureSource *texturesource, Client *client): Environment(client), - m_map(map), + m_map(std::move(map)), m_texturesource(texturesource), m_client(client) { @@ -60,18 +60,17 @@ ClientEnvironment::~ClientEnvironment() delete simple_object; } - // Drop/delete map - m_map->drop(); + m_map.reset(); delete m_local_player; } -Map & ClientEnvironment::getMap() +Map &ClientEnvironment::getMap() { return *m_map; } -ClientMap & ClientEnvironment::getClientMap() +ClientMap &ClientEnvironment::getClientMap() { return *m_map; } diff --git a/src/client/clientenvironment.h b/src/client/clientenvironment.h index bdb8b9726..db31e69f2 100644 --- a/src/client/clientenvironment.h +++ b/src/client/clientenvironment.h @@ -22,6 +22,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "environment.h" #include "util/numeric.h" // IntervalLimiter #include "activeobjectmgr.h" // client::ActiveObjectMgr +#include "irr_ptr.h" #include #ifdef SERVER @@ -66,7 +67,7 @@ typedef std::unordered_map ClientActiveObjectMap; class ClientEnvironment : public Environment { public: - ClientEnvironment(ClientMap *map, ITextureSource *texturesource, Client *client); + ClientEnvironment(irr_ptr map, ITextureSource *texturesource, Client *client); ~ClientEnvironment(); Map & getMap(); @@ -151,7 +152,7 @@ public: u64 getFrameTimeDelta() const { return m_frame_dtime; } private: - ClientMap *m_map; + irr_ptr m_map; LocalPlayer *m_local_player = nullptr; ITextureSource *m_texturesource; Client *m_client; diff --git a/src/client/clientevent.h b/src/client/clientevent.h index 243a94596..8c505786b 100644 --- a/src/client/clientevent.h +++ b/src/client/clientevent.h @@ -20,7 +20,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once #include -#include "irrlichttypes_bloated.h" +#include "irrlichttypes.h" +#include "client/hud.h" // HudElementStat struct ParticleParameters; struct ParticleSpawnerParameters; @@ -34,7 +35,7 @@ enum ClientEventType : u8 CE_NONE, CE_PLAYER_DAMAGE, CE_PLAYER_FORCE_MOVE, - CE_DEATHSCREEN, + CE_DEATHSCREEN_LEGACY, CE_SHOW_FORMSPEC, CE_SHOW_LOCAL_FORMSPEC, CE_SPAWN_PARTICLE, @@ -95,13 +96,6 @@ struct ClientEvent f32 yaw; } player_force_move; struct - { - bool set_camera_point_target; - f32 camera_point_target_x; - f32 camera_point_target_y; - f32 camera_point_target_z; - } deathscreen; - struct { std::string *formspec; std::string *formname; @@ -136,6 +130,7 @@ struct ClientEvent f32 density; u32 color_bright; u32 color_ambient; + u32 color_shadow; f32 height; f32 thickness; f32 speed_x; diff --git a/src/client/clientlauncher.cpp b/src/client/clientlauncher.cpp index 5365d70f9..13b87f2ef 100644 --- a/src/client/clientlauncher.cpp +++ b/src/client/clientlauncher.cpp @@ -19,7 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "gui/mainmenumanager.h" #include "clouds.h" -#include "gui/touchscreengui.h" +#include "gui/touchcontrols.h" #include "server.h" #include "filesys.h" #include "gui/guiMainMenu.h" @@ -27,6 +27,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "player.h" #include "chat.h" #include "gettext.h" +#include "inputhandler.h" #include "profiler.h" #include "gui/guiEngine.h" #include "fontengine.h" @@ -34,6 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "version.h" #include "renderingengine.h" #include "network/networkexceptions.h" +#include "util/tracy_wrapper.h" #include #include #include @@ -70,6 +72,7 @@ ClientLauncher::~ClientLauncher() { delete input; g_settings->deregisterChangedCallback("dpi_change_notifier", setting_changed_callback, this); + g_settings->deregisterChangedCallback("display_density_factor", setting_changed_callback, this); g_settings->deregisterChangedCallback("gui_scaling", setting_changed_callback, this); delete g_fontengine; @@ -131,6 +134,7 @@ bool ClientLauncher::run(GameStartData &start_data, const Settings &cmd_args) guienv = m_rendering_engine->get_gui_env(); config_guienv(); g_settings->registerChangedCallback("dpi_change_notifier", setting_changed_callback, this); + g_settings->registerChangedCallback("display_density_factor", setting_changed_callback, this); g_settings->registerChangedCallback("gui_scaling", setting_changed_callback, this); g_fontengine = new FontEngine(guienv); @@ -138,8 +142,10 @@ bool ClientLauncher::run(GameStartData &start_data, const Settings &cmd_args) // Create the menu clouds // This is only global so it can be used by RenderingEngine::draw_load_screen(). assert(!g_menucloudsmgr && !g_menuclouds); + std::unique_ptr ssrc(createShaderSource()); + ssrc->addShaderConstantSetterFactory(new FogShaderConstantSetterFactory()); g_menucloudsmgr = m_rendering_engine->get_scene_manager()->createNewSceneManager(); - g_menuclouds = new Clouds(g_menucloudsmgr, nullptr, -1, rand()); + g_menuclouds = new Clouds(g_menucloudsmgr, ssrc.get(), -1, rand()); g_menuclouds->setHeight(100.0f); g_menuclouds->update(v3f(0, 0, 0), video::SColor(255, 240, 240, 255)); scene::ICameraSceneNode* camera; @@ -230,9 +236,9 @@ bool ClientLauncher::run(GameStartData &start_data, const Settings &cmd_args) m_rendering_engine->get_scene_manager()->clear(); - if (g_touchscreengui) { - delete g_touchscreengui; - g_touchscreengui = NULL; + if (g_touchcontrols) { + delete g_touchcontrols; + g_touchcontrols = NULL; } /* Save the settings when leaving the game. @@ -543,15 +549,19 @@ void ClientLauncher::main_menu(MainMenuData *menudata) video::IVideoDriver *driver = m_rendering_engine->get_video_driver(); infostream << "Waiting for other menus" << std::endl; + auto framemarker = FrameMarker("ClientLauncher::main_menu()-wait-frame").started(); while (m_rendering_engine->run() && !*kill) { if (!isMenuActive()) break; driver->beginScene(true, true, video::SColor(255, 128, 128, 128)); m_rendering_engine->get_gui_env()->drawAll(); driver->endScene(); + framemarker.end(); // On some computers framerate doesn't seem to be automatically limited sleep_ms(25); + framemarker.start(); } + framemarker.end(); infostream << "Waited for other menus" << std::endl; auto *cur_control = m_rendering_engine->get_raw_device()->getCursorControl(); diff --git a/src/client/clientlauncher.h b/src/client/clientlauncher.h index 7b070451a..ad7604c87 100644 --- a/src/client/clientlauncher.h +++ b/src/client/clientlauncher.h @@ -19,11 +19,14 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once -#include "irrlichttypes_extrabloated.h" -#include "client/inputhandler.h" -#include "gameparams.h" +#include class RenderingEngine; +class Settings; +class MyEventReceiver; +class InputHandler; +struct GameStartData; +struct MainMenuData; class ClientLauncher { diff --git a/src/client/clientmap.cpp b/src/client/clientmap.cpp index 45995c0ea..ab826c775 100644 --- a/src/client/clientmap.cpp +++ b/src/client/clientmap.cpp @@ -22,6 +22,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "client/mesh.h" #include "mapblock_mesh.h" #include +#include #include #include "mapsector.h" #include "mapblock.h" @@ -30,6 +31,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "settings.h" #include "camera.h" // CameraModes #include "util/basic_macros.h" +#include "util/tracy_wrapper.h" #include "client/renderingengine.h" #include @@ -73,11 +75,23 @@ namespace { }; } +/* + ClientMap +*/ + static void on_settings_changed(const std::string &name, void *data) { - static_cast(data)->onSettingChanged(name); + static_cast(data)->onSettingChanged(name, false); } -// ClientMap + +static const std::string ClientMap_settings[] = { + "trilinear_filter", + "bilinear_filter", + "anisotropic_filter", + "transparency_sorting_distance", + "occlusion_culler", + "enable_raytraced_culling", +}; ClientMap::ClientMap( Client *client, @@ -102,37 +116,32 @@ ClientMap::ClientMap( m_box = aabb3f(-BS*1000000,-BS*1000000,-BS*1000000, BS*1000000,BS*1000000,BS*1000000); - /* TODO: Add a callback function so these can be updated when a setting - * changes. At this point in time it doesn't matter (e.g. /set - * is documented to change server settings only) - * - * TODO: Local caching of settings is not optimal and should at some stage - * be updated to use a global settings object for getting thse values - * (as opposed to the this local caching). This can be addressed in - * a later release. - */ - m_cache_trilinear_filter = g_settings->getBool("trilinear_filter"); - m_cache_bilinear_filter = g_settings->getBool("bilinear_filter"); - m_cache_anistropic_filter = g_settings->getBool("anisotropic_filter"); - m_cache_transparency_sorting_distance = g_settings->getU16("transparency_sorting_distance"); - m_loops_occlusion_culler = g_settings->get("occlusion_culler") == "loops"; - g_settings->registerChangedCallback("occlusion_culler", on_settings_changed, this); - m_enable_raytraced_culling = g_settings->getBool("enable_raytraced_culling"); - g_settings->registerChangedCallback("enable_raytraced_culling", on_settings_changed, this); + for (const auto &name : ClientMap_settings) + g_settings->registerChangedCallback(name, on_settings_changed, this); + // load all settings at once + onSettingChanged("", true); } -void ClientMap::onSettingChanged(const std::string &name) +void ClientMap::onSettingChanged(std::string_view name, bool all) { - if (name == "occlusion_culler") + if (all || name == "trilinear_filter") + m_cache_trilinear_filter = g_settings->getBool("trilinear_filter"); + if (all || name == "bilinear_filter") + m_cache_bilinear_filter = g_settings->getBool("bilinear_filter"); + if (all || name == "anisotropic_filter") + m_cache_anistropic_filter = g_settings->getBool("anisotropic_filter"); + if (all || name == "transparency_sorting_distance") + m_cache_transparency_sorting_distance = g_settings->getU16("transparency_sorting_distance"); + if (all || name == "occlusion_culler") m_loops_occlusion_culler = g_settings->get("occlusion_culler") == "loops"; - if (name == "enable_raytraced_culling") + if (all || name == "enable_raytraced_culling") m_enable_raytraced_culling = g_settings->getBool("enable_raytraced_culling"); } ClientMap::~ClientMap() { - g_settings->deregisterChangedCallback("occlusion_culler", on_settings_changed, this); - g_settings->deregisterChangedCallback("enable_raytraced_culling", on_settings_changed, this); + for (const auto &name : ClientMap_settings) + g_settings->deregisterChangedCallback(name, on_settings_changed, this); } void ClientMap::updateCamera(v3f pos, v3f dir, f32 fov, v3s16 offset, video::SColor light_color) @@ -185,6 +194,13 @@ void ClientMap::OnRegisterSceneNode() // we have other way to find it } +void ClientMap::render() +{ + video::IVideoDriver* driver = SceneManager->getVideoDriver(); + driver->setTransform(video::ETS_WORLD, AbsoluteTransformation); + renderMap(driver, SceneManager->getSceneNodeRenderPass()); +} + void ClientMap::getBlocksInViewRange(v3s16 cam_pos_nodes, v3s16 *p_blocks_min, v3s16 *p_blocks_max, float range) { @@ -699,6 +715,8 @@ void ClientMap::touchMapBlocks() void ClientMap::renderMap(video::IVideoDriver* driver, s32 pass) { + ZoneScoped; + bool is_transparent_pass = pass == scene::ESNRP_TRANSPARENT; std::string prefix; @@ -767,15 +785,12 @@ void ClientMap::renderMap(video::IVideoDriver* driver, s32 pass) if (is_frustum_culled(mesh_sphere_center, mesh_sphere_radius)) continue; - v3f block_pos_r = intToFloat(block->getPosRelative() + MAP_BLOCKSIZE / 2, BS); - - float d = camera_position.getDistanceFrom(block_pos_r); - d = MYMAX(0,d - BLOCK_MAX_RADIUS); - // Mesh animation if (pass == scene::ESNRP_SOLID) { - // Pretty random but this should work somewhat nicely - bool faraway = d >= BS * 50; + // 50 nodes is pretty arbitrary but it should work somewhat nicely + float distance_sq = camera_position.getDistanceFromSQ(mesh_sphere_center); + bool faraway = distance_sq >= std::pow(BS * 50 + mesh_sphere_radius, 2.0f); + if (block_mesh->isAnimationForced() || !faraway || mesh_animate_count < (m_control.range_all ? 200 : 50)) { @@ -844,10 +859,8 @@ void ClientMap::renderMap(video::IVideoDriver* driver, s32 pass) drawcall_count += draw_order.size(); for (auto &descriptor : draw_order) { - scene::IMeshBuffer *buf = descriptor.getBuffer(); - if (!descriptor.m_reuse_material) { - auto &material = buf->getMaterial(); + auto &material = descriptor.getMaterial(); // Apply filter settings material.forEachTexture([this] (auto &tex) { @@ -879,8 +892,7 @@ void ClientMap::renderMap(video::IVideoDriver* driver, s32 pass) m.setTranslation(block_wpos - offset); driver->setTransform(video::ETS_WORLD, m); - descriptor.draw(driver); - vertex_count += buf->getIndexCount(); + vertex_count += descriptor.draw(driver); } g_profiler->avg(prefix + "draw meshes [ms]", draw.stop(true)); @@ -1003,8 +1015,7 @@ int ClientMap::getBackgroundBrightness(float max_d, u32 daylight_factor, v3f z_dir = z_directions[i]; core::CMatrix4 a; a.buildRotateFromTo(v3f(0,1,0), z_dir); - v3f dir = m_camera_direction; - a.rotateVect(dir); + v3f dir = a.rotateAndScaleVect(m_camera_direction); int br = 0; float step = BS*1.5; if(max_d > 35*BS) @@ -1197,13 +1208,22 @@ void ClientMap::renderMapShadows(video::IVideoDriver *driver, // Render all mesh buffers in order drawcall_count += draw_order.size(); - for (auto &descriptor : draw_order) { - scene::IMeshBuffer *buf = descriptor.getBuffer(); + bool translucent_foliage = g_settings->getBool("enable_translucent_foliage"); + video::E_MATERIAL_TYPE leaves_material = video::EMT_SOLID; + + // For translucent leaves, we want to use backface culling instead of frontface. + if (translucent_foliage) { + // this is the material leaves would use, compare to nodedef.cpp + auto* shdsrc = m_client->getShaderSource(); + const u32 leaves_shader = shdsrc->getShader("nodes_shader", TILE_MATERIAL_WAVING_LEAVES, NDT_ALLFACES); + leaves_material = shdsrc->getShaderInfo(leaves_shader).material; + } + + for (auto &descriptor : draw_order) { if (!descriptor.m_reuse_material) { // override some material properties - video::SMaterial local_material = buf->getMaterial(); - local_material.MaterialType = material.MaterialType; + video::SMaterial local_material = descriptor.getMaterial(); // do not override culling if the original material renders both back // and front faces in solid mode (e.g. plantlike) // Transparent plants would still render shadows only from one side, @@ -1212,8 +1232,12 @@ void ClientMap::renderMapShadows(video::IVideoDriver *driver, local_material.BackfaceCulling = material.BackfaceCulling; local_material.FrontfaceCulling = material.FrontfaceCulling; } + if (local_material.MaterialType == leaves_material && translucent_foliage) { + local_material.BackfaceCulling = true; + local_material.FrontfaceCulling = false; + } + local_material.MaterialType = material.MaterialType; local_material.BlendOperation = material.BlendOperation; - local_material.Lighting = false; driver->setMaterial(local_material); ++material_swaps; } @@ -1222,8 +1246,7 @@ void ClientMap::renderMapShadows(video::IVideoDriver *driver, m.setTranslation(block_wpos - offset); driver->setTransform(video::ETS_WORLD, m); - descriptor.draw(driver); - vertex_count += buf->getIndexCount(); + vertex_count += descriptor.draw(driver); } // restore the driver material state @@ -1305,27 +1328,35 @@ void ClientMap::updateTransparentMeshBuffers() ScopeProfiler sp(g_profiler, "CM::updateTransparentMeshBuffers", SPT_AVG); u32 sorted_blocks = 0; u32 unsorted_blocks = 0; - f32 sorting_distance_sq = std::pow(m_cache_transparency_sorting_distance * BS, 2.0f); - + bool transparency_sorting_enabled = m_cache_transparency_sorting_distance > 0; + f32 sorting_distance = m_cache_transparency_sorting_distance * BS; // Update the order of transparent mesh buffers in each mesh for (auto it = m_drawlist.begin(); it != m_drawlist.end(); it++) { - MapBlock* block = it->second; - if (!block->mesh) + MapBlock *block = it->second; + MapBlockMesh *blockmesh = block->mesh; + if (!blockmesh) continue; if (m_needs_update_transparent_meshes || - block->mesh->getTransparentBuffers().size() == 0) { + blockmesh->getTransparentBuffers().size() == 0) { + bool do_sort_block = transparency_sorting_enabled; - v3s16 block_pos = block->getPos(); - v3f block_pos_f = intToFloat(block_pos * MAP_BLOCKSIZE + MAP_BLOCKSIZE / 2, BS); - f32 distance = m_camera_position.getDistanceFromSQ(block_pos_f); - if (distance <= sorting_distance_sq) { - block->mesh->updateTransparentBuffers(m_camera_position, block_pos); - ++sorted_blocks; + if (do_sort_block) { + v3f mesh_sphere_center = intToFloat(block->getPosRelative(), BS) + + blockmesh->getBoundingSphereCenter(); + f32 mesh_sphere_radius = blockmesh->getBoundingRadius(); + f32 distance_sq = m_camera_position.getDistanceFromSQ(mesh_sphere_center); + + if (distance_sq > std::pow(sorting_distance + mesh_sphere_radius, 2.0f)) + do_sort_block = false; } - else { - block->mesh->consolidateTransparentBuffers(); + + if (do_sort_block) { + blockmesh->updateTransparentBuffers(m_camera_position, block->getPos()); + ++sorted_blocks; + } else { + blockmesh->consolidateTransparentBuffers(); ++unsorted_blocks; } } @@ -1336,19 +1367,19 @@ void ClientMap::updateTransparentMeshBuffers() m_needs_update_transparent_meshes = false; } -scene::IMeshBuffer* ClientMap::DrawDescriptor::getBuffer() +video::SMaterial &ClientMap::DrawDescriptor::getMaterial() { - return m_use_partial_buffer ? m_partial_buffer->getBuffer() : m_buffer; + return (m_use_partial_buffer ? m_partial_buffer->getBuffer() : m_buffer)->getMaterial(); } -void ClientMap::DrawDescriptor::draw(video::IVideoDriver* driver) +u32 ClientMap::DrawDescriptor::draw(video::IVideoDriver* driver) { if (m_use_partial_buffer) { - m_partial_buffer->beforeDraw(); - driver->drawMeshBuffer(m_partial_buffer->getBuffer()); - m_partial_buffer->afterDraw(); + m_partial_buffer->draw(driver); + return m_partial_buffer->getBuffer()->getVertexCount(); } else { driver->drawMeshBuffer(m_buffer); + return m_buffer->getVertexCount(); } } diff --git a/src/client/clientmap.h b/src/client/clientmap.h index 05c33d67c..2f0a2e986 100644 --- a/src/client/clientmap.h +++ b/src/client/clientmap.h @@ -19,7 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once -#include "irrlichttypes_extrabloated.h" +#include "irrlichttypes_bloated.h" #include "map.h" #include "camera.h" #include @@ -41,6 +41,16 @@ class Client; class ITextureSource; class PartialMeshBuffer; +namespace irr::scene +{ + class IMeshBuffer; +} + +namespace irr::video +{ + class IVideoDriver; +} + /* ClientMap @@ -75,12 +85,7 @@ public: virtual void OnRegisterSceneNode() override; - virtual void render() override - { - video::IVideoDriver* driver = SceneManager->getVideoDriver(); - driver->setTransform(video::ETS_WORLD, AbsoluteTransformation); - renderMap(driver, SceneManager->getSceneNodeRenderPass()); - } + virtual void render() override; virtual const aabb3f &getBoundingBox() const override { @@ -112,7 +117,7 @@ public: f32 getWantedRange() const { return m_control.wanted_range; } f32 getCameraFov() const { return m_camera_fov; } - void onSettingChanged(const std::string &name); + void onSettingChanged(std::string_view name, bool all); protected: // use drop() instead @@ -162,8 +167,9 @@ private: m_pos(pos), m_partial_buffer(buffer), m_reuse_material(false), m_use_partial_buffer(true) {} - scene::IMeshBuffer* getBuffer(); - void draw(video::IVideoDriver* driver); + video::SMaterial &getMaterial(); + /// @return index count + u32 draw(video::IVideoDriver* driver); }; Client *m_client; diff --git a/src/client/clientobject.h b/src/client/clientobject.h index f02815e04..8c2c68d15 100644 --- a/src/client/clientobject.h +++ b/src/client/clientobject.h @@ -19,7 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once -#include "irrlichttypes_extrabloated.h" +#include "irrlichttypes_bloated.h" #include "activeobject.h" #include #include @@ -34,6 +34,13 @@ class LocalPlayer; struct ItemStack; class WieldMeshSceneNode; +namespace irr::scene +{ + class IAnimatedMeshSceneNode; + class ISceneNode; + class ISceneManager; +} + class ClientActiveObject : public ActiveObject { public: diff --git a/src/client/clouds.cpp b/src/client/clouds.cpp index d53d52957..96926f3e0 100644 --- a/src/client/clouds.cpp +++ b/src/client/clouds.cpp @@ -28,11 +28,9 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "settings.h" #include - -// Menu clouds are created later class Clouds; -Clouds *g_menuclouds = NULL; -scene::ISceneManager *g_menucloudsmgr = NULL; +scene::ISceneManager *g_menucloudsmgr = nullptr; +Clouds *g_menuclouds = nullptr; // Constant for now static constexpr const float cloud_size = BS * 64.0f; @@ -49,11 +47,9 @@ Clouds::Clouds(scene::ISceneManager* mgr, IShaderSource *ssrc, scene::ISceneNode(mgr->getRootSceneNode(), mgr, id), m_seed(seed) { + assert(ssrc); m_enable_shaders = g_settings->getBool("enable_shaders"); - // menu clouds use shader-less clouds for simplicity (ssrc == NULL) - m_enable_shaders = m_enable_shaders && ssrc; - m_material.Lighting = false; m_material.BackfaceCulling = true; m_material.FogEnable = true; m_material.AntiAliasing = video::EAAM_SIMPLE; @@ -69,6 +65,8 @@ Clouds::Clouds(scene::ISceneManager* mgr, IShaderSource *ssrc, readSettings(); g_settings->registerChangedCallback("enable_3d_clouds", &cloud_3d_setting_changed, this); + g_settings->registerChangedCallback("soft_clouds", + &cloud_3d_setting_changed, this); updateBox(); @@ -80,6 +78,8 @@ Clouds::~Clouds() { g_settings->deregisterChangedCallback("enable_3d_clouds", &cloud_3d_setting_changed, this); + g_settings->deregisterChangedCallback("soft_clouds", + &cloud_3d_setting_changed, this); } void Clouds::OnRegisterSceneNode() @@ -142,18 +142,21 @@ void Clouds::updateMesh() video::SColorf c_side_2_f(m_color); video::SColorf c_bottom_f(m_color); if (m_enable_shaders) { - // shader mixes the base color, set via EmissiveColor + // shader mixes the base color, set via ColorParam c_top_f = c_side_1_f = c_side_2_f = c_bottom_f = video::SColorf(1.0f, 1.0f, 1.0f, 1.0f); } - c_side_1_f.r *= 0.95f; - c_side_1_f.g *= 0.95f; - c_side_1_f.b *= 0.95f; - c_side_2_f.r *= 0.90f; - c_side_2_f.g *= 0.90f; - c_side_2_f.b *= 0.90f; - c_bottom_f.r *= 0.80f; - c_bottom_f.g *= 0.80f; - c_bottom_f.b *= 0.80f; + video::SColorf shadow = m_params.color_shadow; + + c_side_1_f.r *= shadow.r * 0.25f + 0.75f; + c_side_1_f.g *= shadow.g * 0.25f + 0.75f; + c_side_1_f.b *= shadow.b * 0.25f + 0.75f; + c_side_2_f.r *= shadow.r * 0.5f + 0.5f; + c_side_2_f.g *= shadow.g * 0.5f + 0.5f; + c_side_2_f.b *= shadow.b * 0.5f + 0.5f; + c_bottom_f.r *= shadow.r; + c_bottom_f.g *= shadow.g; + c_bottom_f.b *= shadow.b; + video::SColor c_top = c_top_f.toSColor(); video::SColor c_side_1 = c_side_1_f.toSColor(); video::SColor c_side_2 = c_side_2_f.toSColor(); @@ -178,21 +181,23 @@ void Clouds::updateMesh() auto *mb = m_meshbuffer.get(); + auto &vertices = mb->Vertices->Data; + auto &indices = mb->Indices->Data; { const u32 vertex_count = num_faces_to_draw * 16 * m_cloud_radius_i * m_cloud_radius_i; const u32 quad_count = vertex_count / 4; const u32 index_count = quad_count * 6; // reserve memory - mb->Vertices.reallocate(vertex_count); - mb->Indices.reallocate(index_count); + vertices.reserve(vertex_count); + indices.reserve(index_count); } #define GETINDEX(x, z, radius) (((z)+(radius))*(radius)*2 + (x)+(radius)) #define INAREA(x, z, radius) \ ((x) >= -(radius) && (x) < (radius) && (z) >= -(radius) && (z) < (radius)) - mb->Vertices.set_used(0); + vertices.clear(); for (s16 zi0= -m_cloud_radius_i; zi0 < m_cloud_radius_i; zi0++) for (s16 xi0= -m_cloud_radius_i; xi0 < m_cloud_radius_i; xi0++) { @@ -223,13 +228,14 @@ void Clouds::updateMesh() const f32 ry = is3D() ? m_params.thickness * BS : 0.0f; const f32 rz = cloud_size / 2; - for(u32 i = 0; i < num_faces_to_draw; i++) + bool soft_clouds_enabled = g_settings->getBool("soft_clouds"); + for (u32 i = 0; i < num_faces_to_draw; i++) { - switch(i) + switch (i) { case 0: // top - for (video::S3DVertex &vertex : v) { - vertex.Normal.set(0,1,0); + for (video::S3DVertex& vertex : v) { + vertex.Normal.set(0, 1, 0); } v[0].Pos.set(-rx, ry,-rz); v[1].Pos.set(-rx, ry, rz); @@ -239,12 +245,20 @@ void Clouds::updateMesh() case 1: // back if (INAREA(xi, zi - 1, m_cloud_radius_i)) { u32 j = GETINDEX(xi, zi - 1, m_cloud_radius_i); - if(grid[j]) + if (grid[j]) continue; } - for (video::S3DVertex &vertex : v) { - vertex.Color = c_side_1; - vertex.Normal.set(0,0,-1); + if (soft_clouds_enabled) { + for (video::S3DVertex& vertex : v) { + vertex.Normal.set(0, 0, -1); + } + v[2].Color = c_bottom; + v[3].Color = c_bottom; + } else { + for (video::S3DVertex& vertex : v) { + vertex.Color = c_side_1; + vertex.Normal.set(0, 0, -1); + } } v[0].Pos.set(-rx, ry,-rz); v[1].Pos.set( rx, ry,-rz); @@ -253,28 +267,45 @@ void Clouds::updateMesh() break; case 2: //right if (INAREA(xi + 1, zi, m_cloud_radius_i)) { - u32 j = GETINDEX(xi+1, zi, m_cloud_radius_i); - if(grid[j]) + u32 j = GETINDEX(xi + 1, zi, m_cloud_radius_i); + if (grid[j]) continue; } - for (video::S3DVertex &vertex : v) { - vertex.Color = c_side_2; - vertex.Normal.set(1,0,0); + if (soft_clouds_enabled) { + for (video::S3DVertex& vertex : v) { + vertex.Normal.set(1, 0, 0); + } + v[2].Color = c_bottom; + v[3].Color = c_bottom; } - v[0].Pos.set( rx, ry,-rz); - v[1].Pos.set( rx, ry, rz); - v[2].Pos.set( rx, 0, rz); - v[3].Pos.set( rx, 0,-rz); + else { + for (video::S3DVertex& vertex : v) { + vertex.Color = c_side_2; + vertex.Normal.set(1, 0, 0); + } + } + v[0].Pos.set(rx, ry,-rz); + v[1].Pos.set(rx, ry, rz); + v[2].Pos.set(rx, 0, rz); + v[3].Pos.set(rx, 0,-rz); break; case 3: // front if (INAREA(xi, zi + 1, m_cloud_radius_i)) { u32 j = GETINDEX(xi, zi + 1, m_cloud_radius_i); - if(grid[j]) + if (grid[j]) continue; } - for (video::S3DVertex &vertex : v) { - vertex.Color = c_side_1; - vertex.Normal.set(0,0,-1); + if (soft_clouds_enabled) { + for (video::S3DVertex& vertex : v) { + vertex.Normal.set(0, 0, -1); + } + v[2].Color = c_bottom; + v[3].Color = c_bottom; + } else { + for (video::S3DVertex& vertex : v) { + vertex.Color = c_side_1; + vertex.Normal.set(0, 0, -1); + } } v[0].Pos.set( rx, ry, rz); v[1].Pos.set(-rx, ry, rz); @@ -282,14 +313,22 @@ void Clouds::updateMesh() v[3].Pos.set( rx, 0, rz); break; case 4: // left - if (INAREA(xi-1, zi, m_cloud_radius_i)) { - u32 j = GETINDEX(xi-1, zi, m_cloud_radius_i); - if(grid[j]) + if (INAREA(xi - 1, zi, m_cloud_radius_i)) { + u32 j = GETINDEX(xi - 1, zi, m_cloud_radius_i); + if (grid[j]) continue; } - for (video::S3DVertex &vertex : v) { - vertex.Color = c_side_2; - vertex.Normal.set(-1,0,0); + if (soft_clouds_enabled) { + for (video::S3DVertex& vertex : v) { + vertex.Normal.set(-1, 0, 0); + } + v[2].Color = c_bottom; + v[3].Color = c_bottom; + } else { + for (video::S3DVertex& vertex : v) { + vertex.Color = c_side_2; + vertex.Normal.set(-1, 0, 0); + } } v[0].Pos.set(-rx, ry, rz); v[1].Pos.set(-rx, ry,-rz); @@ -297,9 +336,9 @@ void Clouds::updateMesh() v[3].Pos.set(-rx, 0, rz); break; case 5: // bottom - for (video::S3DVertex &vertex : v) { + for (video::S3DVertex& vertex : v) { vertex.Color = c_bottom; - vertex.Normal.set(0,-1,0); + vertex.Normal.set(0, -1, 0); } v[0].Pos.set( rx, 0, rz); v[1].Pos.set(-rx, 0, rz); @@ -312,7 +351,7 @@ void Clouds::updateMesh() for (video::S3DVertex &vertex : v) { vertex.Pos += pos; - mb->Vertices.push_back(vertex); + vertices.push_back(vertex); } } } @@ -322,18 +361,18 @@ void Clouds::updateMesh() const u32 index_count = quad_count * 6; // rewrite index array as needed if (mb->getIndexCount() > index_count) { - mb->Indices.set_used(index_count); + indices.resize(index_count); mb->setDirty(scene::EBT_INDEX); } else if (mb->getIndexCount() < index_count) { const u32 start = mb->getIndexCount() / 6; assert(start * 6 == mb->getIndexCount()); for (u32 k = start; k < quad_count; k++) { - mb->Indices.push_back(4 * k + 0); - mb->Indices.push_back(4 * k + 1); - mb->Indices.push_back(4 * k + 2); - mb->Indices.push_back(4 * k + 2); - mb->Indices.push_back(4 * k + 3); - mb->Indices.push_back(4 * k + 0); + indices.push_back(4 * k + 0); + indices.push_back(4 * k + 1); + indices.push_back(4 * k + 2); + indices.push_back(4 * k + 2); + indices.push_back(4 * k + 3); + indices.push_back(4 * k + 0); } mb->setDirty(scene::EBT_INDEX); } @@ -365,7 +404,7 @@ void Clouds::render() m_material.BackfaceCulling = is3D(); if (m_enable_shaders) - m_material.EmissiveColor = m_color.toSColor(); + m_material.ColorParam = m_color.toSColor(); driver->setTransform(video::ETS_WORLD, AbsoluteTransformation); driver->setMaterial(m_material); diff --git a/src/client/clouds.h b/src/client/clouds.h index d93fa9b43..7193c03f9 100644 --- a/src/client/clouds.h +++ b/src/client/clouds.h @@ -19,20 +19,28 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once +#include "irrlichttypes_bloated.h" #include "constants.h" #include "irr_ptr.h" -#include "irrlichttypes_extrabloated.h" #include "skyparams.h" #include +#include +#include +#include class IShaderSource; -// Menu clouds -class Clouds; -extern Clouds *g_menuclouds; +namespace irr::scene +{ + class ISceneManager; +} -// Scene manager used for menu clouds +// Menu clouds +// The mainmenu and the loading screen use the same Clouds object so that the +// clouds don't jump when switching between the two. +class Clouds; extern scene::ISceneManager *g_menucloudsmgr; +extern Clouds *g_menuclouds; class Clouds : public scene::ISceneNode { @@ -101,6 +109,14 @@ public: m_params.color_ambient = color_ambient; } + void setColorShadow(video::SColor color_shadow) + { + if (m_params.color_shadow == color_shadow) + return; + m_params.color_shadow = color_shadow; + invalidateMesh(); + } + void setHeight(float height) { if (m_params.height == height) @@ -133,8 +149,8 @@ private: { float height_bs = m_params.height * BS; float thickness_bs = m_params.thickness * BS; - m_box = aabb3f(-BS * 1000000.0f, height_bs - BS * m_camera_offset.Y, -BS * 1000000.0f, - BS * 1000000.0f, height_bs + thickness_bs - BS * m_camera_offset.Y, BS * 1000000.0f); + m_box = aabb3f(-BS * 1000000.0f, height_bs, -BS * 1000000.0f, + BS * 1000000.0f, height_bs + thickness_bs, BS * 1000000.0f); } void updateMesh(); diff --git a/src/client/content_cao.cpp b/src/client/content_cao.cpp index 7913e477a..c8acb3875 100644 --- a/src/client/content_cao.cpp +++ b/src/client/content_cao.cpp @@ -186,6 +186,12 @@ static bool logOnce(const std::ostringstream &from, std::ostream &log_to) return true; } +static void setColorParam(scene::ISceneNode *node, video::SColor color) +{ + for (u32 i = 0; i < node->getMaterialCount(); ++i) + node->getMaterial(i).ColorParam = color; +} + /* TestCAO */ @@ -255,7 +261,6 @@ void TestCAO::addToScene(ITextureSource *tsrc, scene::ISceneManager *smgr) u16 indices[] = {0,1,2,2,3,0}; buf->append(vertices, 4, indices, 6); // Set material - buf->getMaterial().Lighting = false; buf->getMaterial().BackfaceCulling = false; buf->getMaterial().TextureLayers[0].Texture = tsrc->getTextureForMesh("rat.png"); buf->getMaterial().TextureLayers[0].MinFilter = video::ETMINF_NEAREST_MIPMAP_NEAREST; @@ -642,12 +647,7 @@ void GenericCAO::addToScene(ITextureSource *tsrc, scene::ISceneManager *smgr) auto setMaterial = [this] (video::SMaterial &mat) { mat.MaterialType = m_material_type; - mat.Lighting = false; mat.FogEnable = true; - if (m_enable_shaders) { - mat.GouraudShading = false; - mat.NormalizeNormals = true; - } mat.forEachTexture([] (auto &tex) { tex.MinFilter = video::ETMINF_NEAREST_MIPMAP_NEAREST; tex.MagFilter = video::ETMAGF_NEAREST; @@ -704,7 +704,7 @@ void GenericCAO::addToScene(ITextureSource *tsrc, scene::ISceneManager *smgr) // Set material setMaterial(buf->getMaterial()); if (m_enable_shaders) { - buf->getMaterial().EmissiveColor = c; + buf->getMaterial().ColorParam = c; } // Add to mesh @@ -730,7 +730,7 @@ void GenericCAO::addToScene(ITextureSource *tsrc, scene::ISceneManager *smgr) // Set material setMaterial(buf->getMaterial()); if (m_enable_shaders) { - buf->getMaterial().EmissiveColor = c; + buf->getMaterial().ColorParam = c; } // Add to mesh @@ -774,8 +774,6 @@ void GenericCAO::addToScene(ITextureSource *tsrc, scene::ISceneManager *smgr) // set vertex colors to ensure alpha is set setMeshColor(m_animated_meshnode->getMesh(), video::SColor(0xFFFFFFFF)); - setAnimatedMeshColor(m_animated_meshnode, video::SColor(0xFFFFFFFF)); - setSceneNodeMaterials(m_animated_meshnode); m_animated_meshnode->forEachMaterial([this] (auto &mat) { @@ -810,15 +808,21 @@ void GenericCAO::addToScene(ITextureSource *tsrc, scene::ISceneManager *smgr) } /* Set VBO hint */ - // - if shaders are disabled we modify the mesh often - // - sprites are also modified often - // - the wieldmesh sets its own hint - // - bone transformations do not need to modify the vertex data + // wieldmesh sets its own hint, no need to handle it if (m_enable_shaders && (m_meshnode || m_animated_meshnode)) { - if (m_meshnode) + // sprite uses vertex animation + if (m_meshnode && m_prop.visual != "upright_sprite") m_meshnode->getMesh()->setHardwareMappingHint(scene::EHM_STATIC); - if (m_animated_meshnode) - m_animated_meshnode->getMesh()->setHardwareMappingHint(scene::EHM_STATIC); + + if (m_animated_meshnode) { + auto *mesh = m_animated_meshnode->getMesh(); + // skinning happens on the CPU + if (m_animated_meshnode->getJointCount() > 0) + mesh->setHardwareMappingHint(scene::EHM_STREAM, scene::EBT_VERTEX); + else + mesh->setHardwareMappingHint(scene::EHM_STATIC, scene::EBT_VERTEX); + mesh->setHardwareMappingHint(scene::EHM_STATIC, scene::EBT_INDEX); + } } /* don't update while punch texture modifier is active */ @@ -844,14 +848,19 @@ void GenericCAO::addToScene(ITextureSource *tsrc, scene::ISceneManager *smgr) if (m_animated_meshnode) { u32 mat_count = m_animated_meshnode->getMaterialCount(); + assert(mat_count == m_animated_meshnode->getMesh()->getMeshBufferCount()); + u32 max_tex_idx = 0; + for (u32 i = 0; i < mat_count; ++i) { + max_tex_idx = std::max(max_tex_idx, + m_animated_meshnode->getMesh()->getTextureSlot(i)); + } if (mat_count == 0 || m_prop.textures.empty()) { // nothing - } else if (mat_count > m_prop.textures.size()) { + } else if (max_tex_idx >= m_prop.textures.size()) { std::ostringstream oss; oss << "GenericCAO::addToScene(): Model " - << m_prop.mesh << " loaded with " << mat_count - << " mesh buffers but only " << m_prop.textures.size() - << " texture(s) specified, this is deprecated."; + << m_prop.mesh << " is missing " << (max_tex_idx + 1 - m_prop.textures.size()) + << " more texture(s), this is deprecated."; logOnce(oss, warningstream); video::ITexture *last = m_animated_meshnode->getMaterial(0).TextureLayers[0].Texture; @@ -918,26 +927,15 @@ void GenericCAO::setNodeLight(const video::SColor &light_color) } if (m_enable_shaders) { - if (m_prop.visual == "upright_sprite") { - if (!m_meshnode) - return; - for (u32 i = 0; i < m_meshnode->getMaterialCount(); ++i) - m_meshnode->getMaterial(i).EmissiveColor = light_color; - } else { - scene::ISceneNode *node = getSceneNode(); - if (!node) - return; - - for (u32 i = 0; i < node->getMaterialCount(); ++i) { - video::SMaterial &material = node->getMaterial(i); - material.EmissiveColor = light_color; - } - } + auto *node = getSceneNode(); + if (!node) + return; + setColorParam(node, light_color); } else { if (m_meshnode) { setMeshColor(m_meshnode->getMesh(), light_color); } else if (m_animated_meshnode) { - setAnimatedMeshColor(m_animated_meshnode, light_color); + setMeshColor(m_animated_meshnode->getMesh(), light_color); } else if (m_spritenode) { m_spritenode->setColor(light_color); } @@ -1054,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 @@ -1259,6 +1257,16 @@ void GenericCAO::step(float dtime, ClientEnvironment *env) } } +static void setMeshBufferTextureCoords(scene::IMeshBuffer *buf, const v2f *uv, u32 count) +{ + assert(buf->getVertexType() == video::EVT_STANDARD); + assert(buf->getVertexCount() == count); + auto *vertices = static_cast(buf->getVertices()); + for (u32 i = 0; i < count; i++) + vertices[i].TCoords = uv[i]; + buf->setDirty(scene::EBT_VERTEX); +} + void GenericCAO::updateTexturePos() { if(m_spritenode) @@ -1352,15 +1360,6 @@ void GenericCAO::updateTextures(std::string mod) material.MaterialTypeParam = m_material_type_param; material.setTexture(0, tsrc->getTextureForMesh(texturestring)); - // This allows setting per-material colors. However, until a real lighting - // system is added, the code below will have no effect. Once MineTest - // has directional lighting, it should work automatically. - if (!m_prop.colors.empty()) { - material.AmbientColor = m_prop.colors[0]; - material.DiffuseColor = m_prop.colors[0]; - material.SpecularColor = m_prop.colors[0]; - } - material.forEachTexture([=] (auto &tex) { setMaterialFilters(tex, use_bilinear_filter, use_trilinear_filter, use_anisotropic_filter); @@ -1370,9 +1369,11 @@ void GenericCAO::updateTextures(std::string mod) else if (m_animated_meshnode) { if (m_prop.visual == "mesh") { - for (u32 i = 0; i < m_prop.textures.size() && - i < m_animated_meshnode->getMaterialCount(); ++i) { - std::string texturestring = m_prop.textures[i]; + for (u32 i = 0; i < m_animated_meshnode->getMaterialCount(); ++i) { + const auto texture_idx = m_animated_meshnode->getMesh()->getTextureSlot(i); + if (texture_idx >= m_prop.textures.size()) + continue; + std::string texturestring = m_prop.textures[texture_idx]; if (texturestring.empty()) continue; // Empty texture string means don't modify that material texturestring += mod; @@ -1387,7 +1388,6 @@ void GenericCAO::updateTextures(std::string mod) material.MaterialType = m_material_type; material.MaterialTypeParam = m_material_type_param; material.TextureLayers[0].Texture = texture; - material.Lighting = true; material.BackfaceCulling = m_prop.backface_culling; // don't filter low-res textures, makes them look blurry @@ -1402,17 +1402,6 @@ void GenericCAO::updateTextures(std::string mod) use_anisotropic_filter); }); } - for (u32 i = 0; i < m_prop.colors.size() && - i < m_animated_meshnode->getMaterialCount(); ++i) - { - video::SMaterial &material = m_animated_meshnode->getMaterial(i); - // This allows setting per-material colors. However, until a real lighting - // system is added, the code below will have no effect. Once MineTest - // has directional lighting, it should work automatically. - material.AmbientColor = m_prop.colors[i]; - material.DiffuseColor = m_prop.colors[i]; - material.SpecularColor = m_prop.colors[i]; - } } } @@ -1430,20 +1419,9 @@ void GenericCAO::updateTextures(std::string mod) video::SMaterial &material = m_meshnode->getMaterial(i); material.MaterialType = m_material_type; material.MaterialTypeParam = m_material_type_param; - material.Lighting = false; material.setTexture(0, tsrc->getTextureForMesh(texturestring)); material.getTextureMatrix(0).makeIdentity(); - // This allows setting per-material colors. However, until a real lighting - // system is added, the code below will have no effect. Once MineTest - // has directional lighting, it should work automatically. - if(m_prop.colors.size() > i) - { - material.AmbientColor = m_prop.colors[i]; - material.DiffuseColor = m_prop.colors[i]; - material.SpecularColor = m_prop.colors[i]; - } - material.forEachTexture([=] (auto &tex) { setMaterialFilters(tex, use_bilinear_filter, use_trilinear_filter, use_anisotropic_filter); @@ -1460,15 +1438,6 @@ void GenericCAO::updateTextures(std::string mod) auto &material = m_meshnode->getMaterial(0); material.setTexture(0, tsrc->getTextureForMesh(tname)); - // This allows setting per-material colors. However, until a real lighting - // system is added, the code below will have no effect. Once MineTest - // has directional lighting, it should work automatically. - if(!m_prop.colors.empty()) { - material.AmbientColor = m_prop.colors[0]; - material.DiffuseColor = m_prop.colors[0]; - material.SpecularColor = m_prop.colors[0]; - } - material.forEachTexture([=] (auto &tex) { setMaterialFilters(tex, use_bilinear_filter, use_trilinear_filter, use_anisotropic_filter); @@ -1485,19 +1454,6 @@ void GenericCAO::updateTextures(std::string mod) auto &material = m_meshnode->getMaterial(1); material.setTexture(0, tsrc->getTextureForMesh(tname)); - // This allows setting per-material colors. However, until a real lighting - // system is added, the code below will have no effect. Once MineTest - // has directional lighting, it should work automatically. - if (m_prop.colors.size() >= 2) { - material.AmbientColor = m_prop.colors[1]; - material.DiffuseColor = m_prop.colors[1]; - material.SpecularColor = m_prop.colors[1]; - } else if (!m_prop.colors.empty()) { - material.AmbientColor = m_prop.colors[0]; - material.DiffuseColor = m_prop.colors[0]; - material.SpecularColor = m_prop.colors[0]; - } - material.forEachTexture([=] (auto &tex) { setMaterialFilters(tex, use_bilinear_filter, use_trilinear_filter, use_anisotropic_filter); @@ -1518,9 +1474,8 @@ void GenericCAO::updateAnimation() if (!m_animated_meshnode) return; - if (m_animated_meshnode->getStartFrame() != m_animation_range.X || - m_animated_meshnode->getEndFrame() != m_animation_range.Y) - m_animated_meshnode->setFrameLoop(m_animation_range.X, m_animation_range.Y); + // Note: This sets the current frame as well, (re)starting the animation. + m_animated_meshnode->setFrameLoop(m_animation_range.X, m_animation_range.Y); if (m_animated_meshnode->getAnimationSpeed() != m_animation_speed) m_animated_meshnode->setAnimationSpeed(m_animation_speed); m_animated_meshnode->setTransitionTime(m_animation_blend); @@ -1844,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 @@ -1857,7 +1811,7 @@ void GenericCAO::processMessage(const std::string &data) LocalPlayer *player = m_env->getLocalPlayer(); if(player->last_animation == LocalPlayerAnimation::NO_ANIM) { - m_animation_range = v2s32((s32)range.X, (s32)range.Y); + m_animation_range = range; m_animation_speed = readF32(is); m_animation_blend = readF32(is); // these are sent inverted so we get true when the server sends nothing diff --git a/src/client/content_cao.h b/src/client/content_cao.h index 3fdf01bc7..d138e39c3 100644 --- a/src/client/content_cao.h +++ b/src/client/content_cao.h @@ -99,7 +99,7 @@ private: v2s16 m_tx_basepos; bool m_initial_tx_basepos_set = false; bool m_tx_select_horiz_by_yawpitch = false; - v2s32 m_animation_range; + v2f m_animation_range; float m_animation_speed = 15.0f; float m_animation_blend = 0.0f; bool m_animation_loop = true; diff --git a/src/client/content_cso.cpp b/src/client/content_cso.cpp index c175df72e..733044b23 100644 --- a/src/client/content_cso.cpp +++ b/src/client/content_cso.cpp @@ -19,6 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "content_cso.h" #include +#include #include "client/texturesource.h" #include "clientenvironment.h" #include "client.h" @@ -39,7 +40,6 @@ public: video::ITexture *tex = env->getGameDef()->tsrc()->getTextureForMesh("smoke_puff.png"); m_spritenode->forEachMaterial([tex] (auto &mat) { mat.TextureLayers[0].Texture = tex; - mat.Lighting = false; mat.TextureLayers[0].MinFilter = video::ETMINF_NEAREST_MIPMAP_NEAREST; mat.TextureLayers[0].MagFilter = video::ETMAGF_NEAREST; mat.MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL; diff --git a/src/client/content_cso.h b/src/client/content_cso.h index cc9213175..b94580a61 100644 --- a/src/client/content_cso.h +++ b/src/client/content_cso.h @@ -19,8 +19,13 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once -#include "irrlichttypes_extrabloated.h" +#include "irrlichttypes_bloated.h" #include "clientsimpleobject.h" +namespace irr::scene +{ + class ISceneManager; +} + ClientSimpleObject* createSmokePuff(scene::ISceneManager *smgr, ClientEnvironment *env, v3f pos, v2f size); diff --git a/src/client/content_mapblock.cpp b/src/client/content_mapblock.cpp index c351c4b80..6ce3ca0f4 100644 --- a/src/client/content_mapblock.cpp +++ b/src/client/content_mapblock.cpp @@ -19,8 +19,10 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include "content_mapblock.h" +#include "util/basic_macros.h" #include "util/numeric.h" #include "util/directiontables.h" +#include "util/tracy_wrapper.h" #include "mapblock_mesh.h" #include "settings.h" #include "nodedef.h" @@ -81,7 +83,8 @@ MapblockMeshGenerator::MapblockMeshGenerator(MeshMakeData *input, MeshCollector meshmanip(mm), blockpos_nodes(data->m_blockpos * MAP_BLOCKSIZE), enable_mesh_cache(g_settings->getBool("enable_mesh_cache") && - !data->m_smooth_lighting) // Mesh cache is not supported with smooth lighting + !data->m_smooth_lighting), // Mesh cache is not supported with smooth lighting + smooth_liquids(g_settings->getBool("enable_water_reflections")) { } @@ -463,6 +466,8 @@ void MapblockMeshGenerator::drawSolidNode() if (data->m_smooth_lighting) { LightPair lights[6][4]; for (int face = 0; face < 6; ++face) { + if (mask & (1 << face)) + continue; for (int k = 0; k < 4; k++) { v3s16 corner = light_dirs[light_indices[face][k]]; lights[face][k] = LightPair(getSmoothLightSolid( @@ -713,7 +718,7 @@ void MapblockMeshGenerator::drawLiquidSides() if (data->m_smooth_lighting) cur_node.color = blendLightColor(pos); pos += cur_node.origin; - vertices[j] = video::S3DVertex(pos.X, pos.Y, pos.Z, 0, 0, 0, cur_node.color, vertex.u, v); + vertices[j] = video::S3DVertex(pos.X, pos.Y, pos.Z, face.dir.X, face.dir.Y, face.dir.Z, cur_node.color, vertex.u, v); }; collector->append(cur_liquid.tile, vertices, 4, quad_indices, 6); } @@ -736,6 +741,19 @@ void MapblockMeshGenerator::drawLiquidTop() for (int i = 0; i < 4; i++) { int u = corner_resolve[i][0]; int w = corner_resolve[i][1]; + + if (smooth_liquids) { + int x = vertices[i].Pos.X > 0; + int z = vertices[i].Pos.Z > 0; + + f32 dx = 0.5f * (cur_liquid.neighbors[z][x].level - cur_liquid.neighbors[z][x + 1].level + + cur_liquid.neighbors[z + 1][x].level - cur_liquid.neighbors[z + 1][x + 1].level); + f32 dz = 0.5f * (cur_liquid.neighbors[z][x].level - cur_liquid.neighbors[z + 1][x].level + + cur_liquid.neighbors[z][x + 1].level - cur_liquid.neighbors[z + 1][x + 1].level); + + vertices[i].Normal = v3f(dx, 1., dz).normalize(); + } + vertices[i].Pos.Y += cur_liquid.corner_levels[w][u] * BS; if (data->m_smooth_lighting) vertices[i].Color = blendLightColor(vertices[i].Pos); @@ -775,6 +793,10 @@ void MapblockMeshGenerator::drawLiquidTop() vertex.TCoords += tcoord_center; vertex.TCoords += tcoord_translate; + + if (!smooth_liquids) { + vertex.Normal = v3f(dx, 1., dz).normalize(); + } } std::swap(vertices[0].TCoords, vertices[2].TCoords); @@ -994,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); @@ -1523,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]; @@ -1674,7 +1700,9 @@ void MapblockMeshGenerator::drawMeshNode() int mesh_buffer_count = mesh->getMeshBufferCount(); for (int j = 0; j < mesh_buffer_count; j++) { - useTile(j); + // Only up to 6 tiles are supported + const auto tile = mesh->getTextureSlot(j); + useTile(MYMIN(tile, 5)); scene::IMeshBuffer *buf = mesh->getMeshBuffer(j); video::S3DVertex *vertices = (video::S3DVertex *)buf->getVertices(); int vertex_count = buf->getVertexCount(); @@ -1745,6 +1773,8 @@ void MapblockMeshGenerator::drawNode() void MapblockMeshGenerator::generate() { + ZoneScoped; + for (cur_node.p.Z = 0; cur_node.p.Z < data->side_length; cur_node.p.Z++) for (cur_node.p.Y = 0; cur_node.p.Y < data->side_length; cur_node.p.Y++) for (cur_node.p.X = 0; cur_node.p.X < data->side_length; cur_node.p.X++) { diff --git a/src/client/content_mapblock.h b/src/client/content_mapblock.h index 730330a03..1c3060c83 100644 --- a/src/client/content_mapblock.h +++ b/src/client/content_mapblock.h @@ -134,6 +134,7 @@ private: f32 corner_levels[2][2]; }; LiquidData cur_liquid; + bool smooth_liquids = false; void prepareLiquidNodeDrawing(); void getLiquidNeighborhood(); diff --git a/src/client/filecache.cpp b/src/client/filecache.cpp index 1f7e605d1..4dd7ec72f 100644 --- a/src/client/filecache.cpp +++ b/src/client/filecache.cpp @@ -20,7 +20,6 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "filecache.h" -#include "network/networkprotocol.h" #include "log.h" #include "filesys.h" #include diff --git a/src/client/fontengine.cpp b/src/client/fontengine.cpp index e0174e011..5475e9768 100644 --- a/src/client/fontengine.cpp +++ b/src/client/fontengine.cpp @@ -33,10 +33,20 @@ FontEngine *g_fontengine = nullptr; /** callback to be used on change of font size setting */ static void font_setting_changed(const std::string &name, void *userdata) { - if (g_fontengine) - g_fontengine->readSettings(); + static_cast(userdata)->readSettings(); } +static const char *settings[] = { + "font_size", "font_bold", "font_italic", "font_size_divisible_by", + "mono_font_size", "mono_font_size_divisible_by", + "font_shadow", "font_shadow_alpha", + "font_path", "font_path_bold", "font_path_italic", "font_path_bold_italic", + "mono_font_path", "mono_font_path_bold", "mono_font_path_italic", + "mono_font_path_bold_italic", + "fallback_font_path", + "dpi_change_notifier", "display_density_factor", "gui_scaling", +}; + /******************************************************************************/ FontEngine::FontEngine(gui::IGUIEnvironment* env) : m_env(env) @@ -51,24 +61,16 @@ FontEngine::FontEngine(gui::IGUIEnvironment* env) : readSettings(); - const char *settings[] = { - "font_size", "font_bold", "font_italic", "font_size_divisible_by", - "mono_font_size", "mono_font_size_divisible_by", - "font_shadow", "font_shadow_alpha", - "font_path", "font_path_bold", "font_path_italic", "font_path_bold_italic", - "mono_font_path", "mono_font_path_bold", "mono_font_path_italic", - "mono_font_path_bold_italic", - "fallback_font_path", - "dpi_change_notifier", "gui_scaling", - }; - for (auto name : settings) - g_settings->registerChangedCallback(name, font_setting_changed, NULL); + g_settings->registerChangedCallback(name, font_setting_changed, this); } /******************************************************************************/ FontEngine::~FontEngine() { + for (auto name : settings) + g_settings->deregisterChangedCallback(name, font_setting_changed, this); + cleanCache(); } diff --git a/src/client/game.cpp b/src/client/game.cpp index 4ec2ff082..f4ef8ffce 100644 --- a/src/client/game.cpp +++ b/src/client/game.cpp @@ -40,9 +40,10 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "content/subgames.h" #include "client/event_manager.h" #include "fontengine.h" -#include "gui/touchscreengui.h" +#include "gui/touchcontrols.h" #include "itemdef.h" #include "log.h" +#include "log_internal.h" #include "filesys.h" #include "gameparams.h" #include "gettext.h" @@ -79,6 +80,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "hud.h" #include "clientdynamicinfo.h" #include +#include "util/tracy_wrapper.h" #if USE_SOUND #include "client/sound/sound_openal.h" @@ -185,7 +187,7 @@ struct LocalFormspecHandler : public TextDest assert(m_client != nullptr); if (fields.find("quit") != fields.end()) - m_client->sendRespawn(); + m_client->sendRespawnLegacy(); return; } @@ -385,6 +387,8 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter CachedPixelShaderSetting m_minimap_yaw{"yawVec"}; CachedPixelShaderSetting m_camera_offset_pixel{"cameraOffset"}; CachedVertexShaderSetting m_camera_offset_vertex{"cameraOffset"}; + CachedPixelShaderSetting m_camera_position_pixel{ "cameraPosition" }; + CachedVertexShaderSetting m_camera_position_vertex{ "cameraPosition" }; CachedPixelShaderSetting m_texture0{"texture0"}; CachedPixelShaderSetting m_texture1{"texture1"}; CachedPixelShaderSetting m_texture2{"texture2"}; @@ -402,11 +406,8 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter float m_user_exposure_compensation; bool m_bloom_enabled; CachedPixelShaderSetting m_bloom_intensity_pixel{"bloomIntensity"}; - float m_bloom_intensity; CachedPixelShaderSetting m_bloom_strength_pixel{"bloomStrength"}; - float m_bloom_strength; CachedPixelShaderSetting m_bloom_radius_pixel{"bloomRadius"}; - float m_bloom_radius; CachedPixelShaderSetting m_saturation_pixel{"saturation"}; bool m_volumetric_light_enabled; CachedPixelShaderSetting @@ -418,11 +419,8 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter CachedPixelShaderSetting m_volumetric_light_strength_pixel{"volumetricLightStrength"}; - static constexpr std::array SETTING_CALLBACKS = { + static constexpr std::array SETTING_CALLBACKS = { "exposure_compensation", - "bloom_intensity", - "bloom_strength_factor", - "bloom_radius" }; public: @@ -430,12 +428,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); } static void settingsCallback(const std::string &name, void *userdata) @@ -454,9 +446,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_volumetric_light_enabled = g_settings->getBool("enable_volumetric_lighting") && m_bloom_enabled; } @@ -491,6 +480,10 @@ public: m_camera_offset_pixel.set(offset, services); m_camera_offset_vertex.set(offset, services); + v3f camera_position = m_client->getCamera()->getPosition(); + m_camera_position_pixel.set(camera_position, services); + m_camera_position_pixel.set(camera_position, services); + SamplerLayer_t tex_id; tex_id = 0; m_texture0.set(&tex_id, services); @@ -504,7 +497,9 @@ public: m_texel_size0_vertex.set(m_texel_size0, services); m_texel_size0_pixel.set(m_texel_size0, services); - const AutoExposure &exposure_params = m_client->getEnv().getLocalPlayer()->getLighting().exposure; + const auto &lighting = m_client->getEnv().getLocalPlayer()->getLighting(); + + const AutoExposure &exposure_params = lighting.exposure; std::array exposure_buffer = { std::pow(2.0f, exposure_params.luminance_min), std::pow(2.0f, exposure_params.luminance_max), @@ -517,12 +512,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); } - const auto &lighting = m_client->getEnv().getLocalPlayer()->getLighting(); float saturation = lighting.saturation; m_saturation_pixel.set(&saturation, services); @@ -594,7 +591,8 @@ public: m_client(client) {} - void setSky(Sky *sky) { + void setSky(Sky *sky) + { m_sky = sky; for (GameGlobalShaderConstantSetter *ggscs : created_nosky) { ggscs->setSky(m_sky); @@ -725,6 +723,7 @@ protected: void processUserInput(f32 dtime); void processKeyInput(); void processItemSelection(u16 *new_playeritem); + bool shouldShowTouchControls(); void dropSelectedItem(bool single_item = false); void openInventory(); @@ -829,7 +828,7 @@ private: bool disable_camera_update = false; }; - void showDeathFormspec(); + void showDeathFormspecLegacy(); void showPauseMenu(); void pauseAnimation(); @@ -839,7 +838,7 @@ private: void handleClientEvent_None(ClientEvent *event, CameraOrientation *cam); void handleClientEvent_PlayerDamage(ClientEvent *event, CameraOrientation *cam); void handleClientEvent_PlayerForceMove(ClientEvent *event, CameraOrientation *cam); - void handleClientEvent_Deathscreen(ClientEvent *event, CameraOrientation *cam); + void handleClientEvent_DeathscreenLegacy(ClientEvent *event, CameraOrientation *cam); void handleClientEvent_ShowFormSpec(ClientEvent *event, CameraOrientation *cam); void handleClientEvent_ShowLocalFormSpec(ClientEvent *event, CameraOrientation *cam); void handleClientEvent_HandleParticleEvent(ClientEvent *event, @@ -883,17 +882,17 @@ private: SoundMaker *soundmaker = nullptr; ChatBackend *chat_backend = nullptr; - LogOutputBuffer m_chat_log_buf; + CaptureLogOutput m_chat_log_buf; EventManager *eventmgr = nullptr; QuicktuneShortcutter *quicktune = nullptr; std::unique_ptr m_game_ui; - GUIChatConsole *gui_chat_console = nullptr; // Free using ->Drop() + irr_ptr gui_chat_console; MapDrawControl *draw_control = nullptr; Camera *camera = nullptr; - Clouds *clouds = nullptr; // Free using ->Drop() - Sky *sky = nullptr; // Free using ->Drop() + irr_ptr clouds; + irr_ptr sky; Hud *hud = nullptr; Minimap *mapper = nullptr; @@ -931,6 +930,7 @@ private: * (as opposed to the this local caching). This can be addressed in * a later release. */ + bool m_cache_disable_escape_sequences; bool m_cache_doubletap_jump; bool m_cache_enable_clouds; bool m_cache_enable_joysticks; @@ -974,6 +974,10 @@ Game::Game() : m_chat_log_buf(g_logger), m_game_ui(new GameUI()) { + g_settings->registerChangedCallback("chat_log_level", + &settingChangedCallback, this); + g_settings->registerChangedCallback("disable_escape_sequences", + &settingChangedCallback, this); g_settings->registerChangedCallback("doubletap_jump", &settingChangedCallback, this); g_settings->registerChangedCallback("enable_clouds", @@ -1042,6 +1046,10 @@ Game::~Game() clearTextureNameCache(); + g_settings->deregisterChangedCallback("chat_log_level", + &settingChangedCallback, this); + g_settings->deregisterChangedCallback("disable_escape_sequences", + &settingChangedCallback, this); g_settings->deregisterChangedCallback("doubletap_jump", &settingChangedCallback, this); g_settings->deregisterChangedCallback("enable_clouds", @@ -1140,6 +1148,8 @@ bool Game::startup(bool *kill, void Game::run() { + ZoneScoped; + ProfilerGraph graph; RunStats stats = {}; CameraOrientation cam_view_target = {}; @@ -1167,15 +1177,21 @@ void Game::run() const bool initial_window_maximized = !g_settings->getBool("fullscreen") && g_settings->getBool("window_maximized"); + auto framemarker = FrameMarker("Game::run()-frame").started(); + while (m_rendering_engine->run() && !(*kill || g_gamecallback->shutdown_requested || (server && server->isShutdownRequested()))) { + framemarker.end(); + // Calculate dtime = // m_rendering_engine->run() from this iteration // + Sleep time until the wanted FPS are reached draw_times.limit(device, &dtime, g_menumgr.pausesGame()); + framemarker.start(); + const auto current_dynamic_info = ClientDynamicInfo::getCurrent(); if (!current_dynamic_info.equal(client_display_info)) { client_display_info = current_dynamic_info; @@ -1232,6 +1248,8 @@ void Game::run() } } + framemarker.end(); + RenderingEngine::autosaveScreensizeAndCo(initial_screen_size, initial_window_maximized); } @@ -1245,21 +1263,18 @@ void Game::shutdown() // Clear text when exiting. m_game_ui->clearText(); - if (g_touchscreengui) - g_touchscreengui->hide(); + if (g_touchcontrols) + g_touchcontrols->hide(); // only if the shutdown progress bar isn't shown yet if (m_shutdown_progress == 0.0f) showOverlayMessage(N_("Shutting down..."), 0, 0); - if (clouds) - clouds->drop(); + clouds.reset(); - if (gui_chat_console) - gui_chat_console->drop(); + gui_chat_console.reset(); - if (sky) - sky->drop(); + sky.reset(); /* cleanup menus */ while (g_menumgr.menuCount() > 0) { @@ -1271,7 +1286,6 @@ void Game::shutdown() chat_backend->addMessage(L"", L"# Disconnected."); chat_backend->addMessage(L"", L""); - m_chat_log_buf.clear(); if (client) { client->Stop(); @@ -1446,7 +1460,7 @@ void Game::copyServerClientCache() { // It would be possible to let the client directly read the media files // from where the server knows they are. But aside from being more complicated - // it would also *not* fill the media cache and cause slower joining of + // it would also *not* fill the media cache and cause slower joining of // remote servers. // (Imagine that you launch a game once locally and then connect to a server.) @@ -1510,19 +1524,15 @@ bool Game::createClient(const GameStartData &start_data) client->getScript()->on_camera_ready(camera); client->setCamera(camera); - if (g_touchscreengui) { - g_touchscreengui->setUseCrosshair(!isTouchCrosshairDisabled()); - } - /* Clouds */ if (m_cache_enable_clouds) - clouds = new Clouds(smgr, shader_src, -1, rand()); + clouds = make_irr(smgr, shader_src, -1, rand()); /* Skybox */ - sky = new Sky(-1, m_rendering_engine, texture_src, shader_src); - scsf->setSky(sky); + sky = make_irr(-1, m_rendering_engine, texture_src, shader_src); + scsf->setSky(sky.get()); /* Pre-calculated values */ @@ -1564,6 +1574,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(); @@ -1575,11 +1593,13 @@ bool Game::initGui() chat_backend->applySettings(); // Chat backend and console - gui_chat_console = new GUIChatConsole(guienv, guienv->getRootGUIElement(), + gui_chat_console = make_irr(guienv, guienv->getRootGUIElement(), -1, chat_backend, client, &g_menumgr); - if (g_settings->getBool("enable_touch")) - g_touchscreengui = new TouchScreenGUI(device, texture_src); + if (shouldShowTouchControls()) { + g_touchcontrols = new TouchControls(device, texture_src); + g_touchcontrols->setUseCrosshair(!isTouchCrosshairDisabled()); + } return true; } @@ -1673,9 +1693,13 @@ bool Game::connectToServer(const GameStartData &start_data, fps_control.reset(); + auto framemarker = FrameMarker("Game::connectToServer()-frame").started(); + while (m_rendering_engine->run()) { + framemarker.end(); fps_control.limit(device, &dtime); + framemarker.start(); // Update client and server step(dtime); @@ -1721,6 +1745,7 @@ bool Game::connectToServer(const GameStartData &start_data, // Update status showOverlayMessage(N_("Connecting to server..."), dtime, 20); } + framemarker.end(); } catch (con::PeerNotFoundException &e) { warningstream << "This should not happen. Please report a bug." << std::endl; return false; @@ -1738,9 +1763,11 @@ bool Game::getServerContent(bool *aborted) fps_control.reset(); + auto framemarker = FrameMarker("Game::getServerContent()-frame").started(); while (m_rendering_engine->run()) { - + framemarker.end(); fps_control.limit(device, &dtime); + framemarker.start(); // Update client and server step(dtime); @@ -1806,6 +1833,7 @@ bool Game::getServerContent(bool *aborted) texture_src, dtime, progress); } } + framemarker.end(); *aborted = true; infostream << "Connect aborted [device]" << std::endl; @@ -1856,26 +1884,26 @@ inline bool Game::handleCallbacks() } if (g_gamecallback->changepassword_requested) { - (new GUIPasswordChange(guienv, guiroot, -1, - &g_menumgr, client, texture_src))->drop(); + (void)make_irr(guienv, guiroot, -1, + &g_menumgr, client, texture_src); g_gamecallback->changepassword_requested = false; } if (g_gamecallback->changevolume_requested) { - (new GUIVolumeChange(guienv, guiroot, -1, - &g_menumgr, texture_src))->drop(); + (void)make_irr(guienv, guiroot, -1, + &g_menumgr, texture_src); g_gamecallback->changevolume_requested = false; } if (g_gamecallback->keyconfig_requested) { - (new GUIKeyChangeMenu(guienv, guiroot, -1, - &g_menumgr, texture_src))->drop(); + (void)make_irr(guienv, guiroot, -1, + &g_menumgr, texture_src); g_gamecallback->keyconfig_requested = false; } if (!g_gamecallback->show_open_url_dialog.empty()) { - (new GUIOpenURLMenu(guienv, guiroot, -1, - &g_menumgr, texture_src, g_gamecallback->show_open_url_dialog))->drop(); + (void)make_irr(guienv, guiroot, -1, + &g_menumgr, texture_src, g_gamecallback->show_open_url_dialog); g_gamecallback->show_open_url_dialog.clear(); } @@ -1953,6 +1981,14 @@ void Game::updateProfilers(const RunStats &stats, const FpsControl &draw_times, g_profiler->graphAdd("Sleep [us]", draw_times.sleep_time); g_profiler->graphSet("FPS", 1.0f / dtime); + + auto stats2 = driver->getFrameStats(); + g_profiler->avg("Irr: drawcalls", stats2.Drawcalls); + if (stats2.Drawcalls > 0) + g_profiler->avg("Irr: primitives per drawcall", + stats2.PrimitivesDrawn / float(stats2.Drawcalls)); + g_profiler->avg("Irr: buffers uploaded", stats2.HWBuffersUploaded); + g_profiler->avg("Irr: buffers uploaded (bytes)", stats2.HWBuffersUploadedSize); } void Game::updateStats(RunStats *stats, const FpsControl &draw_times, @@ -2012,8 +2048,17 @@ 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)) { + if (!device->isWindowActive() || isMenuActive() || guienv->hasFocus(gui_chat_console.get())) { if (m_game_focused) { m_game_focused = false; infostream << "Game lost focus" << std::endl; @@ -2022,15 +2067,15 @@ void Game::processUserInput(f32 dtime) input->clear(); } - if (g_touchscreengui) - g_touchscreengui->hide(); + if (g_touchcontrols) + g_touchcontrols->hide(); } else { - if (g_touchscreengui) { - /* on touchscreengui step may generate own input events which ain't + if (g_touchcontrols) { + /* on touchcontrols step may generate own input events which ain't * what we want in case we just did clear them */ - g_touchscreengui->show(); - g_touchscreengui->step(dtime); + g_touchcontrols->show(); + g_touchcontrols->step(dtime); } m_game_focused = true; @@ -2227,8 +2272,8 @@ void Game::processItemSelection(u16 *new_playeritem) } } - if (g_touchscreengui) { - std::optional selection = g_touchscreengui->getHotbarSelection(); + if (g_touchcontrols) { + std::optional selection = g_touchcontrols->getHotbarSelection(); if (selection) *new_playeritem = *selection; } @@ -2633,7 +2678,7 @@ void Game::updateCameraDirection(CameraOrientation *cam, float dtime) this results in duplicated input. To avoid that, we don't enable relative mouse mode if we're in touchscreen mode. */ if (cur_control) - cur_control->setRelativeMode(!g_touchscreengui && !isMenuActive()); + cur_control->setRelativeMode(!g_touchcontrols && !isMenuActive()); if ((device->isWindowActive() && device->isWindowFocused() && !isMenuActive()) || input->isRandom()) { @@ -2644,7 +2689,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, @@ -2660,6 +2705,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 @@ -2676,9 +2723,9 @@ f32 Game::getSensitivityScaleFactor() const void Game::updateCameraOrientation(CameraOrientation *cam, float dtime) { - if (g_touchscreengui) { - cam->camera_yaw += g_touchscreengui->getYawChange(); - cam->camera_pitch += g_touchscreengui->getPitchChange(); + if (g_touchcontrols) { + cam->camera_yaw += g_touchcontrols->getYawChange(); + cam->camera_pitch += g_touchcontrols->getPitchChange(); } else { v2s32 center(driver->getScreenSize().Width / 2, driver->getScreenSize().Height / 2); v2s32 dist = input->getMousePos() - center; @@ -2725,9 +2772,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 && @@ -2743,7 +2791,7 @@ void Game::updatePlayerControl(const CameraOrientation &cam) * touch then its meaning is inverted (i.e. holding aux1 means walk and * not fast) */ - if (g_touchscreengui && m_touch_simulate_aux1) { + if (g_touchcontrols && m_touch_simulate_aux1) { control.aux1 = control.aux1 ^ true; } @@ -2769,6 +2817,8 @@ void Game::updatePauseState() inline void Game::step(f32 dtime) { + ZoneScoped; + if (server) { float fps_max = (!device->isWindowFocused() || g_menumgr.pausesGame()) ? g_settings->getFloat("fps_max_unfocused") : @@ -2825,7 +2875,7 @@ const ClientEventHandler Game::clientEventHandler[CLIENTEVENT_MAX] = { {&Game::handleClientEvent_None}, {&Game::handleClientEvent_PlayerDamage}, {&Game::handleClientEvent_PlayerForceMove}, - {&Game::handleClientEvent_Deathscreen}, + {&Game::handleClientEvent_DeathscreenLegacy}, {&Game::handleClientEvent_ShowFormSpec}, {&Game::handleClientEvent_ShowLocalFormSpec}, {&Game::handleClientEvent_HandleParticleEvent}, @@ -2881,20 +2931,9 @@ void Game::handleClientEvent_PlayerForceMove(ClientEvent *event, CameraOrientati cam->camera_pitch = event->player_force_move.pitch; } -void Game::handleClientEvent_Deathscreen(ClientEvent *event, CameraOrientation *cam) +void Game::handleClientEvent_DeathscreenLegacy(ClientEvent *event, CameraOrientation *cam) { - // If client scripting is enabled, deathscreen is handled by CSM code in - // builtin/client/init.lua - if (client->modsLoaded()) - client->getScript()->on_death(); - else - showDeathFormspec(); - - /* Handle visualization */ - LocalPlayer *player = client->getEnv().getLocalPlayer(); - runData.damage_flash = 0; - player->hurt_tilt_timer = 0; - player->hurt_tilt_strength = 0; + showDeathFormspecLegacy(); } void Game::handleClientEvent_ShowFormSpec(ClientEvent *event, CameraOrientation *cam) @@ -3161,6 +3200,7 @@ void Game::handleClientEvent_CloudParams(ClientEvent *event, CameraOrientation * clouds->setDensity(event->cloud_params.density); clouds->setColorBright(video::SColor(event->cloud_params.color_bright)); clouds->setColorAmbient(video::SColor(event->cloud_params.color_ambient)); + clouds->setColorShadow(video::SColor(event->cloud_params.color_shadow)); clouds->setHeight(event->cloud_params.height); clouds->setThickness(event->cloud_params.thickness); clouds->setSpeed(v2f(event->cloud_params.speed_x, event->cloud_params.speed_y)); @@ -3178,9 +3218,27 @@ void Game::processClientEvents(CameraOrientation *cam) void Game::updateChat(f32 dtime) { + auto color_for = [](LogLevel level) -> const char* { + switch (level) { + case LL_ERROR : return "\x1b(c@#F00)"; // red + case LL_WARNING: return "\x1b(c@#EE0)"; // yellow + case LL_INFO : return "\x1b(c@#BBB)"; // grey + case LL_VERBOSE: return "\x1b(c@#888)"; // dark grey + case LL_TRACE : return "\x1b(c@#888)"; // dark grey + default : return ""; + } + }; + // Get new messages from error log buffer - while (!m_chat_log_buf.empty()) - chat_backend->addMessage(L"", utf8_to_wide(m_chat_log_buf.get())); + std::vector entries = m_chat_log_buf.take(); + for (const auto& entry : entries) { + std::string line; + if (!m_cache_disable_escape_sequences) { + line.append(color_for(entry.level)); + } + line.append(entry.combined); + chat_backend->addMessage(L"", utf8_to_wide(line)); + } // Get new messages from client std::wstring message; @@ -3232,8 +3290,8 @@ void Game::updateCamera(f32 dtime) camera->toggleCameraMode(); - if (g_touchscreengui) - g_touchscreengui->setUseCrosshair(!isTouchCrosshairDisabled()); + if (g_touchcontrols) + g_touchcontrols->setUseCrosshair(!isTouchCrosshairDisabled()); // Make the player visible depending on camera mode. playercao->updateMeshCulling(); @@ -3334,8 +3392,8 @@ void Game::processPlayerInteraction(f32 dtime, bool show_hud) } shootline.end = shootline.start + camera_direction * BS * d; - if (g_touchscreengui && isTouchCrosshairDisabled()) { - shootline = g_touchscreengui->getShootline(); + if (g_touchcontrols && isTouchCrosshairDisabled()) { + shootline = g_touchcontrols->getShootline(); // Scale shootline to the acual distance the player can reach shootline.end = shootline.start + shootline.getVector().normalize() * BS * d; @@ -3352,9 +3410,13 @@ void Game::processPlayerInteraction(f32 dtime, bool show_hud) if (pointed != runData.pointed_old) infostream << "Pointing at " << pointed.dump() << std::endl; - if (g_touchscreengui) { - auto mode = selected_def.touch_interaction.getMode(pointed.type); - g_touchscreengui->applyContextControls(mode); + if (g_touchcontrols) { + auto mode = selected_def.touch_interaction.getMode(selected_def, pointed.type); + g_touchcontrols->applyContextControls(mode); + // applyContextControls may change dig/place input. + // Update again so that TOSERVER_INTERACT packets have the correct controls set. + player->control.dig = isKeyDown(KeyType::DIG); + player->control.place = isKeyDown(KeyType::PLACE); } // Note that updating the selection mesh every frame is not particularly efficient, @@ -4044,6 +4106,7 @@ void Game::handleDigging(const PointedThing &pointed, const v3s16 &nodepos, void Game::updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime, const CameraOrientation &cam) { + ZoneScoped; TimeTaker tt_update("Game::updateFrame()"); LocalPlayer *player = client->getEnv().getLocalPlayer(); @@ -4188,7 +4251,8 @@ void Game::updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime, updateShadows(); } - m_game_ui->update(*stats, client, draw_control, cam, runData.pointed_old, gui_chat_console, dtime); + m_game_ui->update(*stats, client, draw_control, cam, runData.pointed_old, + gui_chat_console.get(), dtime); /* make sure menu is on top @@ -4286,7 +4350,9 @@ void Game::updateShadows() float timeoftheday = getWickedTimeOfDay(in_timeofday); bool is_day = timeoftheday > 0.25 && timeoftheday < 0.75; bool is_shadow_visible = is_day ? sky->getSunVisible() : sky->getMoonVisible(); - shadow->setShadowIntensity(is_shadow_visible ? client->getEnv().getLocalPlayer()->getLighting().shadow_intensity : 0.0f); + const auto &lighting = client->getEnv().getLocalPlayer()->getLighting(); + shadow->setShadowIntensity(is_shadow_visible ? lighting.shadow_intensity : 0.0f); + shadow->setShadowTint(lighting.shadow_tint); timeoftheday = std::fmod(timeoftheday + 0.75f, 0.5f) + 0.25f; const float offset_constant = 10000.0f; @@ -4303,6 +4369,8 @@ void Game::updateShadows() void Game::drawScene(ProfilerGraph *graph, RunStats *stats) { + ZoneScoped; + const video::SColor fog_color = this->sky->getFogColor(); const video::SColor sky_color = this->sky->getSkyColor(); @@ -4345,7 +4413,7 @@ void Game::drawScene(ProfilerGraph *graph, RunStats *stats) (player->hud_flags & HUD_FLAG_CROSSHAIR_VISIBLE) && (this->camera->getCameraMode() != CAMERA_MODE_THIRD_FRONT)); - if (g_touchscreengui && isTouchCrosshairDisabled()) + if (g_touchcontrols && isTouchCrosshairDisabled()) draw_crosshair = false; this->m_rendering_engine->draw_scene(sky_color, this->m_game_ui->m_flags.show_hud, @@ -4393,6 +4461,14 @@ void Game::settingChangedCallback(const std::string &setting_name, void *data) void Game::readSettings() { + LogLevel chat_log_level = Logger::stringToLevel(g_settings->get("chat_log_level")); + if (chat_log_level == LL_MAX) { + warningstream << "Supplied unrecognized chat_log_level; showing none." << std::endl; + chat_log_level = LL_NONE; + } + m_chat_log_buf.setLogLevel(chat_log_level); + + m_cache_disable_escape_sequences = g_settings->getBool("disable_escape_sequences"); m_cache_doubletap_jump = g_settings->getBool("doubletap_jump"); m_cache_enable_clouds = g_settings->getBool("enable_clouds"); m_cache_enable_joysticks = g_settings->getBool("enable_joysticks"); @@ -4428,7 +4504,7 @@ void Game::readSettings() ****************************************************************************/ /****************************************************************************/ -void Game::showDeathFormspec() +void Game::showDeathFormspecLegacy() { static std::string formspec_str = std::string("formspec_version[1]") + @@ -4456,7 +4532,7 @@ void Game::showPauseMenu() { std::string control_text; - if (g_touchscreengui) { + if (g_touchcontrols) { control_text = strgettext("Controls:\n" "No menu open:\n" "- slide finger: look around\n" diff --git a/src/client/gameui.cpp b/src/client/gameui.cpp index 0577943cb..41071ef66 100644 --- a/src/client/gameui.cpp +++ b/src/client/gameui.cpp @@ -23,10 +23,14 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include "gui/mainmenumanager.h" #include "gui/guiChatConsole.h" +#include "gui/guiFormSpecMenu.h" +#include "gui/touchcontrols.h" +#include "util/enriched_string.h" #include "util/pointedthing.h" #include "client.h" #include "clientmap.h" #include "fontengine.h" +#include "hud.h" // HUD_FLAG_* #include "nodedef.h" #include "profiler.h" #include "renderingengine.h" @@ -188,16 +192,27 @@ void GameUI::update(const RunStats &stats, Client *client, MapDrawControl *draw_ } } - setStaticText(m_guitext_status, m_statustext.c_str()); - m_guitext_status->setVisible(!m_statustext.empty()); + IGUIStaticText *guitext_status; + bool overriden = g_touchcontrols && g_touchcontrols->isStatusTextOverriden(); + if (overriden) { + guitext_status = g_touchcontrols->getStatusText(); + m_guitext_status->setVisible(false); + } else { + guitext_status = m_guitext_status; + if (g_touchcontrols) + g_touchcontrols->getStatusText()->setVisible(false); + } + + setStaticText(guitext_status, m_statustext.c_str()); + guitext_status->setVisible(!m_statustext.empty()); if (!m_statustext.empty()) { - s32 status_width = m_guitext_status->getTextWidth(); - s32 status_height = m_guitext_status->getTextHeight(); - s32 status_y = screensize.Y - 150; + s32 status_width = guitext_status->getTextWidth(); + s32 status_height = guitext_status->getTextHeight(); + s32 status_y = screensize.Y - (overriden ? 15 : 150); s32 status_x = (screensize.X - status_width) / 2; - m_guitext_status->setRelativePosition(core::rect(status_x , + guitext_status->setRelativePosition(core::rect(status_x , status_y - status_height, status_x + status_width, status_y)); // Fade out @@ -205,8 +220,8 @@ void GameUI::update(const RunStats &stats, Client *client, MapDrawControl *draw_ final_color.setAlpha(0); video::SColor fade_color = m_statustext_initial_color.getInterpolated_quadratic( m_statustext_initial_color, final_color, m_statustext_time / statustext_time_max); - m_guitext_status->setOverrideColor(fade_color); - m_guitext_status->enableOverrideColor(true); + guitext_status->setOverrideColor(fade_color); + guitext_status->enableOverrideColor(true); } // Hide chat when disabled by server or when console is visible diff --git a/src/client/gameui.h b/src/client/gameui.h index 5b87d43e6..59741c96c 100644 --- a/src/client/gameui.h +++ b/src/client/gameui.h @@ -22,15 +22,15 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "irrlichttypes.h" #include -#include "gui/guiFormSpecMenu.h" -#include "util/enriched_string.h" -#include "util/pointedthing.h" #include "game.h" using namespace irr; class Client; +class EnrichedString; class GUIChatConsole; +class GUIFormSpecMenu; struct MapDrawControl; +struct PointedThing; /* * This object intend to contain the core UI elements diff --git a/src/client/guiscalingfilter.cpp b/src/client/guiscalingfilter.cpp index b7b9af0ff..eebf44103 100644 --- a/src/client/guiscalingfilter.cpp +++ b/src/client/guiscalingfilter.cpp @@ -23,6 +23,9 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "util/numeric.h" #include #include "client/renderingengine.h" +#include +#include +#include /* Maintain a static cache to store the images that correspond to textures * in a format that's manipulable by code. Some platforms exhibit issues diff --git a/src/client/guiscalingfilter.h b/src/client/guiscalingfilter.h index f2d2fce10..c929bb2d3 100644 --- a/src/client/guiscalingfilter.h +++ b/src/client/guiscalingfilter.h @@ -18,7 +18,17 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once -#include "irrlichttypes_extrabloated.h" +#include "irrlichttypes.h" +#include +#include +#include + +namespace irr::video +{ + class IImage; + class ITexture; + class IVideoDriver; +} /* Manually insert an image into the cache, useful to avoid texture-to-image * conversion whenever we can intercept it. diff --git a/src/client/hud.cpp b/src/client/hud.cpp index 3a0e25a07..6fd6b7d03 100644 --- a/src/client/hud.cpp +++ b/src/client/hud.cpp @@ -39,7 +39,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "wieldmesh.h" #include "client/renderingengine.h" #include "client/minimap.h" -#include "gui/touchscreengui.h" +#include "gui/touchcontrols.h" #include "util/enriched_string.h" #include "irrlicht_changes/CGUITTFont.h" @@ -61,6 +61,7 @@ Hud::Hud(Client *client, LocalPlayer *player, readScalingSetting(); g_settings->registerChangedCallback("dpi_change_notifier", setting_changed_callback, this); + g_settings->registerChangedCallback("display_density_factor", setting_changed_callback, this); g_settings->registerChangedCallback("hud_scaling", setting_changed_callback, this); for (auto &hbar_color : hbar_colors) @@ -97,7 +98,8 @@ Hud::Hud(Client *client, LocalPlayer *player, m_mode = HIGHLIGHT_BOX; } - m_selection_material.Lighting = false; + // Initialize m_selection_material + if (g_settings->getBool("enable_shaders")) { IShaderSource *shdrsrc = client->getShaderSource(); @@ -118,28 +120,42 @@ Hud::Hud(Client *client, LocalPlayer *player, m_selection_material.MaterialType = video::EMT_SOLID; } + // Initialize m_block_bounds_material + if (g_settings->getBool("enable_shaders")) { + IShaderSource *shdrsrc = client->getShaderSource(); + auto shader_id = shdrsrc->getShader("default_shader", TILE_MATERIAL_ALPHA); + m_block_bounds_material.MaterialType = shdrsrc->getShaderInfo(shader_id).material; + } else { + m_block_bounds_material.MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL; + } + m_block_bounds_material.Thickness = + rangelim(g_settings->getS16("selectionbox_width"), 1, 5); + // Prepare mesh for compass drawing - m_rotation_mesh_buffer.Vertices.set_used(4); - m_rotation_mesh_buffer.Indices.set_used(6); + m_rotation_mesh_buffer.reset(new scene::SMeshBuffer()); + auto *b = m_rotation_mesh_buffer.get(); + auto &vertices = b->Vertices->Data; + auto &indices = b->Indices->Data; + vertices.resize(4); + indices.resize(6); video::SColor white(255, 255, 255, 255); v3f normal(0.f, 0.f, 1.f); - m_rotation_mesh_buffer.Vertices[0] = video::S3DVertex(v3f(-1.f, -1.f, 0.f), normal, white, v2f(0.f, 1.f)); - m_rotation_mesh_buffer.Vertices[1] = video::S3DVertex(v3f(-1.f, 1.f, 0.f), normal, white, v2f(0.f, 0.f)); - m_rotation_mesh_buffer.Vertices[2] = video::S3DVertex(v3f( 1.f, 1.f, 0.f), normal, white, v2f(1.f, 0.f)); - m_rotation_mesh_buffer.Vertices[3] = video::S3DVertex(v3f( 1.f, -1.f, 0.f), normal, white, v2f(1.f, 1.f)); + vertices[0] = video::S3DVertex(v3f(-1.f, -1.f, 0.f), normal, white, v2f(0.f, 1.f)); + vertices[1] = video::S3DVertex(v3f(-1.f, 1.f, 0.f), normal, white, v2f(0.f, 0.f)); + vertices[2] = video::S3DVertex(v3f( 1.f, 1.f, 0.f), normal, white, v2f(1.f, 0.f)); + vertices[3] = video::S3DVertex(v3f( 1.f, -1.f, 0.f), normal, white, v2f(1.f, 1.f)); - m_rotation_mesh_buffer.Indices[0] = 0; - m_rotation_mesh_buffer.Indices[1] = 1; - m_rotation_mesh_buffer.Indices[2] = 2; - m_rotation_mesh_buffer.Indices[3] = 2; - m_rotation_mesh_buffer.Indices[4] = 3; - m_rotation_mesh_buffer.Indices[5] = 0; + indices[0] = 0; + indices[1] = 1; + indices[2] = 2; + indices[3] = 2; + indices[4] = 3; + indices[5] = 0; - m_rotation_mesh_buffer.getMaterial().Lighting = false; - m_rotation_mesh_buffer.getMaterial().MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL; - m_rotation_mesh_buffer.setHardwareMappingHint(scene::EHM_STATIC); + b->getMaterial().MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL; + b->setHardwareMappingHint(scene::EHM_STATIC); } void Hud::readScalingSetting() @@ -155,6 +171,7 @@ void Hud::readScalingSetting() Hud::~Hud() { g_settings->deregisterChangedCallback("dpi_change_notifier", setting_changed_callback, this); + g_settings->deregisterChangedCallback("display_density_factor", setting_changed_callback, this); g_settings->deregisterChangedCallback("hud_scaling", setting_changed_callback, this); if (m_selection_mesh) @@ -240,7 +257,7 @@ void Hud::drawItem(const ItemStack &item, const core::rect& rect, // NOTE: selectitem = 0 -> no selected; selectitem is 1-based // mainlist can be NULL, but draw the frame anyway. -void Hud::drawItems(v2s32 upperleftpos, v2s32 screen_offset, s32 itemcount, +void Hud::drawItems(v2s32 screen_pos, v2s32 screen_offset, s32 itemcount, v2f alignment, s32 inv_offset, InventoryList *mainlist, u16 selectitem, u16 direction, bool is_hotbar) { @@ -253,9 +270,11 @@ void Hud::drawItems(v2s32 upperleftpos, v2s32 screen_offset, s32 itemcount, width = tmp; } - // Position of upper left corner of bar - v2s32 pos = screen_offset * m_scale_factor; - pos += upperleftpos; + // Position: screen_pos + screen_offset + alignment + v2s32 pos(screen_offset.X * m_scale_factor, screen_offset.Y * m_scale_factor); + pos += screen_pos; + pos.X += (alignment.X - 1.0f) * (width * 0.5f); + pos.Y += (alignment.Y - 1.0f) * (height * 0.5f); // Store hotbar_image in member variable, used by drawItem() if (hotbar_image != player->hotbar_image) { @@ -283,20 +302,20 @@ void Hud::drawItems(v2s32 upperleftpos, v2s32 screen_offset, s32 itemcount, // Draw items core::rect imgrect(0, 0, m_hotbar_imagesize, m_hotbar_imagesize); - const s32 list_size = mainlist ? mainlist->getSize() : 0; - for (s32 i = inv_offset; i < itemcount && i < list_size; i++) { + const s32 list_max = std::min(itemcount, (s32) (mainlist ? mainlist->getSize() : 0 )); + for (s32 i = inv_offset; i < list_max; i++) { s32 fullimglen = m_hotbar_imagesize + m_padding * 2; v2s32 steppos; switch (direction) { case HUD_DIR_RIGHT_LEFT: - steppos = v2s32(-(m_padding + (i - inv_offset) * fullimglen), m_padding); + steppos = v2s32(m_padding + (list_max - 1 - i - inv_offset) * fullimglen, m_padding); break; case HUD_DIR_TOP_BOTTOM: steppos = v2s32(m_padding, m_padding + (i - inv_offset) * fullimglen); break; case HUD_DIR_BOTTOM_TOP: - steppos = v2s32(m_padding, -(m_padding + (i - inv_offset) * fullimglen)); + steppos = v2s32(m_padding, m_padding + (list_max - 1 - i - inv_offset) * fullimglen); break; default: steppos = v2s32(m_padding + (i - inv_offset) * fullimglen, m_padding); @@ -307,8 +326,8 @@ void Hud::drawItems(v2s32 upperleftpos, v2s32 screen_offset, s32 itemcount, drawItem(mainlist->getItem(i), item_rect, (i + 1) == selectitem); - if (is_hotbar && g_touchscreengui) - g_touchscreengui->registerHotbarRect(i, item_rect); + if (is_hotbar && g_touchcontrols) + g_touchcontrols->registerHotbarRect(i, item_rect); } } @@ -353,13 +372,20 @@ void Hud::drawLuaElements(const v3s16 &camera_offset) std::vector elems; elems.reserve(player->maxHudId()); - // Add builtin minimap if the server doesn't send it. + // Add builtin elements if the server doesn't send them. + // Declared here such that they have the same lifetime as the elems vector HudElement minimap; + HudElement hotbar; if (client->getProtoVersion() < 44 && (player->hud_flags & HUD_FLAG_MINIMAP_VISIBLE)) { minimap = {HUD_ELEM_MINIMAP, v2f(1, 0), "", v2f(), "", 0 , 0, 0, v2f(-1, 1), v2f(-10, 10), v3f(), v2s32(256, 256), 0, "", 0}; elems.push_back(&minimap); } + if (client->getProtoVersion() < 46 && player->hud_flags & HUD_FLAG_HOTBAR_VISIBLE) { + hotbar = {HUD_ELEM_HOTBAR, v2f(0.5, 1), "", v2f(), "", 0 , 0, 0, v2f(0, -1), + v2f(0, -4), v3f(), v2s32(), 0, "", 0}; + elems.push_back(&hotbar); + } for (size_t i = 0; i != player->maxHudId(); i++) { HudElement *e = player->getHud(i); @@ -434,7 +460,7 @@ void Hud::drawLuaElements(const v3s16 &camera_offset) InventoryList *inv = inventory->getList(e->text); if (!inv) warningstream << "HUD: Unknown inventory list. name=" << e->text << std::endl; - drawItems(pos, v2s32(e->offset.X, e->offset.Y), e->number, 0, + drawItems(pos, v2s32(e->offset.X, e->offset.Y), e->number, e->align, 0, inv, e->item, e->dir, false); break; } case HUD_ELEM_WAYPOINT: { @@ -512,9 +538,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 @@ -544,14 +570,21 @@ void Hud::drawLuaElements(const v3s16 &camera_offset) } break; } case HUD_ELEM_MINIMAP: { - if (e->size.X <= 0 || e->size.Y <= 0) - break; if (!client->getMinimap()) break; // Draw a minimap of size "size" v2s32 dstsize(e->size.X * m_scale_factor, e->size.Y * m_scale_factor); - // (no percent size as minimap would likely be anamorphosed) + + // Only one percentage is supported to avoid distortion. + if (e->size.X < 0) + dstsize.X = dstsize.Y = m_screensize.X * (e->size.X * -0.01); + else if (e->size.Y < 0) + dstsize.X = dstsize.Y = m_screensize.Y * (e->size.Y * -0.01); + + if (dstsize.X <= 0 || dstsize.Y <= 0) + return; + v2s32 offset((e->align.X - 1.0) * dstsize.X / 2, (e->align.Y - 1.0) * dstsize.Y / 2); core::rect rect(0, 0, dstsize.X, dstsize.Y); @@ -559,6 +592,9 @@ void Hud::drawLuaElements(const v3s16 &camera_offset) e->offset.Y * m_scale_factor); client->getMinimap()->drawMinimap(rect); break; } + case HUD_ELEM_HOTBAR: { + drawHotbar(pos, e->offset, e->dir, e->align); + break; } default: infostream << "Hud::drawLuaElements: ignoring drawform " << e->type << " due to unrecognized type" << std::endl; @@ -622,10 +658,10 @@ void Hud::drawCompassRotate(HudElement *e, video::ITexture *texture, driver->setTransform(video::ETS_VIEW, core::matrix4()); driver->setTransform(video::ETS_WORLD, Matrix); - video::SMaterial &material = m_rotation_mesh_buffer.getMaterial(); + auto &material = m_rotation_mesh_buffer->getMaterial(); material.TextureLayers[0].Texture = texture; driver->setMaterial(material); - driver->drawMeshBuffer(&m_rotation_mesh_buffer); + driver->drawMeshBuffer(m_rotation_mesh_buffer.get()); driver->setTransform(video::ETS_WORLD, core::matrix4()); driver->setTransform(video::ETS_VIEW, oldViewMat); @@ -768,12 +804,10 @@ void Hud::drawStatbar(v2s32 pos, u16 corner, u16 drawdir, } } } - - -void Hud::drawHotbar(u16 playeritem) +void Hud::drawHotbar(const v2s32 &pos, const v2f &offset, u16 dir, const v2f &align) { - if (g_touchscreengui) - g_touchscreengui->resetHotbarRects(); + if (g_touchcontrols) + g_touchcontrols->resetHotbarRects(); InventoryList *mainlist = inventory->getList("main"); if (mainlist == NULL) { @@ -781,30 +815,24 @@ void Hud::drawHotbar(u16 playeritem) return; } - v2s32 centerlowerpos(m_displaycenter.X, m_screensize.Y); + u16 playeritem = player->getWieldIndex(); + v2s32 screen_offset(offset.X, offset.Y); s32 hotbar_itemcount = player->getMaxHotbarItemcount(); s32 width = hotbar_itemcount * (m_hotbar_imagesize + m_padding * 2); - v2s32 pos = centerlowerpos - v2s32(width / 2, m_hotbar_imagesize + m_padding * 3); const v2u32 &window_size = RenderingEngine::getWindowSize(); if ((float) width / (float) window_size.X <= g_settings->getFloat("hud_hotbar_max_width")) { - if (player->hud_flags & HUD_FLAG_HOTBAR_VISIBLE) { - drawItems(pos, v2s32(0, 0), hotbar_itemcount, 0, mainlist, playeritem + 1, 0, true); - } + drawItems(pos, screen_offset, hotbar_itemcount, align, 0, + mainlist, playeritem + 1, dir, true); } else { - pos.X += width/4; + v2s32 upper_pos = pos - v2s32(0, m_hotbar_imagesize + m_padding); - v2s32 secondpos = pos; - pos = pos - v2s32(0, m_hotbar_imagesize + m_padding); - - if (player->hud_flags & HUD_FLAG_HOTBAR_VISIBLE) { - drawItems(pos, v2s32(0, 0), hotbar_itemcount / 2, 0, - mainlist, playeritem + 1, 0, true); - drawItems(secondpos, v2s32(0, 0), hotbar_itemcount, - hotbar_itemcount / 2, mainlist, playeritem + 1, 0, true); - } + drawItems(upper_pos, screen_offset, hotbar_itemcount / 2, align, 0, + mainlist, playeritem + 1, dir, true); + drawItems(pos, screen_offset, hotbar_itemcount, align, + hotbar_itemcount / 2, mainlist, playeritem + 1, dir, true); } } @@ -933,33 +961,57 @@ void Hud::drawBlockBounds() } video::SMaterial old_material = driver->getMaterial2D(); - driver->setMaterial(m_selection_material); + driver->setMaterial(m_block_bounds_material); + + u16 mesh_chunk_size = std::max(1, g_settings->getU16("client_mesh_chunk")); v3s16 pos = player->getStandingNodePos(); - - v3s16 blockPos( + v3s16 block_pos( floorf((float) pos.X / MAP_BLOCKSIZE), floorf((float) pos.Y / MAP_BLOCKSIZE), floorf((float) pos.Z / MAP_BLOCKSIZE) ); - v3f offset = intToFloat(client->getCamera()->getOffset(), BS); + v3f cam_offset = intToFloat(client->getCamera()->getOffset(), BS); - s8 radius = m_block_bounds_mode == BLOCK_BOUNDS_NEAR ? 2 : 0; + v3f half_node = v3f(BS, BS, BS) / 2.0f; + v3f base_corner = intToFloat(block_pos * MAP_BLOCKSIZE, BS) - cam_offset - half_node; - v3f halfNode = v3f(BS, BS, BS) / 2.0f; + s16 radius = m_block_bounds_mode == BLOCK_BOUNDS_NEAR ? + rangelim(g_settings->getU16("show_block_bounds_radius_near"), 0, 1000) : 0; - for (s8 x = -radius; x <= radius; x++) - for (s8 y = -radius; y <= radius; y++) - for (s8 z = -radius; z <= radius; z++) { - v3s16 blockOffset(x, y, z); + for (s16 x = -radius; x <= radius + 1; x++) + for (s16 y = -radius; y <= radius + 1; y++) { + // Red for mesh chunk edges, yellow for other block edges. + auto choose_color = [&](s16 x_base, s16 y_base) { + // See also MeshGrid::isMeshPos(). + // If the block is mesh pos, it means it's at the (-,-,-) corner of + // the mesh. And we're drawing a (-,-) edge of this block. Hence, + // it is an edge of the mesh grid. + return (x + x_base) % mesh_chunk_size == 0 + && (y + y_base) % mesh_chunk_size == 0 ? + video::SColor(255, 255, 0, 0) : + video::SColor(255, 255, 255, 0); + }; - aabb3f box( - intToFloat((blockPos + blockOffset) * MAP_BLOCKSIZE, BS) - offset - halfNode, - intToFloat(((blockPos + blockOffset) * MAP_BLOCKSIZE) + (MAP_BLOCKSIZE - 1), BS) - offset + halfNode + v3f pmin = v3f(x, y, -radius) * MAP_BLOCKSIZE * BS; + v3f pmax = v3f(x, y, 1 + radius) * MAP_BLOCKSIZE * BS; + + driver->draw3DLine( + base_corner + v3f(pmin.X, pmin.Y, pmin.Z), + base_corner + v3f(pmax.X, pmax.Y, pmax.Z), + choose_color(block_pos.X, block_pos.Y) + ); + driver->draw3DLine( + base_corner + v3f(pmin.X, pmin.Z, pmin.Y), + base_corner + v3f(pmax.X, pmax.Z, pmax.Y), + choose_color(block_pos.X, block_pos.Z) + ); + driver->draw3DLine( + base_corner + v3f(pmin.Z, pmin.X, pmin.Y), + base_corner + v3f(pmax.Z, pmax.X, pmax.Y), + choose_color(block_pos.Y, block_pos.Z) ); - - driver->draw3DBox(box, video::SColor(255, 255, 0, 0)); } driver->setMaterial(old_material); @@ -1146,7 +1198,6 @@ void drawItemStack( video::SMaterial &material = buf->getMaterial(); material.MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL_REF; - material.Lighting = false; driver->setMaterial(material); driver->drawMeshBuffer(buf); } diff --git a/src/client/hud.h b/src/client/hud.h index b2b5dd09c..120215f45 100644 --- a/src/client/hud.h +++ b/src/client/hud.h @@ -22,6 +22,9 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include +#include +#include +#include "irr_ptr.h" #include "irr_aabb3d.h" #include "../hud.h" @@ -32,6 +35,17 @@ class InventoryList; class LocalPlayer; struct ItemStack; +namespace irr::scene +{ + class IMesh; +} + +namespace irr::video +{ + class ITexture; + class IVideoDriver; +} + class Hud { public: @@ -63,7 +77,7 @@ public: void disableBlockBounds(); void drawBlockBounds(); - void drawHotbar(u16 playeritem); + void drawHotbar(const v2s32 &pos, const v2f &offset, u16 direction, const v2f &align); void resizeHotbar(); void drawCrosshair(); void drawSelectionMesh(); @@ -99,7 +113,7 @@ private: const std::string &texture, const std::string& bgtexture, s32 count, s32 maxcount, v2s32 offset, v2s32 size = v2s32()); - void drawItems(v2s32 upperleftpos, v2s32 screen_offset, s32 itemcount, + void drawItems(v2s32 screen_pos, v2s32 screen_offset, s32 itemcount, v2f alignment, s32 inv_offset, InventoryList *mainlist, u16 selectitem, u16 direction, bool is_hotbar); @@ -137,8 +151,9 @@ private: v3f m_selected_face_normal; video::SMaterial m_selection_material; + video::SMaterial m_block_bounds_material; - scene::SMeshBuffer m_rotation_mesh_buffer; + irr_ptr m_rotation_mesh_buffer; enum { diff --git a/src/client/imagefilters.cpp b/src/client/imagefilters.cpp index db6523ad3..57e444151 100644 --- a/src/client/imagefilters.cpp +++ b/src/client/imagefilters.cpp @@ -22,6 +22,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include +#include // Simple 2D bitmap class with just the functionality needed here class Bitmap { diff --git a/src/client/imagefilters.h b/src/client/imagefilters.h index e6cbf2d29..f7e06ad09 100644 --- a/src/client/imagefilters.h +++ b/src/client/imagefilters.h @@ -18,7 +18,14 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once -#include "irrlichttypes_extrabloated.h" +#include "irrlichttypes.h" +#include + +namespace irr::video +{ + class IVideoDriver; + class IImage; +} /* Fill in RGB values for transparent pixels, to correct for odd colors * appearing at borders when blending. This is because many PNG optimizers diff --git a/src/client/imagesource.cpp b/src/client/imagesource.cpp index adc39f130..195d57a41 100644 --- a/src/client/imagesource.cpp +++ b/src/client/imagesource.cpp @@ -1447,6 +1447,8 @@ bool ImageSource::generateImagePart(std::string_view part_of_name, video::IImage *img = generateImage(filename, source_image_names); if (img) { + upscaleImagesToMatchLargest(baseimg, img); + apply_mask(img, baseimg, v2s32(0, 0), v2s32(0, 0), img->getDimension()); img->drop(); @@ -1908,7 +1910,8 @@ video::IImage* ImageSource::generateImage(std::string_view name, video::IImage *tmp = generateImage(name2, source_image_names); if (!tmp) { errorstream << "generateImage(): " - "Failed to generate \"" << name2 << "\"" + "Failed to generate \"" << name2 << "\"\n" + "part of texture \"" << name << "\"" << std::endl; return NULL; } @@ -1923,7 +1926,8 @@ video::IImage* ImageSource::generateImage(std::string_view name, } else if (!generateImagePart(last_part_of_name, baseimg, source_image_names)) { // Generate image according to part of name errorstream << "generateImage(): " - "Failed to generate \"" << last_part_of_name << "\"" + "Failed to generate \"" << last_part_of_name << "\"\n" + "part of texture \"" << name << "\"" << std::endl; } diff --git a/src/client/inputhandler.cpp b/src/client/inputhandler.cpp index 4233916c2..2ce058ff4 100644 --- a/src/client/inputhandler.cpp +++ b/src/client/inputhandler.cpp @@ -22,8 +22,10 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "util/numeric.h" #include "inputhandler.h" #include "gui/mainmenumanager.h" -#include "gui/touchscreengui.h" +#include "gui/touchcontrols.h" #include "hud.h" +#include "log_internal.h" +#include "client/renderingengine.h" void KeyCache::populate_nonchanging() { @@ -141,10 +143,15 @@ 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_touchscreengui) - g_touchscreengui->setVisible(false); + if (g_touchcontrols) + g_touchcontrols->setVisible(false); return g_menumgr.preprocessEvent(event); } @@ -168,9 +175,9 @@ bool MyEventReceiver::OnEvent(const SEvent &event) return true; } - } else if (g_touchscreengui && event.EventType == irr::EET_TOUCH_INPUT_EVENT) { - // In case of touchscreengui, we have to handle different events - g_touchscreengui->translateEvent(event); + } else if (g_touchcontrols && event.EventType == irr::EET_TOUCH_INPUT_EVENT) { + // In case of touchcontrols, we have to handle different events + g_touchcontrols->translateEvent(event); return true; } else if (event.EventType == irr::EET_JOYSTICK_INPUT_EVENT) { // joystick may be nullptr if game is launched with '--random-input' parameter @@ -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_touchscreengui && g_touchscreengui->getMovementSpeed()) - return g_touchscreengui->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_touchscreengui && g_touchscreengui->getMovementSpeed()) - return g_touchscreengui->getMovementDirection(); + // `getJoystickDirection() == 0` means forward, so we cannot use + // `getJoystickDirection()` as a condition. + if (g_touchcontrols && g_touchcontrols->getJoystickSpeed()) + return g_touchcontrols->getJoystickDirection(); return joystick.getMovementDirection(); } +v2s32 RealInputHandler::getMousePos() +{ + auto control = RenderingEngine::get_raw_device()->getCursorControl(); + if (control) { + return control->getPosition(); + } + + return m_mousepos; +} + +void RealInputHandler::setMousePos(s32 x, s32 y) +{ + auto control = RenderingEngine::get_raw_device()->getCursorControl(); + if (control) { + control->setPosition(x, y); + } else { + m_mousepos = v2s32(x, y); + } +} + /* * RandomInputHandler */ @@ -320,25 +318,11 @@ void RandomInputHandler::step(float dtime) counterMovement -= dtime; if (counterMovement < 0.0) { counterMovement = 0.1 * Rand(1, 40); - movementSpeed = Rand(0,100)*0.01; - movementDirection = Rand(-100, 100)*0.01 * M_PI; + joystickSpeed = Rand(0,100)*0.01; + joystickDirection = Rand(-100, 100)*0.01 * M_PI; } } else { - bool f = keydown[keycache.key[KeyType::FORWARD]], - l = keydown[keycache.key[KeyType::LEFT]]; - if (f || l) { - movementSpeed = 1.0f; - if (f && !l) - movementDirection = 0.0; - else if (!f && l) - movementDirection = -M_PI_2; - else if (f && l) - movementDirection = -M_PI_4; - else - movementDirection = 0.0; - } else { - movementSpeed = 0.0; - movementDirection = 0.0; - } + joystickSpeed = 0.0f; + joystickDirection = 0.0f; } } diff --git a/src/client/inputhandler.h b/src/client/inputhandler.h index 400503a3d..ee76151f4 100644 --- a/src/client/inputhandler.h +++ b/src/client/inputhandler.h @@ -19,14 +19,18 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once -#include "irrlichttypes_extrabloated.h" +#include "irrlichttypes.h" #include "joystick_controller.h" #include #include "keycode.h" -#include "renderingengine.h" class InputHandler; +enum class PointerType { + Mouse, + Touch, +}; + /**************************************************************************** Fast key cache for main game loop ****************************************************************************/ @@ -199,6 +203,8 @@ public: JoystickController *joystick = nullptr; + PointerType getLastPointerType() { return last_pointer_type; } + private: s32 mouse_wheel = 0; @@ -223,6 +229,8 @@ private: // Intentionally not reset by clearInput/releaseAllKeys. bool fullscreen_is_down = false; + + PointerType last_pointer_type = PointerType::Mouse; }; class InputHandler @@ -247,8 +255,8 @@ public: virtual bool wasKeyReleased(GameKeyType k) = 0; virtual bool cancelPressed() = 0; - virtual float getMovementSpeed() = 0; - virtual float getMovementDirection() = 0; + virtual float getJoystickSpeed() = 0; + virtual float getJoystickDirection() = 0; virtual void clearWasKeyPressed() {} virtual void clearWasKeyReleased() {} @@ -269,11 +277,12 @@ public: JoystickController joystick; KeyCache keycache; }; + /* - Separated input handler + Separated input handler implementations */ -class RealInputHandler : public InputHandler +class RealInputHandler final : public InputHandler { public: RealInputHandler(MyEventReceiver *receiver) : m_receiver(receiver) @@ -303,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() { @@ -330,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() { @@ -372,7 +364,7 @@ private: v2s32 m_mousepos; }; -class RandomInputHandler : public InputHandler +class RandomInputHandler final : public InputHandler { public: RandomInputHandler() = default; @@ -387,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); } @@ -402,6 +394,6 @@ private: KeyList keydown; v2s32 mousepos; v2s32 mousespeed; - float movementSpeed; - float movementDirection; + float joystickSpeed; + float joystickDirection; }; diff --git a/src/client/joystick_controller.h b/src/client/joystick_controller.h index 98486df43..d74cc3db6 100644 --- a/src/client/joystick_controller.h +++ b/src/client/joystick_controller.h @@ -155,6 +155,11 @@ public: float getMovementDirection(); float getMovementSpeed(); + u8 getJoystickId() const + { + return m_joystick_id; + } + f32 doubling_dtime; private: diff --git a/src/client/localplayer.cpp b/src/client/localplayer.cpp index aa335e90e..75f755578 100644 --- a/src/client/localplayer.cpp +++ b/src/client/localplayer.cpp @@ -360,7 +360,7 @@ void LocalPlayer::move(f32 dtime, Environment *env, f32 pos_max_d, collisionMoveResult result = collisionMoveSimple(env, m_client, pos_max_d, m_collisionbox, player_stepheight, dtime, - &position, &m_speed, accel_f); + &position, &m_speed, accel_f, m_cao); bool could_sneak = control.sneak && !free_move && !in_liquid && !is_climbing && physics_override.sneak; @@ -444,7 +444,7 @@ void LocalPlayer::move(f32 dtime, Environment *env, f32 pos_max_d, v3f check_pos = position; check_pos.Y += y_diff * dtime * 22.0f + BS * 0.01f; if (y_diff < BS * 0.6f || (physics_override.sneak_glitch - && !collision_check_intersection(env, m_client, m_collisionbox, check_pos))) { + && !collision_check_intersection(env, m_client, m_collisionbox, check_pos, m_cao))) { // Smoothen the movement (based on 'position.Y = bmax.Y') position.Y = std::min(check_pos.Y, bmax.Y); m_speed.Y = 0.0f; @@ -990,7 +990,7 @@ void LocalPlayer::old_move(f32 dtime, Environment *env, f32 pos_max_d, collisionMoveResult result = collisionMoveSimple(env, m_client, pos_max_d, m_collisionbox, player_stepheight, dtime, - &position, &m_speed, accel_f); + &position, &m_speed, accel_f, m_cao); // Position was slightly changed; update standing node pos if (touching_ground) @@ -1254,7 +1254,7 @@ void LocalPlayer::handleAutojump(f32 dtime, Environment *env, // try at peak of jump, zero step height collisionMoveResult jump_result = collisionMoveSimple(env, m_client, pos_max_d, - m_collisionbox, 0.0f, dtime, &jump_pos, &jump_speed, v3f(0.0f)); + m_collisionbox, 0.0f, dtime, &jump_pos, &jump_speed, v3f(0.0f), m_cao); // see if we can get a little bit farther horizontally if we had // jumped diff --git a/src/client/localplayer.h b/src/client/localplayer.h index 815fafa8b..275f556e4 100644 --- a/src/client/localplayer.h +++ b/src/client/localplayer.h @@ -105,6 +105,8 @@ public: u8 last_camera_fov = 0; u8 last_wanted_range = 0; bool last_camera_inverted = false; + f32 last_movement_speed = 0.0f; + f32 last_movement_dir = 0.0f; float camera_impact = 0.0f; diff --git a/src/client/mapblock_mesh.cpp b/src/client/mapblock_mesh.cpp index 429464e04..071b03132 100644 --- a/src/client/mapblock_mesh.cpp +++ b/src/client/mapblock_mesh.cpp @@ -28,6 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "minimap.h" #include "content_mapblock.h" #include "util/directiontables.h" +#include "util/tracy_wrapper.h" #include "client/meshgen/collector.h" #include "client/renderingengine.h" #include @@ -592,17 +593,11 @@ void MapBlockBspTree::traverse(s32 node, v3f viewpoint, std::vector &output PartialMeshBuffer */ -void PartialMeshBuffer::beforeDraw() const +void PartialMeshBuffer::draw(video::IVideoDriver *driver) const { - // Patch the indexes in the mesh buffer before draw - m_buffer->Indices = std::move(m_vertex_indexes); - m_buffer->setDirty(scene::EBT_INDEX); -} - -void PartialMeshBuffer::afterDraw() const -{ - // Take the data back - m_vertex_indexes = m_buffer->Indices.steal(); + const auto pType = m_buffer->getPrimitiveType(); + driver->drawBuffers(m_buffer->getVertexBuffer(), m_indices.get(), + m_indices->getPrimitiveCount(pType), pType); } /* @@ -617,8 +612,10 @@ MapBlockMesh::MapBlockMesh(Client *client, MeshMakeData *data, v3s16 camera_offs m_last_crack(-1), m_last_daynight_ratio((u32) -1) { + ZoneScoped; + for (auto &m : m_mesh) - m = new scene::SMesh(); + m = make_irr(); m_enable_shaders = data->m_use_shaders; auto mesh_grid = client->getMeshGrid(); @@ -666,7 +663,7 @@ MapBlockMesh::MapBlockMesh(Client *client, MeshMakeData *data, v3s16 camera_offs m_bounding_radius = std::sqrt(collector.m_bounding_radius_sq); for (int layer = 0; layer < MAX_TILE_LAYERS; layer++) { - scene::SMesh *mesh = (scene::SMesh *)m_mesh[layer]; + scene::SMesh *mesh = static_cast(m_mesh[layer].get()); for(u32 i = 0; i < collector.prebuffers[layer].size(); i++) { @@ -739,7 +736,6 @@ MapBlockMesh::MapBlockMesh(Client *client, MeshMakeData *data, v3s16 camera_offs // Create material video::SMaterial material; - material.Lighting = false; material.BackfaceCulling = true; material.FogEnable = true; material.setTexture(0, p.layer.texture); @@ -752,9 +748,6 @@ MapBlockMesh::MapBlockMesh(Client *client, MeshMakeData *data, v3s16 camera_offs material.MaterialType = m_shdrsrc->getShaderInfo( p.layer.shader_id).material; p.layer.applyMaterialOptionsWithShaders(material); - if (p.layer.normal_texture) - material.setTexture(1, p.layer.normal_texture); - material.setTexture(2, p.layer.flags_texture); } else { p.layer.applyMaterialOptions(material); } @@ -783,12 +776,11 @@ MapBlockMesh::MapBlockMesh(Client *client, MeshMakeData *data, v3s16 camera_offs } if (mesh) { - // Use VBO for mesh (this just would set this for ever buffer) + // Use VBO for mesh (this just would set this for every buffer) mesh->setHardwareMappingHint(scene::EHM_STATIC); } } - //std::cout<<"added "<side_length); // Check if animation is required for this mesh @@ -801,10 +793,10 @@ MapBlockMesh::MapBlockMesh(Client *client, MeshMakeData *data, v3s16 camera_offs MapBlockMesh::~MapBlockMesh() { size_t sz = 0; - for (scene::IMesh *m : m_mesh) { + for (auto &&m : m_mesh) { for (u32 i = 0; i < m->getMeshBufferCount(); i++) sz += m->getMeshBuffer(i)->getSize(); - m->drop(); + m.reset(); } for (MinimapMapblock *block : m_minimap_mapblocks) delete block; @@ -865,11 +857,6 @@ bool MapBlockMesh::animate(bool faraway, float time, int crack, const FrameSpec &frame = (*tile.frames)[frameno]; buf->getMaterial().setTexture(0, frame.texture); - if (m_enable_shaders) { - if (frame.normal_texture) - buf->getMaterial().setTexture(1, frame.normal_texture); - buf->getMaterial().setTexture(2, frame.flags_texture); - } } // Day-night transition @@ -878,7 +865,7 @@ bool MapBlockMesh::animate(bool faraway, float time, int crack, get_sunlight_color(&day_color, daynight_ratio); for (auto &daynight_diff : m_daynight_diffs) { - auto *mesh = m_mesh[daynight_diff.first.first]; + auto *mesh = m_mesh[daynight_diff.first.first].get(); mesh->setDirty(scene::EBT_VERTEX); // force reload to VBO scene::IMeshBuffer *buf = mesh-> getMeshBuffer(daynight_diff.first.second); @@ -906,6 +893,7 @@ void MapBlockMesh::updateTransparentBuffers(v3f camera_pos, v3s16 block_pos) m_bsp_tree.traverse(rel_camera_pos, triangle_refs); // arrange index sequences into partial buffers + m_transparent_buffers_consolidated = false; m_transparent_buffers.clear(); scene::SMeshBuffer *current_buffer = nullptr; @@ -930,6 +918,8 @@ void MapBlockMesh::updateTransparentBuffers(v3f camera_pos, v3s16 block_pos) void MapBlockMesh::consolidateTransparentBuffers() { + if (m_transparent_buffers_consolidated) + return; m_transparent_buffers.clear(); scene::SMeshBuffer *current_buffer = nullptr; @@ -952,6 +942,8 @@ void MapBlockMesh::consolidateTransparentBuffers() if (!current_strain.empty()) { this->m_transparent_buffers.emplace_back(current_buffer, std::move(current_strain)); } + + m_transparent_buffers_consolidated = true; } video::SColor encode_light(u16 light, u8 emissive_light) diff --git a/src/client/mapblock_mesh.h b/src/client/mapblock_mesh.h index 7cd368762..c52df5ed3 100644 --- a/src/client/mapblock_mesh.h +++ b/src/client/mapblock_mesh.h @@ -20,6 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once #include "irrlichttypes_extrabloated.h" +#include "irr_ptr.h" #include "util/numeric.h" #include "client/tile.h" #include "voxel.h" @@ -144,26 +145,24 @@ private: * * Attach alternate `Indices` to an existing mesh buffer, to make it possible to use different * indices with the same vertex buffer. - * - * Irrlicht does not currently support this: `CMeshBuffer` ties together a single vertex buffer - * and a single index buffer. There's no way to share these between mesh buffers. - * */ class PartialMeshBuffer { public: - PartialMeshBuffer(scene::SMeshBuffer *buffer, std::vector &&vertex_indexes) : - m_buffer(buffer), m_vertex_indexes(std::move(vertex_indexes)) - {} + PartialMeshBuffer(scene::SMeshBuffer *buffer, std::vector &&vertex_indices) : + m_buffer(buffer), m_indices(make_irr()) + { + m_indices->Data = std::move(vertex_indices); + m_indices->setHardwareMappingHint(scene::EHM_STATIC); + } - scene::IMeshBuffer *getBuffer() const { return m_buffer; } - const std::vector &getVertexIndexes() const { return m_vertex_indexes; } + auto *getBuffer() const { return m_buffer; } + + void draw(video::IVideoDriver *driver) const; - void beforeDraw() const; - void afterDraw() const; private: scene::SMeshBuffer *m_buffer; - mutable std::vector m_vertex_indexes; + irr_ptr m_indices; }; /* @@ -194,12 +193,12 @@ public: scene::IMesh *getMesh() { - return m_mesh[0]; + return m_mesh[0].get(); } scene::IMesh *getMesh(u8 layer) { - return m_mesh[layer]; + return m_mesh[layer].get(); } std::vector moveMinimapMapblocks() @@ -243,7 +242,7 @@ private: TileLayer tile; }; - scene::IMesh *m_mesh[MAX_TILE_LAYERS]; + irr_ptr m_mesh[MAX_TILE_LAYERS]; std::vector m_minimap_mapblocks; ITextureSource *m_tsrc; IShaderSource *m_shdrsrc; @@ -282,6 +281,8 @@ private: MapBlockBspTree m_bsp_tree; // Ordered list of references to parts of transparent buffers to draw std::vector m_transparent_buffers; + // Is m_transparent_buffers currently in consolidated form? + bool m_transparent_buffers_consolidated = false; }; /*! diff --git a/src/client/mesh.cpp b/src/client/mesh.cpp index 711f7e1c6..9e66404e3 100644 --- a/src/client/mesh.cpp +++ b/src/client/mesh.cpp @@ -18,6 +18,7 @@ with this program; if not, write to the Free Software Foundation, Inc., */ #include "mesh.h" +#include "S3DVertex.h" #include "debug.h" #include "log.h" #include @@ -33,7 +34,7 @@ inline static void applyShadeFactor(video::SColor& color, float factor) color.setBlue(core::clamp(core::round32(color.getBlue()*factor), 0, 255)); } -void applyFacesShading(video::SColor &color, const v3f &normal) +void applyFacesShading(video::SColor &color, const v3f normal) { /* Some drawtypes have normals set to (0, 0, 0), this must result in @@ -98,7 +99,6 @@ scene::IAnimatedMesh* createCubeMesh(v3f scale) scene::IMeshBuffer *buf = new scene::SMeshBuffer(); buf->append(vertices + 4 * i, 4, indices, 6); // Set default material - buf->getMaterial().Lighting = false; buf->getMaterial().MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL_REF; buf->getMaterial().forEachTexture([] (auto &tex) { tex.MinFilter = video::ETMINF_NEAREST_MIPMAP_NEAREST; @@ -132,6 +132,7 @@ void scaleMesh(scene::IMesh *mesh, v3f scale) for (u32 i = 0; i < vertex_count; i++) ((video::S3DVertex *)(vertices + i * stride))->Pos *= scale; + buf->setDirty(scene::EBT_VERTEX); buf->recalculateBoundingBox(); // calculate total bounding box @@ -160,6 +161,7 @@ void translateMesh(scene::IMesh *mesh, v3f vec) for (u32 i = 0; i < vertex_count; i++) ((video::S3DVertex *)(vertices + i * stride))->Pos += vec; + buf->setDirty(scene::EBT_VERTEX); buf->recalculateBoundingBox(); // calculate total bounding box @@ -171,23 +173,17 @@ void translateMesh(scene::IMesh *mesh, v3f vec) mesh->setBoundingBox(bbox); } -void setMeshBufferColor(scene::IMeshBuffer *buf, const video::SColor &color) +void setMeshBufferColor(scene::IMeshBuffer *buf, const video::SColor color) { const u32 stride = getVertexPitchFromType(buf->getVertexType()); u32 vertex_count = buf->getVertexCount(); u8 *vertices = (u8 *) buf->getVertices(); for (u32 i = 0; i < vertex_count; i++) ((video::S3DVertex *) (vertices + i * stride))->Color = color; + buf->setDirty(scene::EBT_VERTEX); } -void setAnimatedMeshColor(scene::IAnimatedMeshSceneNode *node, const video::SColor &color) -{ - for (u32 i = 0; i < node->getMaterialCount(); ++i) { - node->getMaterial(i).EmissiveColor = color; - } -} - -void setMeshColor(scene::IMesh *mesh, const video::SColor &color) +void setMeshColor(scene::IMesh *mesh, const video::SColor color) { if (mesh == NULL) return; @@ -197,15 +193,6 @@ void setMeshColor(scene::IMesh *mesh, const video::SColor &color) setMeshBufferColor(mesh->getMeshBuffer(j), color); } -void setMeshBufferTextureCoords(scene::IMeshBuffer *buf, const v2f *uv, u32 count) -{ - const u32 stride = getVertexPitchFromType(buf->getVertexType()); - assert(buf->getVertexCount() >= count); - u8 *vertices = (u8 *) buf->getVertices(); - for (u32 i = 0; i < count; i++) - ((video::S3DVertex*) (vertices + i * stride))->TCoords = uv[i]; -} - template static void applyToMesh(scene::IMesh *mesh, const F &fn) { @@ -217,6 +204,7 @@ static void applyToMesh(scene::IMesh *mesh, const F &fn) char *vertices = reinterpret_cast(buf->getVertices()); for (u32 i = 0; i < vertex_count; i++) fn(reinterpret_cast(vertices + i * stride)); + buf->setDirty(scene::EBT_VERTEX); } } @@ -233,6 +221,7 @@ void colorizeMeshBuffer(scene::IMeshBuffer *buf, const video::SColor *buffercolo // Apply shading applyFacesShading(*vc, vertex->Normal); } + buf->setDirty(scene::EBT_VERTEX); } void setMeshColorByNormalXYZ(scene::IMesh *mesh, @@ -397,8 +386,8 @@ scene::SMesh* cloneMesh(scene::IMesh *src_mesh) scene::IMeshBuffer *temp_buf = cloneMeshBuffer( src_mesh->getMeshBuffer(j)); dst_mesh->addMeshBuffer(temp_buf); + dst_mesh->setTextureSlot(j, src_mesh->getTextureSlot(j)); temp_buf->drop(); - } return dst_mesh; } @@ -411,7 +400,6 @@ scene::IMesh* convertNodeboxesToMesh(const std::vector &boxes, for (u16 j = 0; j < 6; j++) { scene::IMeshBuffer *buf = new scene::SMeshBuffer(); - buf->getMaterial().Lighting = false; buf->getMaterial().forEachTexture([] (auto &tex) { tex.MinFilter = video::ETMINF_NEAREST_MIPMAP_NEAREST; tex.MagFilter = video::ETMAGF_NEAREST; diff --git a/src/client/mesh.h b/src/client/mesh.h index 0c3e8942e..c04cfb0af 100644 --- a/src/client/mesh.h +++ b/src/client/mesh.h @@ -27,7 +27,7 @@ with this program; if not, write to the Free Software Foundation, Inc., * Applies shading to a color based on the surface's * normal vector. */ -void applyFacesShading(video::SColor &color, const v3f &normal); +void applyFacesShading(video::SColor &color, const v3f normal); /* Create a new cube mesh. @@ -52,24 +52,12 @@ void translateMesh(scene::IMesh *mesh, v3f vec); /*! * Sets a constant color for all vertices in the mesh buffer. */ -void setMeshBufferColor(scene::IMeshBuffer *buf, const video::SColor &color); +void setMeshBufferColor(scene::IMeshBuffer *buf, const video::SColor color); /* Set a constant color for all vertices in the mesh */ -void setMeshColor(scene::IMesh *mesh, const video::SColor &color); - - -/* - Sets texture coords for vertices in the mesh buffer. - `uv[]` must have `count` elements -*/ -void setMeshBufferTextureCoords(scene::IMeshBuffer *buf, const v2f *uv, u32 count); - -/* - Set a constant color for an animated mesh -*/ -void setAnimatedMeshColor(scene::IAnimatedMeshSceneNode *node, const video::SColor &color); +void setMeshColor(scene::IMesh *mesh, const video::SColor color); /*! * Overwrites the color of a mesh buffer. @@ -139,5 +127,5 @@ bool checkMeshNormals(scene::IMesh *mesh); Set the MinFilter, MagFilter and AnisotropicFilter properties of a texture layer according to the three relevant boolean values found in the Minetest settings. -*/ +*/ void setMaterialFilters(video::SMaterialLayer &tex, bool bilinear, bool trilinear, bool anisotropic); diff --git a/src/client/minimap.cpp b/src/client/minimap.cpp index afac89843..13dca5fe9 100644 --- a/src/client/minimap.cpp +++ b/src/client/minimap.cpp @@ -201,7 +201,7 @@ Minimap::Minimap(Client *client) addMode(MINIMAP_TYPE_RADAR, 128); // Initialize minimap data - data = new MinimapData; + data = std::make_unique(); data->map_invalidated = true; data->minimap_shape_round = g_settings->getBool("minimap_shape_round"); @@ -209,11 +209,11 @@ Minimap::Minimap(Client *client) setModeIndex(0); // Create mesh buffer for minimap - m_meshbuffer = getMinimapMeshBuffer(); + m_meshbuffer = createMinimapMeshBuffer(); // Initialize and start thread - m_minimap_update_thread = new MinimapUpdateThread(); - m_minimap_update_thread->data = data; + m_minimap_update_thread = std::make_unique(); + m_minimap_update_thread->data = data.get(); m_minimap_update_thread->start(); } @@ -222,7 +222,7 @@ Minimap::~Minimap() m_minimap_update_thread->stop(); m_minimap_update_thread->wait(); - m_meshbuffer->drop(); + m_meshbuffer.reset(); if (data->minimap_mask_round) data->minimap_mask_round->drop(); @@ -232,12 +232,10 @@ Minimap::~Minimap() driver->removeTexture(data->texture); driver->removeTexture(data->heightmap_texture); - for (MinimapMarker *m : m_markers) - delete m; m_markers.clear(); - delete data; - delete m_minimap_update_thread; + data.reset(); + m_minimap_update_thread.reset(); } void Minimap::addBlock(v3s16 pos, MinimapMapblock *data) @@ -552,24 +550,26 @@ v3f Minimap::getYawVec() return v3f(1.0, 0.0, 1.0); } -scene::SMeshBuffer *Minimap::getMinimapMeshBuffer() +irr_ptr Minimap::createMinimapMeshBuffer() { - scene::SMeshBuffer *buf = new scene::SMeshBuffer(); - buf->Vertices.set_used(4); - buf->Indices.set_used(6); + auto buf = make_irr(); + auto &vertices = buf->Vertices->Data; + auto &indices = buf->Indices->Data; + vertices.resize(4); + indices.resize(6); static const video::SColor c(255, 255, 255, 255); - buf->Vertices[0] = video::S3DVertex(-1, -1, 0, 0, 0, 1, c, 0, 1); - buf->Vertices[1] = video::S3DVertex(-1, 1, 0, 0, 0, 1, c, 0, 0); - buf->Vertices[2] = video::S3DVertex( 1, 1, 0, 0, 0, 1, c, 1, 0); - buf->Vertices[3] = video::S3DVertex( 1, -1, 0, 0, 0, 1, c, 1, 1); + vertices[0] = video::S3DVertex(-1, -1, 0, 0, 0, 1, c, 0, 1); + vertices[1] = video::S3DVertex(-1, 1, 0, 0, 0, 1, c, 0, 0); + vertices[2] = video::S3DVertex( 1, 1, 0, 0, 0, 1, c, 1, 0); + vertices[3] = video::S3DVertex( 1, -1, 0, 0, 0, 1, c, 1, 1); - buf->Indices[0] = 0; - buf->Indices[1] = 1; - buf->Indices[2] = 2; - buf->Indices[3] = 2; - buf->Indices[4] = 3; - buf->Indices[5] = 0; + indices[0] = 0; + indices[1] = 1; + indices[2] = 2; + indices[3] = 2; + indices[4] = 3; + indices[5] = 0; buf->setHardwareMappingHint(scene::EHM_STATIC); return buf; @@ -610,7 +610,6 @@ void Minimap::drawMinimap(core::rect rect) tex.MinFilter = video::ETMINF_LINEAR_MIPMAP_LINEAR; tex.MagFilter = video::ETMAGF_LINEAR; }); - material.Lighting = false; material.TextureLayers[0].Texture = minimap_texture; material.TextureLayers[1].Texture = data->heightmap_texture; @@ -627,7 +626,7 @@ void Minimap::drawMinimap(core::rect rect) // Draw minimap driver->setTransform(video::ETS_WORLD, matrix); driver->setMaterial(material); - driver->drawMeshBuffer(m_meshbuffer); + driver->drawMeshBuffer(m_meshbuffer.get()); // Draw overlay video::ITexture *minimap_overlay = data->minimap_shape_round ? @@ -635,7 +634,7 @@ void Minimap::drawMinimap(core::rect rect) material.TextureLayers[0].Texture = minimap_overlay; material.MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL; driver->setMaterial(material); - driver->drawMeshBuffer(m_meshbuffer); + driver->drawMeshBuffer(m_meshbuffer.get()); // Draw player marker on minimap if (data->minimap_shape_round) { @@ -647,7 +646,7 @@ void Minimap::drawMinimap(core::rect rect) material.TextureLayers[0].Texture = data->player_marker; driver->setTransform(video::ETS_WORLD, matrix); driver->setMaterial(material); - driver->drawMeshBuffer(m_meshbuffer); + driver->drawMeshBuffer(m_meshbuffer.get()); // Reset transformations driver->setTransform(video::ETS_VIEW, oldViewMat); @@ -685,17 +684,17 @@ void Minimap::drawMinimap(core::rect rect) } } -MinimapMarker* Minimap::addMarker(scene::ISceneNode *parent_node) +MinimapMarker *Minimap::addMarker(scene::ISceneNode *parent_node) { - MinimapMarker *m = new MinimapMarker(parent_node); - m_markers.push_back(m); - return m; + auto m = std::make_unique(parent_node); + auto ret = m.get(); + m_markers.push_back(std::move(m)); + return ret; } void Minimap::removeMarker(MinimapMarker **m) { - m_markers.remove(*m); - delete *m; + m_markers.remove_if([ptr = *m](const auto &up) { return up.get() == ptr; }); *m = nullptr; } @@ -709,7 +708,7 @@ void Minimap::updateActiveMarkers() data->mode.scan_height / 2, data->mode.map_size / 2); - for (MinimapMarker *marker : m_markers) { + for (auto &&marker : m_markers) { v3s16 pos = floatToInt(marker->parent_node->getAbsolutePosition() + cam_offset, BS) - pos_offset; if (pos.X < 0 || pos.X > data->mode.map_size || diff --git a/src/client/minimap.h b/src/client/minimap.h index f819deaf4..0c419fa63 100644 --- a/src/client/minimap.h +++ b/src/client/minimap.h @@ -21,6 +21,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "../hud.h" #include "irrlichttypes_extrabloated.h" +#include "irr_ptr.h" #include "util/thread.h" #include "voxel.h" #include @@ -148,7 +149,7 @@ public: void blitMinimapPixelsToImageSurface(video::IImage *map_image, video::IImage *heightmap_image); - scene::SMeshBuffer *getMinimapMeshBuffer(); + irr_ptr createMinimapMeshBuffer(); MinimapMarker* addMarker(scene::ISceneNode *parent_node); void removeMarker(MinimapMarker **marker); @@ -158,20 +159,20 @@ public: video::IVideoDriver *driver; Client* client; - MinimapData *data; + std::unique_ptr data; private: ITextureSource *m_tsrc; IShaderSource *m_shdrsrc; const NodeDefManager *m_ndef; - MinimapUpdateThread *m_minimap_update_thread = nullptr; - scene::SMeshBuffer *m_meshbuffer; + std::unique_ptr m_minimap_update_thread; + irr_ptr m_meshbuffer; bool m_enable_shaders; std::vector m_modes; size_t m_current_mode_index; u16 m_surface_mode_scan_height; f32 m_angle; std::mutex m_mutex; - std::list m_markers; + std::list> m_markers; std::list m_active_markers; }; diff --git a/src/client/particles.cpp b/src/client/particles.cpp index c19282424..3a2dace12 100644 --- a/src/client/particles.cpp +++ b/src/client/particles.cpp @@ -357,16 +357,18 @@ void ParticleSpawner::spawnParticle(ClientEnvironment *env, float radius, if (attached_absolute_pos_rot_matrix) { // Apply attachment rotation - attached_absolute_pos_rot_matrix->rotateVect(pp.vel); - attached_absolute_pos_rot_matrix->rotateVect(pp.acc); + pp.vel = attached_absolute_pos_rot_matrix->rotateAndScaleVect(pp.vel); + pp.acc = attached_absolute_pos_rot_matrix->rotateAndScaleVect(pp.acc); } if (attractor_obj) attractor_origin += attractor_obj->getPosition() / BS; if (attractor_direction_obj) { auto *attractor_absolute_pos_rot_matrix = attractor_direction_obj->getAbsolutePosRotMatrix(); - if (attractor_absolute_pos_rot_matrix) - attractor_absolute_pos_rot_matrix->rotateVect(attractor_direction); + if (attractor_absolute_pos_rot_matrix) { + attractor_direction = attractor_absolute_pos_rot_matrix + ->rotateAndScaleVect(attractor_direction); + } } pp.expirationtime = r_exp.pickWithin(); @@ -989,7 +991,6 @@ video::SMaterial ParticleManager::getMaterialForParticle(const ClientParticleTex video::SMaterial material; // Texture - material.Lighting = false; material.BackfaceCulling = false; material.FogEnable = true; material.forEachTexture([] (auto &tex) { diff --git a/src/client/render/anaglyph.cpp b/src/client/render/anaglyph.cpp index 4cb42db50..b1117998f 100644 --- a/src/client/render/anaglyph.cpp +++ b/src/client/render/anaglyph.cpp @@ -20,6 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "anaglyph.h" #include "client/camera.h" +#include /// SetColorMaskStep step diff --git a/src/client/render/core.h b/src/client/render/core.h index c5617bcb2..fb60b274e 100644 --- a/src/client/render/core.h +++ b/src/client/render/core.h @@ -21,6 +21,11 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once #include "irrlichttypes_extrabloated.h" +namespace irr +{ + class IrrlichtDevice; +} + class ShadowRenderer; class Camera; class Client; diff --git a/src/client/render/pipeline.h b/src/client/render/pipeline.h index abb108652..d0e3b2c38 100644 --- a/src/client/render/pipeline.h +++ b/src/client/render/pipeline.h @@ -19,6 +19,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once #include "irrlichttypes_extrabloated.h" +#include // used in all render/*.cpp +#include // used in all render/*.cpp #include #include @@ -31,6 +33,11 @@ class Client; class Hud; class ShadowRenderer; +namespace irr::video +{ + class IRenderTarget; +} + struct PipelineContext { PipelineContext(IrrlichtDevice *_device, Client *_client, Hud *_hud, ShadowRenderer *_shadow_renderer, video::SColor _color, v2u32 _target_size) diff --git a/src/client/render/plain.cpp b/src/client/render/plain.cpp index 60a732415..0ca2476ee 100644 --- a/src/client/render/plain.cpp +++ b/src/client/render/plain.cpp @@ -61,7 +61,6 @@ void DrawHUD::run(PipelineContext &context) if (context.draw_crosshair) context.hud->drawCrosshair(); - context.hud->drawHotbar(context.client->getEnv().getLocalPlayer()->getWieldIndex()); context.hud->drawLuaElements(context.client->getCamera()->getOffset()); context.client->getCamera()->drawNametags(); } diff --git a/src/client/renderingengine.cpp b/src/client/renderingengine.cpp index 18b9ff158..f0d2abddb 100644 --- a/src/client/renderingengine.cpp +++ b/src/client/renderingengine.cpp @@ -36,13 +36,11 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "inputhandler.h" #include "gettext.h" #include "filesys.h" -#include "../gui/guiSkin.h" #include "irrlicht_changes/static_text.h" #include "irr_ptr.h" RenderingEngine *RenderingEngine::s_singleton = nullptr; const video::SColor RenderingEngine::MENU_SKY_COLOR = video::SColor(255, 140, 186, 250); -const float RenderingEngine::BASE_BLOOM_STRENGTH = 1.0f; /* Helper classes */ @@ -126,27 +124,6 @@ IShaderConstantSetter *FogShaderConstantSetterFactory::create() /* Other helpers */ -static gui::GUISkin *createSkin(gui::IGUIEnvironment *environment, - gui::EGUI_SKIN_TYPE type, video::IVideoDriver *driver) -{ - gui::GUISkin *skin = new gui::GUISkin(type, driver); - - gui::IGUIFont *builtinfont = environment->getBuiltInFont(); - gui::IGUIFontBitmap *bitfont = nullptr; - if (builtinfont && builtinfont->getType() == gui::EGFT_BITMAP) - bitfont = (gui::IGUIFontBitmap*)builtinfont; - - gui::IGUISpriteBank *bank = 0; - skin->setFont(builtinfont); - - if (bitfont) - bank = bitfont->getSpriteBank(); - - skin->setSpriteBank(bank); - - return skin; -} - static std::optional chooseVideoDriver() { auto &&configured_name = g_settings->get("video_driver"); @@ -195,7 +172,7 @@ static irr::IrrlichtDevice *createDevice(SIrrlichtCreationParameters params, std /* RenderingEngine class */ -RenderingEngine::RenderingEngine(IEventReceiver *receiver) +RenderingEngine::RenderingEngine(MyEventReceiver *receiver) { sanity_check(!s_singleton); @@ -248,12 +225,9 @@ RenderingEngine::RenderingEngine(IEventReceiver *receiver) // This changes the minimum allowed number of vertices in a VBO. Default is 500. driver->setMinHardwareBufferVertexCount(4); - s_singleton = this; + m_receiver = receiver; - auto skin = createSkin(m_device->getGUIEnvironment(), - gui::EGST_WINDOWS_METALLIC, driver); - m_device->getGUIEnvironment()->setSkin(skin); - skin->drop(); + s_singleton = this; g_settings->registerChangedCallback("fullscreen", settingChangedCallback, this); g_settings->registerChangedCallback("window_maximized", settingChangedCallback, this); @@ -418,7 +392,6 @@ std::vector RenderingEngine::getSupportedVideoDrivers() video::EDT_OPENGL, video::EDT_OPENGL3, video::EDT_OGLES2, - video::EDT_OGLES1, video::EDT_NULL, }; std::vector drivers; @@ -454,7 +427,6 @@ const VideoDriverInfo &RenderingEngine::getVideoDriverInfo(irr::video::E_DRIVER_ {(int)video::EDT_NULL, {"null", "NULL Driver"}}, {(int)video::EDT_OPENGL, {"opengl", "OpenGL"}}, {(int)video::EDT_OPENGL3, {"opengl3", "OpenGL 3+"}}, - {(int)video::EDT_OGLES1, {"ogles1", "OpenGL ES1"}}, {(int)video::EDT_OGLES2, {"ogles2", "OpenGL ES2"}}, }; return driver_info_map.at((int)type); diff --git a/src/client/renderingengine.h b/src/client/renderingengine.h index 1a2a63513..ffdda636c 100644 --- a/src/client/renderingengine.h +++ b/src/client/renderingengine.h @@ -23,12 +23,14 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include +#include "client/inputhandler.h" #include "irrlichttypes_extrabloated.h" #include "debug.h" #include "client/shader.h" #include "client/render/core.h" // include the shadow mapper classes too #include "client/shadows/dynamicshadowsrender.h" +#include #ifdef SERVER #error Do not include in server builds @@ -80,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); @@ -167,6 +168,12 @@ public: const irr::core::dimension2d initial_screen_size, const bool initial_window_maximized); + static PointerType getLastPointerType() + { + sanity_check(s_singleton && s_singleton->m_receiver); + return s_singleton->m_receiver->getLastPointerType(); + } + private: static void settingChangedCallback(const std::string &name, void *data); v2u32 _getWindowSize() const; @@ -174,5 +181,6 @@ private: std::unique_ptr core; irr::IrrlichtDevice *m_device = nullptr; irr::video::IVideoDriver *driver; + MyEventReceiver *m_receiver = nullptr; static RenderingEngine *s_singleton; }; diff --git a/src/client/shader.cpp b/src/client/shader.cpp index dae53ff96..d368ccb09 100644 --- a/src/client/shader.cpp +++ b/src/client/shader.cpp @@ -218,15 +218,15 @@ class MainShaderConstantSetter : public IShaderConstantSetter CachedVertexShaderSetting m_texture{"mTexture"}; // commonly used way to pass material color to shader - video::SColor m_emissive_color; - CachedPixelShaderSetting m_emissive_color_setting{"emissiveColor"}; + video::SColor m_material_color; + CachedPixelShaderSetting m_material_color_setting{"materialColor"}; public: ~MainShaderConstantSetter() = default; virtual void onSetMaterial(const video::SMaterial& material) override { - m_emissive_color = material.EmissiveColor; + m_material_color = material.ColorParam; } virtual void onSetConstants(video::IMaterialRendererServices *services) override @@ -249,13 +249,13 @@ public: m_world_view_proj.set(worldViewProj, services); if (driver->getDriverType() == video::EDT_OGLES2 || driver->getDriverType() == video::EDT_OPENGL3) { - core::matrix4 texture = driver->getTransform(video::ETS_TEXTURE_0); + auto &texture = driver->getTransform(video::ETS_TEXTURE_0); m_world_view.set(worldView, services); m_texture.set(texture, services); } - video::SColorf emissive_color(m_emissive_color); - m_emissive_color_setting.set(emissive_color, services); + video::SColorf colorf(m_material_color); + m_material_color_setting.set(colorf, services); } }; @@ -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 @@ -573,6 +585,7 @@ ShaderInfo ShaderSource::generateShader(const std::string &name, } else { shaders_header << "#version 100\n"; } + // cf. EVertexAttributes.h for the predefined ones vertex_header = R"( precision mediump float; @@ -582,15 +595,19 @@ ShaderInfo ShaderSource::generateShader(const std::string &name, attribute highp vec4 inVertexPosition; attribute lowp vec4 inVertexColor; - attribute mediump vec4 inTexCoord0; + attribute mediump vec2 inTexCoord0; attribute mediump vec3 inVertexNormal; 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 @@ -688,6 +705,15 @@ ShaderInfo ShaderSource::generateShader(const std::string &name, if (g_settings->getBool("shadow_poisson_filter")) shaders_header << "#define POISSON_FILTER 1\n"; + if (g_settings->getBool("enable_water_reflections")) + shaders_header << "#define ENABLE_WATER_REFLECTIONS 1\n"; + + if (g_settings->getBool("enable_translucent_foliage")) + shaders_header << "#define ENABLE_TRANSLUCENT_FOLIAGE 1\n"; + + if (g_settings->getBool("enable_node_specular")) + shaders_header << "#define ENABLE_NODE_SPECULAR 1\n"; + s32 shadow_filter = g_settings->getS32("shadow_filters"); shaders_header << "#define SHADOW_FILTER " << shadow_filter << "\n"; diff --git a/src/client/shadows/dynamicshadows.cpp b/src/client/shadows/dynamicshadows.cpp index 11db9bea7..2722c871b 100644 --- a/src/client/shadows/dynamicshadows.cpp +++ b/src/client/shadows/dynamicshadows.cpp @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "client/clientenvironment.h" #include "client/clientmap.h" #include "client/camera.h" +#include using m4f = core::matrix4; @@ -136,8 +137,8 @@ void DirectionalLight::update_frustum(const Camera *cam, Client *client, bool fo // when camera offset changes, adjust the current frustum view matrix to avoid flicker v3s16 cam_offset = cam->getOffset(); if (cam_offset != shadow_frustum.camera_offset) { - v3f rotated_offset; - shadow_frustum.ViewMat.rotateVect(rotated_offset, intToFloat(cam_offset - shadow_frustum.camera_offset, BS)); + v3f rotated_offset = shadow_frustum.ViewMat.rotateAndScaleVect( + intToFloat(cam_offset - shadow_frustum.camera_offset, BS)); shadow_frustum.ViewMat.setTranslation(shadow_frustum.ViewMat.getTranslation() + rotated_offset); shadow_frustum.player += intToFloat(shadow_frustum.camera_offset - cam->getOffset(), BS); shadow_frustum.camera_offset = cam_offset; diff --git a/src/client/shadows/dynamicshadowsrender.cpp b/src/client/shadows/dynamicshadowsrender.cpp index 1f49eed09..cfa54dfb7 100644 --- a/src/client/shadows/dynamicshadowsrender.cpp +++ b/src/client/shadows/dynamicshadowsrender.cpp @@ -32,6 +32,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "EShaderTypes.h" #include "IGPUProgrammingServices.h" #include "IMaterialRenderer.h" +#include ShadowRenderer::ShadowRenderer(IrrlichtDevice *device, Client *client) : m_smgr(device->getSceneManager()), m_driver(device->getVideoDriver()), @@ -140,7 +141,7 @@ void ShadowRenderer::initialize() } createShaders(); - + m_texture_format = m_shadow_map_texture_32bit ? video::ECOLOR_FORMAT::ECF_R32F diff --git a/src/client/shadows/dynamicshadowsrender.h b/src/client/shadows/dynamicshadowsrender.h index fc139e28b..d4437be68 100644 --- a/src/client/shadows/dynamicshadowsrender.h +++ b/src/client/shadows/dynamicshadowsrender.h @@ -21,6 +21,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include +#include #include "irrlichttypes_extrabloated.h" #include "client/shadows/dynamicshadows.h" @@ -93,9 +94,11 @@ public: bool is_active() const { return m_shadows_enabled && shadowMapTextureFinal != nullptr; } void setTimeOfDay(float isDay) { m_time_day = isDay; }; void setShadowIntensity(float shadow_intensity); + void setShadowTint(video::SColor shadow_tint) { m_shadow_tint = shadow_tint; } s32 getShadowSamples() const { return m_shadow_samples; } float getShadowStrength() const { return m_shadows_enabled ? m_shadow_strength : 0.0f; } + video::SColor getShadowTint() const { return m_shadow_tint; } float getTimeOfDay() const { return m_time_day; } f32 getPerspectiveBiasXY() { return m_perspective_bias_xy; } @@ -130,6 +133,7 @@ private: std::vector m_shadow_node_array; float m_shadow_strength; + video::SColor m_shadow_tint{ 255, 0, 0, 0 }; float m_shadow_strength_gamma; float m_shadow_map_max_distance; float m_shadow_map_texture_size; diff --git a/src/client/shadows/shadowsScreenQuad.cpp b/src/client/shadows/shadowsScreenQuad.cpp index 5f6d38157..d1713578d 100644 --- a/src/client/shadows/shadowsScreenQuad.cpp +++ b/src/client/shadows/shadowsScreenQuad.cpp @@ -18,11 +18,11 @@ with this program; if not, write to the Free Software Foundation, Inc., */ #include "shadowsScreenQuad.h" +#include shadowScreenQuad::shadowScreenQuad() { Material.Wireframe = false; - Material.Lighting = false; video::SColor color(0x0); Vertices[0] = video::S3DVertex( diff --git a/src/client/shadows/shadowsshadercallbacks.cpp b/src/client/shadows/shadowsshadercallbacks.cpp index 32d3e36be..fc4f73186 100644 --- a/src/client/shadows/shadowsshadercallbacks.cpp +++ b/src/client/shadows/shadowsshadercallbacks.cpp @@ -40,6 +40,9 @@ void ShadowConstantSetter::onSetConstants(video::IMaterialRendererServices *serv f32 ShadowStrength = shadow->getShadowStrength(); m_shadow_strength.set(&ShadowStrength, services); + video::SColor ShadowTint = shadow->getShadowTint(); + m_shadow_tint.set(ShadowTint, services); + f32 timeOfDay = shadow->getTimeOfDay(); m_time_of_day.set(&timeOfDay, services); diff --git a/src/client/shadows/shadowsshadercallbacks.h b/src/client/shadows/shadowsshadercallbacks.h index af83f021e..beac25d72 100644 --- a/src/client/shadows/shadowsshadercallbacks.h +++ b/src/client/shadows/shadowsshadercallbacks.h @@ -31,6 +31,7 @@ class ShadowConstantSetter : public IShaderConstantSetter CachedPixelShaderSetting m_light_direction{"v_LightDirection"}; CachedPixelShaderSetting m_texture_res{"f_textureresolution"}; CachedPixelShaderSetting m_shadow_strength{"f_shadow_strength"}; + CachedPixelShaderSetting m_shadow_tint{ "shadow_tint" }; CachedPixelShaderSetting m_time_of_day{"f_timeofday"}; CachedPixelShaderSetting m_shadowfar{"f_shadowfar"}; CachedPixelShaderSetting m_camera_pos{"CameraPos"}; diff --git a/src/client/sky.cpp b/src/client/sky.cpp index 92e5df218..27640bc28 100644 --- a/src/client/sky.cpp +++ b/src/client/sky.cpp @@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include +#include "client/mesh.h" #include "client/tile.h" #include "noise.h" // easeCurve #include "profiler.h" @@ -38,7 +39,6 @@ using namespace irr::core; static video::SMaterial baseMaterial() { video::SMaterial mat; - mat.Lighting = false; mat.ZBuffer = video::ECFN_DISABLED; mat.ZWriteEnable = video::EZW_OFF; mat.AntiAliasing = 0; @@ -77,10 +77,9 @@ Sky::Sky(s32 id, RenderingEngine *rendering_engine, ITextureSource *tsrc, IShade // Create materials m_materials[0] = baseMaterial(); - // FIXME: shouldn't this check m_enable_shaders? - m_materials[0].MaterialType = ssrc->getShaderInfo(ssrc->getShader("stars_shader", TILE_MATERIAL_ALPHA)).material; - m_materials[0].Lighting = true; - m_materials[0].ColorMaterial = video::ECM_NONE; + m_materials[0].MaterialType = m_enable_shaders ? + ssrc->getShaderInfo(ssrc->getShader("stars_shader", TILE_MATERIAL_ALPHA)).material : + video::EMT_TRANSPARENT_ALPHA_CHANNEL; m_materials[1] = baseMaterial(); m_materials[1].MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL; @@ -95,7 +94,6 @@ Sky::Sky(s32 id, RenderingEngine *rendering_engine, ITextureSource *tsrc, IShade for (int i = 5; i < 11; i++) { m_materials[i] = baseMaterial(); - m_materials[i].Lighting = true; m_materials[i].MaterialType = video::EMT_SOLID; } @@ -169,7 +167,8 @@ void Sky::render() video::SColor texel_color (255, texel->getRed(), texel->getGreen(), texel->getBlue()); m_sun_tonemap->unlock(); - m_materials[3].EmissiveColor = texel_color; + // Only accessed by our code later, not used by a shader + m_materials[3].ColorParam = texel_color; } if (m_moon_tonemap) { @@ -178,7 +177,8 @@ void Sky::render() video::SColor texel_color (255, texel->getRed(), texel->getGreen(), texel->getBlue()); m_moon_tonemap->unlock(); - m_materials[4].EmissiveColor = texel_color; + // Only accessed by our code later, not used by a shader + m_materials[4].ColorParam = texel_color; } const f32 t = 1.0f; @@ -465,11 +465,11 @@ void Sky::update(float time_of_day, float time_brightness, // which keeps previous behavior. if (m_sun_tonemap && m_default_tint) { pointcolor_sun_f.r = pointcolor_light * - (float)m_materials[3].EmissiveColor.getRed() / 255; + (float)m_materials[3].ColorParam.getRed() / 255; pointcolor_sun_f.b = pointcolor_light * - (float)m_materials[3].EmissiveColor.getBlue() / 255; + (float)m_materials[3].ColorParam.getBlue() / 255; pointcolor_sun_f.g = pointcolor_light * - (float)m_materials[3].EmissiveColor.getGreen() / 255; + (float)m_materials[3].ColorParam.getGreen() / 255; } else if (!m_default_tint) { pointcolor_sun_f = m_sky_params.fog_sun_tint; } else { @@ -498,11 +498,11 @@ void Sky::update(float time_of_day, float time_brightness, } if (m_moon_tonemap && m_default_tint) { pointcolor_moon_f.r = pointcolor_light * - (float)m_materials[4].EmissiveColor.getRed() / 255; + (float)m_materials[4].ColorParam.getRed() / 255; pointcolor_moon_f.b = pointcolor_light * - (float)m_materials[4].EmissiveColor.getBlue() / 255; + (float)m_materials[4].ColorParam.getBlue() / 255; pointcolor_moon_f.g = pointcolor_light * - (float)m_materials[4].EmissiveColor.getGreen() / 255; + (float)m_materials[4].ColorParam.getGreen() / 255; } video::SColor pointcolor_sun = pointcolor_sun_f.toSColor(); @@ -603,11 +603,8 @@ void Sky::draw_sun(video::IVideoDriver *driver, const video::SColor &suncolor, // Another magic number that contributes to the ratio 1.57 sun/moon size // difference. float d = (sunsize * 1.7) * m_sun_params.scale; - video::SColor c; - if (m_sun_tonemap) - c = video::SColor(0, 0, 0, 0); - else - c = video::SColor(255, 255, 255, 255); + video::SColor c = m_sun_tonemap ? m_materials[3].ColorParam : + video::SColor(255, 255, 255, 255); draw_sky_body(vertices, -d, d, c); place_sky_body(vertices, 90, wicked_time_of_day * 360 - 90); driver->drawIndexedTriangleList(&vertices[0], 4, indices, 2); @@ -660,11 +657,8 @@ void Sky::draw_moon(video::IVideoDriver *driver, const video::SColor &mooncolor, // Another magic number that contributes to the ratio 1.57 sun/moon size // difference. float d = (moonsize * 1.9) * m_moon_params.scale; - video::SColor c; - if (m_moon_tonemap) - c = video::SColor(0, 0, 0, 0); - else - c = video::SColor(255, 255, 255, 255); + video::SColor c = m_sun_tonemap ? m_materials[4].ColorParam : + video::SColor(255, 255, 255, 255); draw_sky_body(vertices, -d, d, c); place_sky_body(vertices, -90, wicked_time_of_day * 360 - 90); driver->drawIndexedTriangleList(&vertices[0], 4, indices, 2); @@ -688,7 +682,10 @@ void Sky::draw_stars(video::IVideoDriver * driver, float wicked_time_of_day) color.a *= alpha; if (color.a <= 0.0f) // Stars are only drawn when not fully transparent return; - m_materials[0].EmissiveColor = color.toSColor(); + if (m_enable_shaders) + m_materials[0].ColorParam = color.toSColor(); + else + setMeshBufferColor(m_stars.get(), color.toSColor()); auto sky_rotation = core::matrix4().setRotationAxisRadians(2.0f * M_PI * (wicked_time_of_day - 0.25f), v3f(0.0f, 0.0f, 1.0f)); auto world_matrix = driver->getTransform(video::ETS_WORLD); @@ -742,7 +739,6 @@ void Sky::setSunTexture(const std::string &sun_texture, m_sun_params.tonemap = sun_tonemap; m_sun_tonemap = tsrc->isKnownSourceImage(sun_tonemap) ? tsrc->getTexture(sun_tonemap) : nullptr; - m_materials[3].Lighting = !!m_sun_tonemap; if (m_sun_params.texture == sun_texture && !m_first_update) return; @@ -762,7 +758,6 @@ void Sky::setSunTexture(const std::string &sun_texture, m_materials[3].setTexture(0, m_sun_texture); m_materials[3].MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL; disableTextureFiltering(m_materials[3]); - m_materials[3].Lighting = !!m_sun_tonemap; } } @@ -786,7 +781,6 @@ void Sky::setMoonTexture(const std::string &moon_texture, m_moon_params.tonemap = moon_tonemap; m_moon_tonemap = tsrc->isKnownSourceImage(moon_tonemap) ? tsrc->getTexture(moon_tonemap) : nullptr; - m_materials[4].Lighting = !!m_moon_tonemap; if (m_moon_params.texture == moon_texture && !m_first_update) return; @@ -806,7 +800,6 @@ void Sky::setMoonTexture(const std::string &moon_texture, m_materials[4].setTexture(0, m_moon_texture); m_materials[4].MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL; disableTextureFiltering(m_materials[4]); - m_materials[4].Lighting = !!m_moon_tonemap; } } @@ -830,10 +823,11 @@ void Sky::updateStars() warningstream << "Requested " << m_star_params.count << " stars but " << 0x4000 << " is the max\n"; m_star_params.count = 0x4000; } - m_stars->Vertices.reallocate(4 * m_star_params.count); - m_stars->Indices.reallocate(6 * m_star_params.count); + auto &vertices = m_stars->Vertices->Data; + auto &indices = m_stars->Indices->Data; + vertices.reserve(4 * m_star_params.count); + indices.reserve(6 * m_star_params.count); - video::SColor fallback_color = m_star_params.starcolor; // used on GLES 2 “without shaders” PcgRandom rgen(m_seed); float d = (0.006 / 2) * m_star_params.scale; for (u16 i = 0; i < m_star_params.count; i++) { @@ -844,28 +838,25 @@ void Sky::updateStars() ); core::CMatrix4 a; a.buildRotateFromTo(v3f(0, 1, 0), r); - v3f p = v3f(-d, 1, -d); - v3f p1 = v3f(d, 1, -d); - v3f p2 = v3f(d, 1, d); - v3f p3 = v3f(-d, 1, d); - a.rotateVect(p); - a.rotateVect(p1); - a.rotateVect(p2); - a.rotateVect(p3); - m_stars->Vertices.push_back(video::S3DVertex(p, {}, fallback_color, {})); - m_stars->Vertices.push_back(video::S3DVertex(p1, {}, fallback_color, {})); - m_stars->Vertices.push_back(video::S3DVertex(p2, {}, fallback_color, {})); - m_stars->Vertices.push_back(video::S3DVertex(p3, {}, fallback_color, {})); + 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, {}, {}, {})); + vertices.push_back(video::S3DVertex(p3, {}, {}, {})); } for (u16 i = 0; i < m_star_params.count; i++) { - m_stars->Indices.push_back(i * 4 + 0); - m_stars->Indices.push_back(i * 4 + 1); - m_stars->Indices.push_back(i * 4 + 2); - m_stars->Indices.push_back(i * 4 + 2); - m_stars->Indices.push_back(i * 4 + 3); - m_stars->Indices.push_back(i * 4 + 0); + indices.push_back(i * 4 + 0); + indices.push_back(i * 4 + 1); + indices.push_back(i * 4 + 2); + indices.push_back(i * 4 + 2); + indices.push_back(i * 4 + 3); + indices.push_back(i * 4 + 0); } - m_stars->setHardwareMappingHint(scene::EHM_STATIC); + if (m_enable_shaders) + m_stars->setHardwareMappingHint(scene::EHM_STATIC); } void Sky::setSkyColors(const SkyColor &sky_color) diff --git a/src/client/sky.h b/src/client/sky.h index 2eadea561..eba301e18 100644 --- a/src/client/sky.h +++ b/src/client/sky.h @@ -19,16 +19,22 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once -#include "irrlichttypes_extrabloated.h" +#include "irrlichttypes_bloated.h" #include +#include #include -#include "camera.h" +#include "camera.h" // CameraMode #include "irr_ptr.h" -#include "shader.h" #include "skyparams.h" #define SKY_MATERIAL_COUNT 12 +namespace irr::video +{ + class IVideoDriver; +} + +class IShaderSource; class ITextureSource; // Skybox, rendered with zbuffer turned off, before all other nodes. diff --git a/src/client/sound/ogg_file.cpp b/src/client/sound/ogg_file.cpp index 11659c706..660dfdf94 100644 --- a/src/client/sound/ogg_file.cpp +++ b/src/client/sound/ogg_file.cpp @@ -26,6 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include // memcpy +#include namespace sound { diff --git a/src/client/sound/sound_manager.cpp b/src/client/sound/sound_manager.cpp index 679d3a155..fc171d565 100644 --- a/src/client/sound/sound_manager.cpp +++ b/src/client/sound/sound_manager.cpp @@ -26,9 +26,12 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "sound_singleton.h" #include "util/numeric.h" // myrand() +#include "util/tracy_wrapper.h" #include "filesys.h" #include "porting.h" +#include + namespace sound { void OpenALSoundManager::stepStreams(f32 dtime) @@ -347,6 +350,13 @@ void OpenALSoundManager::updateListener(const v3f &pos_, const v3f &vel_, void OpenALSoundManager::setListenerGain(f32 gain) { +#if defined(__APPLE__) + /* macOS OpenAL implementation ignore setting AL_GAIN to zero + * so we use smallest possible value + */ + if (gain == 0.0f) + gain = std::numeric_limits::min(); +#endif alListenerf(AL_GAIN, gain); } @@ -492,6 +502,8 @@ void *OpenALSoundManager::run() u64 t_step_start = porting::getTimeMs(); while (true) { + auto framemarker = FrameMarker("OpenALSoundManager::run()-frame").started(); + auto get_time_since_last_step = [&] { return (f32)(porting::getTimeMs() - t_step_start); }; diff --git a/src/client/sound/sound_singleton.h b/src/client/sound/sound_singleton.h index 32cd2d4f8..10ecc0d96 100644 --- a/src/client/sound/sound_singleton.h +++ b/src/client/sound/sound_singleton.h @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once +#include #include "al_helpers.h" namespace sound { diff --git a/src/client/texturesource.cpp b/src/client/texturesource.cpp index 12a21771a..f18fa6cbf 100644 --- a/src/client/texturesource.cpp +++ b/src/client/texturesource.cpp @@ -39,7 +39,7 @@ struct TextureInfo }; // TextureSource -class TextureSource : public IWritableTextureSource +class TextureSource final : public IWritableTextureSource { public: TextureSource(); @@ -137,7 +137,6 @@ public: video::ITexture* getNormalTexture(const std::string &name); video::SColor getTextureAverageColor(const std::string &name); - video::ITexture *getShaderFlagsTexture(bool normamap_present); private: @@ -541,25 +540,3 @@ video::SColor TextureSource::getTextureAverageColor(const std::string &name) return c; } - - -video::ITexture *TextureSource::getShaderFlagsTexture(bool normalmap_present) -{ - std::string tname = "__shaderFlagsTexture"; - tname += normalmap_present ? "1" : "0"; - - if (isKnownSourceImage(tname)) { - return getTexture(tname); - } - - video::IVideoDriver *driver = RenderingEngine::get_video_driver(); - video::IImage *flags_image = driver->createImage( - video::ECF_A8R8G8B8, core::dimension2d(1, 1)); - sanity_check(flags_image); - video::SColor c(255, normalmap_present ? 255 : 0, 0, 0); - flags_image->setPixel(0, 0, c); - insertSourceImage(tname, flags_image); - flags_image->drop(); - return getTexture(tname); - -} diff --git a/src/client/texturesource.h b/src/client/texturesource.h index 5fef20821..324c58e4f 100644 --- a/src/client/texturesource.h +++ b/src/client/texturesource.h @@ -20,10 +20,16 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once #include "irrlichttypes.h" -#include +#include #include #include +namespace irr::video +{ + class IImage; + class ITexture; +} + typedef std::vector Palette; /* @@ -65,7 +71,6 @@ public: virtual bool isKnownSourceImage(const std::string &name)=0; virtual video::ITexture* getNormalTexture(const std::string &name)=0; virtual video::SColor getTextureAverageColor(const std::string &name)=0; - virtual video::ITexture *getShaderFlagsTexture(bool normalmap_present)=0; }; class IWritableTextureSource : public ITextureSource @@ -87,7 +92,6 @@ public: virtual void rebuildImagesAndTextures()=0; virtual video::ITexture* getNormalTexture(const std::string &name)=0; virtual video::SColor getTextureAverageColor(const std::string &name)=0; - virtual video::ITexture *getShaderFlagsTexture(bool normalmap_present)=0; }; IWritableTextureSource *createTextureSource(); diff --git a/src/client/tile.h b/src/client/tile.h index d761eefdd..f41b127bf 100644 --- a/src/client/tile.h +++ b/src/client/tile.h @@ -62,8 +62,6 @@ struct FrameSpec u32 texture_id = 0; video::ITexture *texture = nullptr; - video::ITexture *normal_texture = nullptr; - video::ITexture *flags_texture = nullptr; }; #define MAX_TILE_LAYERS 2 @@ -114,8 +112,6 @@ struct TileLayer // Ordered for size, please do not reorder video::ITexture *texture = nullptr; - video::ITexture *normal_texture = nullptr; - video::ITexture *flags_texture = nullptr; u32 shader_id = 0; diff --git a/src/client/wieldmesh.cpp b/src/client/wieldmesh.cpp index 66f89efb1..e66214ae6 100644 --- a/src/client/wieldmesh.cpp +++ b/src/client/wieldmesh.cpp @@ -194,10 +194,9 @@ private: static ExtrusionMeshCache *g_extrusion_mesh_cache = nullptr; -WieldMeshSceneNode::WieldMeshSceneNode(scene::ISceneManager *mgr, s32 id, bool lighting): +WieldMeshSceneNode::WieldMeshSceneNode(scene::ISceneManager *mgr, s32 id): scene::ISceneNode(mgr->getRootSceneNode(), mgr, id), - m_material_type(video::EMT_TRANSPARENT_ALPHA_CHANNEL_REF), - m_lighting(lighting) + m_material_type(video::EMT_TRANSPARENT_ALPHA_CHANNEL_REF) { m_enable_shaders = g_settings->getBool("enable_shaders"); m_anisotropic_filter = g_settings->getBool("anisotropic_filter"); @@ -306,9 +305,6 @@ void WieldMeshSceneNode::setExtruded(const std::string &imagename, }); // mipmaps cause "thin black line" artifacts material.UseMipMaps = false; - if (m_enable_shaders) { - material.setTexture(2, tsrc->getShaderFlagsTexture(false)); - } } } @@ -343,7 +339,6 @@ static scene::SMesh *createSpecialNodeMesh(Client *client, MapNode n, if (p.layer.material_flags & MATERIAL_FLAG_ANIMATION) { const FrameSpec &frame = (*p.layer.frames)[0]; p.layer.texture = frame.texture; - p.layer.normal_texture = frame.normal_texture; } for (video::S3DVertex &v : p.vertices) { v.Color.setAlpha(255); @@ -394,8 +389,7 @@ void WieldMeshSceneNode::setItem(const ItemStack &item, Client *client, bool che // overlay is white, if present m_colors.emplace_back(true, video::SColor(0xFFFFFFFF)); // initialize the color - if (!m_lighting) - setColor(video::SColor(0xFFFFFFFF)); + setColor(video::SColor(0xFFFFFFFF)); return; } @@ -472,8 +466,7 @@ void WieldMeshSceneNode::setItem(const ItemStack &item, Client *client, bool che } // initialize the color - if (!m_lighting) - setColor(video::SColor(0xFFFFFFFF)); + setColor(video::SColor(0xFFFFFFFF)); return; } else { const std::string inventory_image = item.getInventoryImage(idef); @@ -489,8 +482,7 @@ void WieldMeshSceneNode::setItem(const ItemStack &item, Client *client, bool che m_colors.emplace_back(true, video::SColor(0xFFFFFFFF)); // initialize the color - if (!m_lighting) - setColor(video::SColor(0xFFFFFFFF)); + setColor(video::SColor(0xFFFFFFFF)); return; } @@ -500,7 +492,6 @@ void WieldMeshSceneNode::setItem(const ItemStack &item, Client *client, bool che void WieldMeshSceneNode::setColor(video::SColor c) { - assert(!m_lighting); scene::IMesh *mesh = m_meshnode->getMesh(); if (!mesh) return; @@ -539,7 +530,7 @@ void WieldMeshSceneNode::setNodeLightColor(video::SColor color) if (m_enable_shaders) { for (u32 i = 0; i < m_meshnode->getMaterialCount(); ++i) { video::SMaterial &material = m_meshnode->getMaterial(i); - material.EmissiveColor = color; + material.ColorParam = color; } } else { setColor(color); @@ -569,11 +560,6 @@ void WieldMeshSceneNode::changeToMesh(scene::IMesh *mesh) mesh->setHardwareMappingHint(scene::EHM_DYNAMIC); } - m_meshnode->forEachMaterial([this] (auto &mat) { - mat.Lighting = m_lighting; - // need to normalize normals when lighting is enabled (because of setScale()) - mat.NormalizeNormals = m_lighting; - }); m_meshnode->setVisible(true); } @@ -671,7 +657,6 @@ void getItemMesh(Client *client, const ItemStack &item, ItemMesh *result) tex.MagFilter = video::ETMAGF_NEAREST; }); material.BackfaceCulling = cull_backface; - material.Lighting = false; } rotateMeshXZby(mesh, -45); @@ -724,7 +709,6 @@ scene::SMesh *getExtrudedMesh(ITextureSource *tsrc, tex.MagFilter = video::ETMAGF_NEAREST; }); material.BackfaceCulling = true; - material.Lighting = false; material.MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL; material.MaterialTypeParam = 0.5f; } @@ -772,16 +756,6 @@ void postProcessNodeMesh(scene::SMesh *mesh, const ContentFeatures &f, } else { material.setTexture(0, layer->texture); } - if (use_shaders) { - if (layer->normal_texture) { - if (layer->animation_frame_count > 1) { - const FrameSpec &animation_frame = (*layer->frames)[0]; - material.setTexture(1, animation_frame.normal_texture); - } else - material.setTexture(1, layer->normal_texture); - } - material.setTexture(2, layer->flags_texture); - } if (apply_scale && tile->world_aligned) { u32 n = buf->getVertexCount(); diff --git a/src/client/wieldmesh.h b/src/client/wieldmesh.h index 6358a6665..08240c8ef 100644 --- a/src/client/wieldmesh.h +++ b/src/client/wieldmesh.h @@ -21,7 +21,20 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include -#include "irrlichttypes_extrabloated.h" +#include "irr_aabb3d.h" +#include "irr_v3d.h" +#include +#include +#include + +namespace irr::scene +{ + class ISceneManager; + class IMesh; + struct SMesh; +} + +using namespace irr; struct ItemStack; class Client; @@ -91,7 +104,7 @@ struct ItemMesh class WieldMeshSceneNode : public scene::ISceneNode { public: - WieldMeshSceneNode(scene::ISceneManager *mgr, s32 id = -1, bool lighting = false); + WieldMeshSceneNode(scene::ISceneManager *mgr, s32 id = -1); virtual ~WieldMeshSceneNode(); void setCube(const ContentFeatures &f, v3f wield_scale); @@ -119,9 +132,6 @@ private: scene::IMeshSceneNode *m_meshnode = nullptr; video::E_MATERIAL_TYPE m_material_type; - // True if SMaterial::Lighting should be enabled. - bool m_lighting; - bool m_enable_shaders; bool m_anisotropic_filter; bool m_bilinear_filter; diff --git a/src/clientdynamicinfo.cpp b/src/clientdynamicinfo.cpp index a9b2a6ef3..12bc23abd 100644 --- a/src/clientdynamicinfo.cpp +++ b/src/clientdynamicinfo.cpp @@ -23,7 +23,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "settings.h" #include "client/renderingengine.h" -#include "gui/touchscreengui.h" +#include "gui/guiFormSpecMenu.h" +#include "gui/touchcontrols.h" ClientDynamicInfo ClientDynamicInfo::getCurrent() { @@ -33,23 +34,26 @@ ClientDynamicInfo ClientDynamicInfo::getCurrent() f32 hud_scaling = g_settings->getFloat("hud_scaling", 0.5f, 20.0f); f32 real_gui_scaling = gui_scaling * density; f32 real_hud_scaling = hud_scaling * density; - bool touch_controls = g_touchscreengui; + bool touch_controls = g_touchcontrols; return { screen_size, real_gui_scaling, real_hud_scaling, - ClientDynamicInfo::calculateMaxFSSize(screen_size, gui_scaling), + ClientDynamicInfo::calculateMaxFSSize(screen_size, density, gui_scaling), touch_controls }; } -v2f32 ClientDynamicInfo::calculateMaxFSSize(v2u32 render_target_size, f32 gui_scaling) +v2f32 ClientDynamicInfo::calculateMaxFSSize(v2u32 render_target_size, f32 density, f32 gui_scaling) { - f32 factor = (g_settings->getBool("enable_touch") ? 10 : 15) / gui_scaling; - f32 ratio = (f32)render_target_size.X / (f32)render_target_size.Y; - if (ratio < 1) - return { factor, factor / ratio }; - else - return { factor * ratio, factor }; + // must stay in sync with GUIFormSpecMenu::calculateImgsize + + const double screen_dpi = density * 96; + + // assume padding[0,0] since max_formspec_size is used for fullscreen formspecs + double prefer_imgsize = GUIFormSpecMenu::getImgsize(render_target_size, + screen_dpi, gui_scaling); + return v2f32(render_target_size.X / prefer_imgsize, + render_target_size.Y / prefer_imgsize); } #endif diff --git a/src/clientdynamicinfo.h b/src/clientdynamicinfo.h index 39faeeecc..c43fcb8d8 100644 --- a/src/clientdynamicinfo.h +++ b/src/clientdynamicinfo.h @@ -42,6 +42,6 @@ public: static ClientDynamicInfo getCurrent(); private: - static v2f32 calculateMaxFSSize(v2u32 render_target_size, f32 gui_scaling); + static v2f32 calculateMaxFSSize(v2u32 render_target_size, f32 density, f32 gui_scaling); #endif }; diff --git a/src/cmake_config.h.in b/src/cmake_config.h.in index a8eb53edd..5dc6e4b74 100644 --- a/src/cmake_config.h.in +++ b/src/cmake_config.h.in @@ -41,3 +41,4 @@ #cmakedefine01 BUILD_UNITTESTS #cmakedefine01 BUILD_BENCHMARKS #cmakedefine01 USE_SDL2 +#cmakedefine01 BUILD_WITH_TRACY diff --git a/src/collision.cpp b/src/collision.cpp index d804f7f0d..f554dac80 100644 --- a/src/collision.cpp +++ b/src/collision.cpp @@ -273,7 +273,7 @@ static void add_object_boxes(Environment *env, const v3f pos_f, const v3f speed_f, ActiveObject *self, std::vector &cinfo) { - auto process_object = [&] (ActiveObject *object) { + auto process_object = [&cinfo] (ActiveObject *object) { if (object && object->collideWithObjects()) { aabb3f box; if (object->getCollisionBox(&box)) @@ -292,7 +292,7 @@ static void add_object_boxes(Environment *env, c_env->getActiveObjects(pos_f, distance, clientobjects); for (auto &clientobject : clientobjects) { - // Do collide with everything but itself and the parent CAO + // Do collide with everything but itself and children if (!self || (self != clientobject.obj && self != clientobject.obj->getParent())) { process_object(clientobject.obj); @@ -301,12 +301,12 @@ static void add_object_boxes(Environment *env, // add collision with local player LocalPlayer *lplayer = c_env->getLocalPlayer(); - if (lplayer->getParent() == nullptr) { + auto *obj = (ClientActiveObject*) lplayer->getCAO(); + if (!self || (self != obj && self != obj->getParent())) { aabb3f lplayer_collisionbox = lplayer->getCollisionbox(); v3f lplayer_pos = lplayer->getPosition(); lplayer_collisionbox.MinEdge += lplayer_pos; lplayer_collisionbox.MaxEdge += lplayer_pos; - auto *obj = (ActiveObject*) lplayer->getCAO(); cinfo.emplace_back(obj, 0, lplayer_collisionbox); } } @@ -315,7 +315,7 @@ static void add_object_boxes(Environment *env, { ServerEnvironment *s_env = dynamic_cast(env); if (s_env) { - // search for objects which are not us, or we are not its parent. + // search for objects which are not us and not our children. // we directly process the object in this callback to avoid useless // looping afterwards. auto include_obj_cb = [self, &process_object] (ServerActiveObject *obj) { @@ -623,8 +623,10 @@ bool collision_check_intersection(Environment *env, IGameDef *gamedef, Collision detection */ aabb3f checkbox = box_0; - checkbox.MinEdge += pos_f; - checkbox.MaxEdge += pos_f; + // aabbox3d::intersectsWithBox(box) returns true when the faces are touching perfectly. + // However, we do not want want a true-ish return value in that case. Add some tolerance. + checkbox.MinEdge += pos_f + (0.1f * BS); + checkbox.MaxEdge += pos_f - (0.1f * BS); /* Go through every node and object box diff --git a/src/collision.h b/src/collision.h index b29a222c3..a698e6328 100644 --- a/src/collision.h +++ b/src/collision.h @@ -65,7 +65,8 @@ struct collisionMoveResult std::vector collisions; }; -// Moves using a single iteration; speed should not exceed pos_max_d/dtime +/// @brief Moves using a single iteration; speed should not exceed pos_max_d/dtime +/// @param self (optional) ActiveObject to ignore in the collision detection. collisionMoveResult collisionMoveSimple(Environment *env,IGameDef *gamedef, f32 pos_max_d, const aabb3f &box_0, f32 stepheight, f32 dtime, @@ -73,7 +74,11 @@ collisionMoveResult collisionMoveSimple(Environment *env,IGameDef *gamedef, v3f accel_f, ActiveObject *self=NULL, bool collide_with_objects=true); -// check if box is in collision on actual position +/// @brief A simpler version of "collisionMoveSimple" that only checks whether +/// a collision occurs at the given position. +/// @param self (optional) ActiveObject to ignore in the collision detection. +/// @returns `true` when `box_0` truly intersects with a node or object. +/// Touching faces are not counted as intersection. bool collision_check_intersection(Environment *env, IGameDef *gamedef, const aabb3f &box_0, const v3f &pos_f, ActiveObject *self = nullptr, bool collide_with_objects = true); diff --git a/src/craftdef.cpp b/src/craftdef.cpp index 72b8e8f9d..611632579 100644 --- a/src/craftdef.cpp +++ b/src/craftdef.cpp @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include +#include #include "gamedef.h" #include "inventory.h" #include "util/serialize.h" diff --git a/src/debug.cpp b/src/debug.cpp index 04d59a6d7..8a9704dd7 100644 --- a/src/debug.cpp +++ b/src/debug.cpp @@ -54,7 +54,7 @@ void sanity_check_fn(const char *assertion, const char *file, #endif errorstream << std::endl << "In thread " << std::hex - << std::this_thread::get_id() << ":" << std::endl; + << std::this_thread::get_id() << ":\n" << std::dec; errorstream << file << ":" << line << ": " << function << ": An engine assumption '" << assertion << "' failed." << std::endl; @@ -69,7 +69,7 @@ void fatal_error_fn(const char *msg, const char *file, #endif errorstream << std::endl << "In thread " << std::hex - << std::this_thread::get_id() << ":" << std::endl; + << std::this_thread::get_id() << ":\n" << std::dec; errorstream << file << ":" << line << ": " << function << ": A fatal error occurred: " << msg << std::endl; diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp index f8cfd18b4..049359ef1 100644 --- a/src/defaultsettings.cpp +++ b/src/defaultsettings.cpp @@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "porting.h" #include "mapgen/mapgen.h" // Mapgen::setDefaultSettings #include "util/string.h" +#include "server.h" /* @@ -76,13 +77,6 @@ static bool detect_touch() return false; } - return false; -#elif defined(_WIN32) - // 0x01 The device has an integrated touch digitizer - // 0x80 The device is ready to receive digitizer input. - if ((GetSystemMetrics(SM_DIGITIZER) & 0x81) == 0x81) - return true; - return false; #else // we don't know, return default @@ -104,7 +98,21 @@ void set_default_settings() // Client settings->setDefault("address", ""); settings->setDefault("enable_sound", "true"); - settings->setDefault("enable_touch", bool_to_cstr(has_touch)); +#if defined(__unix__) && !defined(__APPLE__) && !defined (__ANDROID__) + // On Linux+X11 (not Linux+Wayland or Linux+XWayland), I've encountered a bug + // where fake mouse events were generated from touch events if in relative + // mouse mode, resulting in the touchscreen controls being instantly disabled + // again and thus making them unusable. + // => We can't switch based on the last input method used. + // => Fall back to hardware detection. + settings->setDefault("touch_controls", bool_to_cstr(has_touch)); +#else + settings->setDefault("touch_controls", "auto"); +#endif + // Since GUI scaling shouldn't suddenly change during a session, we use + // hardware detection for "touch_gui" instead of switching based on the last + // input method used. + settings->setDefault("touch_gui", bool_to_cstr(has_touch)); settings->setDefault("sound_volume", "0.8"); settings->setDefault("sound_volume_unfocused", "0.3"); settings->setDefault("mute_sound", "false"); @@ -281,6 +289,7 @@ void set_default_settings() settings->setDefault("view_bobbing_amount", "1.0"); settings->setDefault("fall_bobbing_amount", "0.03"); settings->setDefault("enable_3d_clouds", "true"); + settings->setDefault("soft_clouds", "false"); settings->setDefault("cloud_radius", "12"); settings->setDefault("menu_clouds", "true"); settings->setDefault("translucent_liquids", "true"); @@ -309,6 +318,7 @@ void set_default_settings() settings->setDefault("enable_particles", "true"); settings->setDefault("arm_inertia", "true"); settings->setDefault("show_nametag_backgrounds", "true"); + settings->setDefault("show_block_bounds_radius_near", "4"); settings->setDefault("transparency_sorting_distance", "16"); settings->setDefault("enable_minimap", "true"); @@ -336,10 +346,10 @@ void set_default_settings() settings->setDefault("antialiasing", "none"); settings->setDefault("enable_bloom", "false"); settings->setDefault("enable_bloom_debug", "false"); - settings->setDefault("bloom_strength_factor", "1.0"); - settings->setDefault("bloom_intensity", "0.05"); - settings->setDefault("bloom_radius", "1"); settings->setDefault("enable_volumetric_lighting", "false"); + settings->setDefault("enable_water_reflections", "false"); + settings->setDefault("enable_translucent_foliage", "false"); + settings->setDefault("enable_node_specular", "false"); // Effects Shadows settings->setDefault("enable_dynamic_shadows", "false"); @@ -450,7 +460,9 @@ void set_default_settings() settings->setDefault("enable_pvp", "true"); settings->setDefault("enable_mod_channels", "false"); settings->setDefault("disallow_empty_password", "false"); - settings->setDefault("disable_anticheat", "false"); + settings->setDefault("anticheat_flags", flagdesc_anticheat, + AC_DIGGING | AC_INTERACTION | AC_MOVEMENT); + settings->setDefault("anticheat_movement_tolerance", "1.0"); settings->setDefault("enable_rollback_recording", "false"); settings->setDefault("deprecated_lua_api_handling", "log"); diff --git a/src/emerge.cpp b/src/emerge.cpp index 425e294b8..788e2b745 100644 --- a/src/emerge.cpp +++ b/src/emerge.cpp @@ -31,6 +31,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "filesys.h" #include "log.h" #include "servermap.h" +#include "database/database.h" #include "mapblock.h" #include "mapgen/mg_biome.h" #include "mapgen/mg_ore.h" @@ -185,10 +186,22 @@ SchematicManager *EmergeManager::getWritableSchematicManager() return schemmgr; } +void EmergeManager::initMap(MapDatabaseAccessor *holder) +{ + FATAL_ERROR_IF(m_db, "Map database already initialized."); + assert(holder->dbase); + m_db = holder; +} + +void EmergeManager::resetMap() +{ + FATAL_ERROR_IF(m_threads_active, "Threads are still active."); + m_db = nullptr; +} void EmergeManager::initMapgens(MapgenParams *params) { - FATAL_ERROR_IF(!m_mapgens.empty(), "Mapgen already initialised."); + FATAL_ERROR_IF(!m_mapgens.empty(), "Mapgen already initialized."); mgparams = params; @@ -303,6 +316,12 @@ bool EmergeManager::enqueueBlockEmergeEx( } +size_t EmergeManager::getQueueSize() +{ + MutexAutoLock queuelock(m_queue_mutex); + return m_blocks_enqueued.size(); +} + bool EmergeManager::isBlockInQueue(v3s16 pos) { MutexAutoLock queuelock(m_queue_mutex); @@ -466,7 +485,7 @@ void EmergeThread::signal() } -bool EmergeThread::pushBlock(const v3s16 &pos) +bool EmergeThread::pushBlock(v3s16 pos) { m_block_queue.push(pos); return true; @@ -491,7 +510,7 @@ void EmergeThread::cancelPendingItems() } -void EmergeThread::runCompletionCallbacks(const v3s16 &pos, EmergeAction action, +void EmergeThread::runCompletionCallbacks(v3s16 pos, EmergeAction action, const EmergeCallbackList &callbacks) { m_emerge->reportCompletedEmerge(action); @@ -524,21 +543,38 @@ bool EmergeThread::popBlockEmerge(v3s16 *pos, BlockEmergeData *bedata) } -EmergeAction EmergeThread::getBlockOrStartGen( - const v3s16 &pos, bool allow_gen, MapBlock **block, BlockMakeData *bmdata) +EmergeAction EmergeThread::getBlockOrStartGen(const v3s16 pos, bool allow_gen, + const std::string *from_db, MapBlock **block, BlockMakeData *bmdata) { - MutexAutoLock envlock(m_server->m_env_mutex); + //TimeTaker tt("", nullptr, PRECISION_MICRO); + Server::EnvAutoLock envlock(m_server); + //g_profiler->avg("EmergeThread: lock wait time [us]", tt.stop()); + + auto block_ok = [] (MapBlock *b) { + return b && b->isGenerated(); + }; // 1). Attempt to fetch block from memory *block = m_map->getBlockNoCreateNoEx(pos); if (*block) { - if ((*block)->isGenerated()) + if (block_ok(*block)) { + // if we just read it from the db but the block exists that means + // someone else was faster. don't touch it to prevent data loss. + if (from_db) + verbosestream << "getBlockOrStartGen: block loading raced" << std::endl; return EMERGE_FROM_MEMORY; + } } else { - // 2). Attempt to load block from disk if it was not in the memory - *block = m_map->loadBlock(pos); - if (*block && (*block)->isGenerated()) + if (!from_db) { + // 2). We should attempt loading it return EMERGE_FROM_DISK; + } + // 2). Second invocation, we have the data + if (!from_db->empty()) { + *block = m_map->loadBlock(*from_db, pos); + if (block_ok(*block)) + return EMERGE_FROM_DISK; + } } // 3). Attempt to start generation @@ -553,7 +589,7 @@ EmergeAction EmergeThread::getBlockOrStartGen( MapBlock *EmergeThread::finishGen(v3s16 pos, BlockMakeData *bmdata, std::map *modified_blocks) { - MutexAutoLock envlock(m_server->m_env_mutex); + Server::EnvAutoLock envlock(m_server); ScopeProfiler sp(g_profiler, "EmergeThread: after Mapgen::makeChunk", SPT_AVG); @@ -643,7 +679,8 @@ void *EmergeThread::run() BEGIN_DEBUG_EXCEPTION_HANDLER v3s16 pos; - std::map modified_blocks; + std::map modified_blocks; + std::string databuf; m_map = &m_server->m_env->getServerMap(); m_emerge = m_server->getEmergeManager(); @@ -669,13 +706,30 @@ void *EmergeThread::run() continue; } + g_profiler->add(m_name + ": processed [#]", 1); + if (blockpos_over_max_limit(pos)) continue; bool allow_gen = bedata.flags & BLOCK_EMERGE_ALLOW_GEN; EMERGE_DBG_OUT("pos=" << pos << " allow_gen=" << allow_gen); - action = getBlockOrStartGen(pos, allow_gen, &block, &bmdata); + action = getBlockOrStartGen(pos, allow_gen, nullptr, &block, &bmdata); + + /* Try to load it */ + if (action == EMERGE_FROM_DISK) { + auto &m_db = *m_emerge->m_db; + { + ScopeProfiler sp(g_profiler, "EmergeThread: load block - async (sum)"); + MutexAutoLock dblock(m_db.mutex); + m_db.loadBlock(pos, databuf); + } + // actually load it, then decide again + action = getBlockOrStartGen(pos, allow_gen, &databuf, &block, &bmdata); + databuf.clear(); + } + + /* Generate it */ if (action == EMERGE_GENERATED) { bool error = false; m_trans_liquid = &bmdata.transforming_liquid; @@ -716,7 +770,7 @@ void *EmergeThread::run() MapEditEvent event; event.type = MEET_OTHER; event.setModifiedBlocks(modified_blocks); - MutexAutoLock envlock(m_server->m_env_mutex); + Server::EnvAutoLock envlock(m_server); m_map->dispatchEvent(event); } modified_blocks.clear(); diff --git a/src/emerge.h b/src/emerge.h index d7f018feb..cbdcc4c7c 100644 --- a/src/emerge.h +++ b/src/emerge.h @@ -46,6 +46,7 @@ class DecorationManager; class SchematicManager; class Server; class ModApiMapgen; +struct MapDatabaseAccessor; // Structure containing inputs/outputs for chunk generation struct BlockMakeData { @@ -173,6 +174,10 @@ public: SchematicManager *getWritableSchematicManager(); void initMapgens(MapgenParams *mgparams); + /// @param holder non-owned reference that must stay alive + void initMap(MapDatabaseAccessor *holder); + /// resets the reference + void resetMap(); void startThreads(); void stopThreads(); @@ -191,6 +196,7 @@ public: EmergeCompletionCallback callback, void *callback_param); + size_t getQueueSize(); bool isBlockInQueue(v3s16 pos); Mapgen *getCurrentMapgen(); @@ -206,6 +212,9 @@ private: std::vector m_threads; bool m_threads_active = false; + // The map database + MapDatabaseAccessor *m_db = nullptr; + std::mutex m_queue_mutex; std::map m_blocks_enqueued; std::unordered_map m_peer_queue_count; diff --git a/src/emerge_internal.h b/src/emerge_internal.h index 439c8227b..08e36778d 100644 --- a/src/emerge_internal.h +++ b/src/emerge_internal.h @@ -40,7 +40,7 @@ class EmergeScripting; class EmergeThread : public Thread { public: bool enable_mapgen_debug_info; - int id; + const int id; // Index of this thread EmergeThread(Server *server, int ethreadid); ~EmergeThread() = default; @@ -49,7 +49,7 @@ public: void signal(); // Requires queue mutex held - bool pushBlock(const v3s16 &pos); + bool pushBlock(v3s16 pos); void cancelPendingItems(); @@ -59,7 +59,7 @@ public: protected: void runCompletionCallbacks( - const v3s16 &pos, EmergeAction action, + v3s16 pos, EmergeAction action, const EmergeCallbackList &callbacks); private: @@ -79,8 +79,20 @@ private: bool popBlockEmerge(v3s16 *pos, BlockEmergeData *bedata); - EmergeAction getBlockOrStartGen( - const v3s16 &pos, bool allow_gen, MapBlock **block, BlockMakeData *data); + /** + * Try to get a block from memory and decide what to do. + * + * @param pos block position + * @param from_db serialized block data, optional + * (for second call after EMERGE_FROM_DISK was returned) + * @param allow_gen allow invoking mapgen? + * @param block output pointer for block + * @param data info for mapgen + * @return what to do for this block + */ + EmergeAction getBlockOrStartGen(v3s16 pos, bool allow_gen, + const std::string *from_db, MapBlock **block, BlockMakeData *data); + MapBlock *finishGen(v3s16 pos, BlockMakeData *bmdata, std::map *modified_blocks); diff --git a/src/filesys.cpp b/src/filesys.cpp index 4287c8b05..b0a1f318e 100644 --- a/src/filesys.cpp +++ b/src/filesys.cpp @@ -26,6 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include +#include #include "log.h" #include "config.h" #include "porting.h" @@ -34,6 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #endif + #ifdef __linux__ #include #include @@ -42,6 +44,19 @@ with this program; if not, write to the Free Software Foundation, Inc., #endif #endif +#ifdef _WIN32 +#include +#include +#include +#include +#else +#include +#include +#include +#include +#include +#endif + // Error from last OS call as string #ifdef _WIN32 #define LAST_OS_ERROR() porting::ConvertError(GetLastError()) @@ -58,11 +73,6 @@ namespace fs * Windows * ***********/ -#include -#include -#include -#include - std::vector GetDirListing(const std::string &pathstring) { std::vector listing; @@ -272,12 +282,6 @@ bool CopyFileContents(const std::string &source, const std::string &target) * POSIX * *********/ -#include -#include -#include -#include -#include - std::vector GetDirListing(const std::string &pathstring) { std::vector listing; @@ -380,41 +384,41 @@ bool RecursiveDelete(const std::string &path) Execute the 'rm' command directly, by fork() and execve() */ - infostream<<"Removing \""< argv = { + "rm", "-rf", path.c_str(), - NULL + nullptr }; - verbosestream<<"Executing '"<(argv.data())); - execv(argv[0], const_cast(argv)); - - // Execv shouldn't return. Failed. + // note: use cerr because our logging won't flush in forked process + std::cerr << "exec errno: " << errno << ": " << strerror(errno) + << std::endl; _exit(1); - } - else - { + } else { // Parent - int child_status; + int status; pid_t tpid; - do{ - tpid = wait(&child_status); - }while(tpid != child_pid); - return (child_status == 0); + do + tpid = waitpid(child_pid, &status, 0); + while (tpid != child_pid); + return WIFEXITED(status) && WEXITSTATUS(status) == 0; } } diff --git a/src/gamedef.h b/src/gamedef.h index 9a6c55ab1..f8d6d79e7 100644 --- a/src/gamedef.h +++ b/src/gamedef.h @@ -34,19 +34,19 @@ class Camera; class ModChannel; class ModStorage; class ModStorageDatabase; +struct SubgameSpec; +struct ModSpec; +struct ModIPCStore; namespace irr::scene { class IAnimatedMesh; class ISceneManager; } -struct SubgameSpec; -struct ModSpec; /* An interface for fetching game-global definitions like tool and mapnode properties */ - class IGameDef { public: @@ -63,6 +63,9 @@ public: // environment thread. virtual IRollbackManager* getRollbackManager() { return NULL; } + // Only usable on server. + virtual ModIPCStore *getModIPCStore() { return nullptr; } + // Shorthands // TODO: these should be made const-safe so that a const IGameDef* is // actually usable diff --git a/src/gettext.h b/src/gettext.h index 042729c1a..507d27e64 100644 --- a/src/gettext.h +++ b/src/gettext.h @@ -36,7 +36,8 @@ with this program; if not, write to the Free Software Foundation, Inc., // the USE_GETTEXT=0 case and can't assume that gettext is installed. #include - #define gettext(String) String + #define gettext(String) (String) + #define ngettext(String1, String2, n) ((n) == 1 ? (String1) : (String2)) #endif #define _(String) gettext(String) diff --git a/src/gettext_plural_form.cpp b/src/gettext_plural_form.cpp new file mode 100644 index 000000000..6a5322421 --- /dev/null +++ b/src/gettext_plural_form.cpp @@ -0,0 +1,256 @@ +// Minetest +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "gettext_plural_form.h" +#include "util/string.h" + +static size_t minsize(const GettextPluralForm::Ptr &form) +{ + return form ? form->size() : 0; +} + +static size_t minsize(const GettextPluralForm::Ptr &f, const GettextPluralForm::Ptr &g) +{ + if (sizeof(g) > 0) + return std::min(minsize(f), minsize(g)); + return f ? f->size() : 0; +} + +class Identity: public GettextPluralForm +{ + public: + Identity(size_t nplurals): GettextPluralForm(nplurals) {}; + NumT operator()(const NumT n) const override + { + return n; + } +}; + +class ConstValue: public GettextPluralForm +{ + public: + ConstValue(size_t nplurals, NumT val): GettextPluralForm(nplurals), value(val) {}; + NumT operator()(const NumT n) const override + { + return value; + } + private: + NumT value; +}; + +template typename F> +class UnaryOperation: public GettextPluralForm +{ + public: + UnaryOperation(const Ptr &op): + GettextPluralForm(minsize(op)), op(op) {} + NumT operator()(const NumT n) const override + { + if (operator bool()) + return func((*op)(n)); + return 0; + } + private: + Ptr op; + static constexpr F func = {}; +}; + +template typename F> +class BinaryOperation: public GettextPluralForm +{ + public: + BinaryOperation(const Ptr &lhs, const Ptr &rhs): + GettextPluralForm(minsize(lhs, rhs)), + lhs(lhs), rhs(rhs) {} + NumT operator()(const NumT n) const override + { + if (operator bool()) + return func((*lhs)(n), (*rhs)(n)); + return 0; + } + private: + Ptr lhs, rhs; + static constexpr F func = {}; +}; + +class TernaryOperation: public GettextPluralForm +{ + public: + TernaryOperation(const Ptr &cond, const Ptr &val, const Ptr &alt): + GettextPluralForm(std::min(minsize(cond), minsize(val, alt))), + cond(cond), val(val), alt(alt) {} + NumT operator()(const NumT n) const override + { + if (operator bool()) + return (*cond)(n) ? (*val)(n) : (*alt)(n); + return 0; + } + private: + Ptr cond, val, alt; +}; + +typedef std::pair ParserResult; +typedef ParserResult (*Parser)(const size_t, const std::wstring_view &); + +static ParserResult parse_expr(const size_t nplurals, const std::wstring_view &str); + +template typename Operator> +static ParserResult reduce_ltr(const size_t nplurals, const ParserResult &res, const wchar_t* pattern) +{ + if (!str_starts_with(res.second, pattern)) + return ParserResult(nullptr, res.second); + auto next = Parser(nplurals, res.second.substr(std::char_traits::length(pattern))); + if (!next.first) + return next; + next.first = GettextPluralForm::Ptr(new BinaryOperation(res.first, next.first)); + next.second = trim(next.second); + return next; +} + +template +static ParserResult reduce_ltr(const size_t nplurals, const ParserResult &res, const wchar_t**) +{ + return ParserResult(nullptr, res.second); +} + +template typename Operator, template typename... Operators> +static ParserResult reduce_ltr(const size_t nplurals, const ParserResult &res, const wchar_t** patterns) +{ + auto next = reduce_ltr(nplurals, res, patterns[0]); + if (next.first || next.second != res.second) + return next; + return reduce_ltr(nplurals, res, patterns+1); +} + +template typename Operator, template typename... Operators> +static ParserResult parse_ltr(const size_t nplurals, const std::wstring_view &str, const wchar_t** patterns) +{ + auto &&pres = Parser(nplurals, str); + if (!pres.first) + return pres; + pres.second = trim(pres.second); + while (!pres.second.empty()) { + auto next = reduce_ltr(nplurals, pres, patterns); + if (!next.first) + return pres; + next.second = trim(next.second); + pres = next; + } + return pres; +} + +static ParserResult parse_atomic(const size_t nplurals, const std::wstring_view &str) +{ + if (str.empty()) + return ParserResult(nullptr, str); + if (str[0] == 'n') + return ParserResult(new Identity(nplurals), trim(str.substr(1))); + + wchar_t* endp; + auto val = wcstoul(str.data(), &endp, 10); + return ParserResult(new ConstValue(nplurals, val), trim(str.substr(endp-str.data()))); +} + +static ParserResult parse_parenthesized(const size_t nplurals, const std::wstring_view &str) +{ + if (str.empty()) + return ParserResult(nullptr, str); + if (str[0] != '(') + return parse_atomic(nplurals, str); + auto result = parse_expr(nplurals, str.substr(1)); + if (result.first) { + if (result.second.empty() || result.second[0] != ')') + result.first = nullptr; + else + result.second = trim(result.second.substr(1)); + } + return result; +} + +static ParserResult parse_negation(const size_t nplurals, const std::wstring_view &str) +{ + if (str.empty()) + return ParserResult(nullptr, str); + if (str[0] != '!') + return parse_parenthesized(nplurals, str); + auto result = parse_negation(nplurals, trim(str.substr(1))); + if (result.first) + result.first = GettextPluralForm::Ptr(new UnaryOperation(result.first)); + return result; +} + +static ParserResult parse_multiplicative(const size_t nplurals, const std::wstring_view &str) +{ + static const wchar_t *patterns[] = { L"*", L"/", L"%" }; + return parse_ltr(nplurals, str, patterns); +} + +static ParserResult parse_additive(const size_t nplurals, const std::wstring_view &str) +{ + static const wchar_t *patterns[] = { L"+", L"-" }; + return parse_ltr(nplurals, str, patterns); +} + +static ParserResult parse_comparison(const size_t nplurals, const std::wstring_view &str) +{ + static const wchar_t *patterns[] = { L"<=", L">=", L"<", L">" }; + return parse_ltr(nplurals, str, patterns); +} + +static ParserResult parse_equality(const size_t nplurals, const std::wstring_view &str) +{ + static const wchar_t *patterns[] = { L"==", L"!=" }; + return parse_ltr(nplurals, str, patterns); +} + +static ParserResult parse_conjunction(const size_t nplurals, const std::wstring_view &str) +{ + static const wchar_t *and_pattern[] = { L"&&" }; + return parse_ltr(nplurals, str, and_pattern); +} + +static ParserResult parse_disjunction(const size_t nplurals, const std::wstring_view &str) +{ + static const wchar_t *or_pattern[] = { L"||" }; + return parse_ltr(nplurals, str, or_pattern); +} + +static ParserResult parse_ternary(const size_t nplurals, const std::wstring_view &str) +{ + auto pres = parse_disjunction(nplurals, str); + if (pres.second.empty() || pres.second[0] != '?') // no ? : + return pres; + auto cond = pres.first; + pres = parse_ternary(nplurals, trim(pres.second.substr(1))); + if (pres.second.empty() || pres.second[0] != ':') + return ParserResult(nullptr, pres.second); + auto val = pres.first; + pres = parse_ternary(nplurals, trim(pres.second.substr(1))); + return ParserResult(new TernaryOperation(cond, val, pres.first), pres.second); +} + +static ParserResult parse_expr(const size_t nplurals, const std::wstring_view &str) +{ + return parse_ternary(nplurals, trim(str)); +} + +GettextPluralForm::Ptr GettextPluralForm::parse(const size_t nplurals, const std::wstring_view &str) +{ + if (nplurals == 0) + return nullptr; + auto result = parse_expr(nplurals, str); + if (!result.second.empty()) + return nullptr; + return result.first; +} + +GettextPluralForm::Ptr GettextPluralForm::parseHeaderLine(const std::wstring_view &str) +{ + if (!str_starts_with(str, L"Plural-Forms: nplurals=") || !str_ends_with(str, L";")) + return nullptr; + auto nplurals = wcstoul(str.data()+23, nullptr, 10); + auto pos = str.find(L"plural="); + if (pos == str.npos) + return nullptr; + return parse(nplurals, str.substr(pos+7, str.size()-pos-8)); +} diff --git a/src/gettext_plural_form.h b/src/gettext_plural_form.h new file mode 100644 index 000000000..d73718965 --- /dev/null +++ b/src/gettext_plural_form.h @@ -0,0 +1,33 @@ +// Minetest +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once +#include +#include + +// Note that this only implements a subset of C expressions. See: +// https://git.savannah.gnu.org/gitweb/?p=gettext.git;a=blob;f=gettext-runtime/intl/plural.y +class GettextPluralForm +{ +public: + using NumT = unsigned long; + using Ptr = std::shared_ptr; + + size_t size() const + { + return nplurals; + }; + virtual NumT operator()(const NumT) const = 0; + virtual operator bool() const + { + return size() > 0; + } + virtual ~GettextPluralForm() {}; + + static GettextPluralForm::Ptr parse(const size_t nplurals, const std::wstring_view &str); + static GettextPluralForm::Ptr parseHeaderLine(const std::wstring_view &str); +protected: + GettextPluralForm(size_t nplurals): nplurals(nplurals) {}; +private: + const size_t nplurals; +}; diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 66d6a9ee8..04a03609d 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -19,12 +19,11 @@ set(gui_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/guiScene.cpp ${CMAKE_CURRENT_SOURCE_DIR}/guiScrollBar.cpp ${CMAKE_CURRENT_SOURCE_DIR}/guiScrollContainer.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/guiSkin.cpp ${CMAKE_CURRENT_SOURCE_DIR}/guiTable.cpp ${CMAKE_CURRENT_SOURCE_DIR}/guiHyperText.cpp ${CMAKE_CURRENT_SOURCE_DIR}/guiVolumeChange.cpp ${CMAKE_CURRENT_SOURCE_DIR}/modalMenu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/profilergraph.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/touchscreengui.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/touchcontrols.cpp PARENT_SCOPE ) diff --git a/src/gui/StyleSpec.h b/src/gui/StyleSpec.h index ef7416bca..373590a50 100644 --- a/src/gui/StyleSpec.h +++ b/src/gui/StyleSpec.h @@ -17,16 +17,17 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +#pragma once + #include "client/texturesource.h" #include "client/fontengine.h" #include "debug.h" -#include "irrlichttypes_extrabloated.h" +#include "irrlichttypes_bloated.h" #include "util/string.h" #include #include #include -#pragma once class StyleSpec { diff --git a/src/gui/guiAnimatedImage.cpp b/src/gui/guiAnimatedImage.cpp index 5e14d2ef2..1c13531dc 100644 --- a/src/gui/guiAnimatedImage.cpp +++ b/src/gui/guiAnimatedImage.cpp @@ -6,6 +6,7 @@ #include "util/string.h" #include #include +#include GUIAnimatedImage::GUIAnimatedImage(gui::IGUIEnvironment *env, gui::IGUIElement *parent, s32 id, const core::rect &rectangle) : diff --git a/src/gui/guiBackgroundImage.cpp b/src/gui/guiBackgroundImage.cpp index 96197ca2c..f3b71bb8c 100644 --- a/src/gui/guiBackgroundImage.cpp +++ b/src/gui/guiBackgroundImage.cpp @@ -19,6 +19,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #include "client/guiscalingfilter.h" #include "log.h" #include "client/texturesource.h" +#include GUIBackgroundImage::GUIBackgroundImage(gui::IGUIEnvironment *env, gui::IGUIElement *parent, s32 id, const core::rect &rectangle, diff --git a/src/gui/guiBox.cpp b/src/gui/guiBox.cpp index 443f1064f..972eb4538 100644 --- a/src/gui/guiBox.cpp +++ b/src/gui/guiBox.cpp @@ -18,6 +18,7 @@ with this program; if not, write to the Free Software Foundation, Inc., */ #include "guiBox.h" +#include GUIBox::GUIBox(gui::IGUIEnvironment *env, gui::IGUIElement *parent, s32 id, const core::rect &rectangle, diff --git a/src/gui/guiButton.cpp b/src/gui/guiButton.cpp index d78433edd..9592ba922 100644 --- a/src/gui/guiButton.cpp +++ b/src/gui/guiButton.cpp @@ -253,8 +253,8 @@ void GUIButton::draw() setFromState(); } - GUISkin* skin = dynamic_cast(Environment->getSkin()); video::IVideoDriver* driver = Environment->getVideoDriver(); + IGUISkin *skin = Environment->getSkin(); // END PATCH if (DrawBorder) @@ -737,7 +737,7 @@ void GUIButton::setFromStyle(const StyleSpec& style) Padding.UpperLeftCorner + BgMiddle.UpperLeftCorner, Padding.LowerRightCorner + BgMiddle.LowerRightCorner); - GUISkin* skin = dynamic_cast(Environment->getSkin()); + IGUISkin *skin = Environment->getSkin(); core::vector2d defaultPressOffset( skin->getSize(irr::gui::EGDS_BUTTON_PRESSED_IMAGE_OFFSET_X), skin->getSize(irr::gui::EGDS_BUTTON_PRESSED_IMAGE_OFFSET_Y)); diff --git a/src/gui/guiButton.h b/src/gui/guiButton.h index 4fad8747c..dd71788ba 100644 --- a/src/gui/guiButton.h +++ b/src/gui/guiButton.h @@ -10,7 +10,6 @@ #include "IGUISpriteBank.h" #include "ITexture.h" #include "SColor.h" -#include "guiSkin.h" #include "StyleSpec.h" using namespace irr; diff --git a/src/gui/guiEngine.cpp b/src/gui/guiEngine.cpp index fdc13fa14..200c26fa0 100644 --- a/src/gui/guiEngine.cpp +++ b/src/gui/guiEngine.cpp @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "client/renderingengine.h" #include "client/shader.h" #include "client/tile.h" +#include "clientdynamicinfo.h" #include "config.h" #include "content/content.h" #include "content/mods.h" @@ -40,6 +41,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include "client/imagefilters.h" +#include "util/tracy_wrapper.h" #if USE_SOUND #include "client/sound/sound_openal.h" @@ -141,10 +143,6 @@ GUIEngine::GUIEngine(JoystickController *joystick, // create texture source m_texture_source = std::make_unique(rendering_engine->get_video_driver()); - // create shader source - // (currently only used by clouds) - m_shader_source.reset(createShaderSource()); - // create soundmanager #if USE_SOUND if (g_settings->getBool("enable_sound") && g_sound_manager_singleton.get()) { @@ -216,15 +214,28 @@ GUIEngine::GUIEngine(JoystickController *joystick, /******************************************************************************/ -std::string findLocaleFileInMods(const std::string &path, const std::string &filename) +std::string findLocaleFileWithExtension(const std::string &path) +{ + if (fs::PathExists(path + ".mo")) + return path + ".mo"; + if (fs::PathExists(path + ".po")) + return path + ".po"; + if (fs::PathExists(path + ".tr")) + return path + ".tr"; + return ""; +} + + +/******************************************************************************/ +std::string findLocaleFileInMods(const std::string &path, const std::string &filename_no_ext) { std::vector mods = flattenMods(getModsInPath(path, "root", true)); for (const auto &mod : mods) { - std::string ret = mod.path + DIR_DELIM "locale" DIR_DELIM + filename; - if (fs::PathExists(ret)) { + std::string ret = findLocaleFileWithExtension( + mod.path + DIR_DELIM "locale" DIR_DELIM + filename_no_ext); + if (!ret.empty()) return ret; - } } return ""; @@ -237,19 +248,26 @@ Translations *GUIEngine::getContentTranslations(const std::string &path, if (domain.empty() || lang_code.empty()) return nullptr; - std::string filename = domain + "." + lang_code + ".tr"; - std::string key = path + DIR_DELIM "locale" DIR_DELIM + filename; + std::string filename_no_ext = domain + "." + lang_code; + std::string key = path + DIR_DELIM "locale" DIR_DELIM + filename_no_ext; if (key == m_last_translations_key) return &m_last_translations; std::string trans_path = key; - ContentType type = getContentType(path); - if (type == ContentType::GAME) - trans_path = findLocaleFileInMods(path + DIR_DELIM "mods" DIR_DELIM, filename); - else if (type == ContentType::MODPACK) - trans_path = findLocaleFileInMods(path, filename); - // We don't need to search for locale files in a mod, as there's only one `locale` folder. + + switch (getContentType(path)) { + case ContentType::GAME: + trans_path = findLocaleFileInMods(path + DIR_DELIM "mods" DIR_DELIM, + filename_no_ext); + break; + case ContentType::MODPACK: + trans_path = findLocaleFileInMods(path, filename_no_ext); + break; + default: + trans_path = findLocaleFileWithExtension(trans_path); + break; + } if (trans_path.empty()) return nullptr; @@ -259,7 +277,7 @@ Translations *GUIEngine::getContentTranslations(const std::string &path, std::string data; if (fs::ReadFile(trans_path, data)) { - m_last_translations.loadTranslation(data); + m_last_translations.loadTranslation(fs::GetFilenameFromPath(trans_path.c_str()), data); } return &m_last_translations; @@ -295,10 +313,6 @@ void GUIEngine::run() IrrlichtDevice *device = m_rendering_engine->get_raw_device(); video::IVideoDriver *driver = device->getVideoDriver(); - // Always create clouds because they may or may not be - // needed based on the game selected - cloudInit(); - unsigned int text_height = g_fontengine->getTextHeight(); // Reset fog color @@ -323,15 +337,19 @@ void GUIEngine::run() ); const bool initial_window_maximized = !g_settings->getBool("fullscreen") && g_settings->getBool("window_maximized"); + auto last_window_info = ClientDynamicInfo::getCurrent(); FpsControl fps_control; f32 dtime = 0.0f; fps_control.reset(); - while (m_rendering_engine->run() && !m_startgame && !m_kill) { + auto framemarker = FrameMarker("GUIEngine::run()-frame").started(); + while (m_rendering_engine->run() && !m_startgame && !m_kill) { + framemarker.end(); fps_control.limit(device, &dtime); + framemarker.start(); if (device->isWindowVisible()) { // check if we need to update the "upper left corner"-text @@ -339,6 +357,11 @@ void GUIEngine::run() updateTopLeftTextSize(); text_height = g_fontengine->getTextHeight(); } + auto window_info = ClientDynamicInfo::getCurrent(); + if (!window_info.equal(last_window_info)) { + m_script->handleMainMenuEvent("WindowInfoChange"); + last_window_info = window_info; + } driver->beginScene(true, true, RenderingEngine::MENU_SKY_COLOR); @@ -371,6 +394,7 @@ void GUIEngine::run() m_menu->getAndroidUIInput(); #endif } + framemarker.end(); m_script->beforeClose(); @@ -390,8 +414,6 @@ GUIEngine::~GUIEngine() m_irr_toplefttext->remove(); - m_cloud.clouds.reset(); - // delete textures for (image_definition &texture : m_textures) { if (texture.texture) @@ -399,26 +421,11 @@ GUIEngine::~GUIEngine() } } -/******************************************************************************/ -void GUIEngine::cloudInit() -{ - m_shader_source->addShaderConstantSetterFactory( - new FogShaderConstantSetterFactory()); - - m_cloud.clouds = make_irr(m_smgr, m_shader_source.get(), -1, rand()); - m_cloud.clouds->setHeight(100.0f); - m_cloud.clouds->update(v3f(0, 0, 0), video::SColor(255,240,240,255)); - - m_cloud.camera = m_smgr->addCameraSceneNode(0, - v3f(0,0,0), v3f(0, 60, 100)); - m_cloud.camera->setFarValue(10000); -} - /******************************************************************************/ void GUIEngine::drawClouds(float dtime) { - m_cloud.clouds->step(dtime*3); - m_smgr->drawAll(); + g_menuclouds->step(dtime * 3); + g_menucloudsmgr->drawAll(); } /******************************************************************************/ diff --git a/src/gui/guiEngine.h b/src/gui/guiEngine.h index fa4e1ebd3..2df0a0b60 100644 --- a/src/gui/guiEngine.h +++ b/src/gui/guiEngine.h @@ -203,8 +203,6 @@ private: MainMenuData *m_data = nullptr; /** texture source */ std::unique_ptr m_texture_source; - /** shader source */ - std::unique_ptr m_shader_source; /** sound manager */ std::unique_ptr m_sound_manager; @@ -279,23 +277,11 @@ private: /** and text that is in it */ EnrichedString m_toplefttext; - /** initialize cloud subsystem */ - void cloudInit(); /** do preprocessing for cloud subsystem */ void drawClouds(float dtime); - /** internam data required for drawing clouds */ - struct clouddata { - /** pointer to cloud class */ - irr_ptr clouds; - /** camera required for drawing clouds */ - scene::ICameraSceneNode *camera = nullptr; - }; - /** is drawing of clouds enabled atm */ - bool m_clouds_enabled = true; - /** data used to draw clouds */ - clouddata m_cloud; + bool m_clouds_enabled = true; static void fullscreenChangedCallback(const std::string &name, void *data); }; diff --git a/src/gui/guiFormSpecMenu.cpp b/src/gui/guiFormSpecMenu.cpp index a2982fdd2..efd7b7e8c 100644 --- a/src/gui/guiFormSpecMenu.cpp +++ b/src/gui/guiFormSpecMenu.cpp @@ -38,6 +38,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include "client/renderingengine.h" +#include "client/joystick_controller.h" #include "log.h" #include "client/hud.h" // drawItemStack #include "filesys.h" @@ -315,7 +316,7 @@ void GUIFormSpecMenu::parseSize(parserData* data, const std::string &element) data->invsize.Y = MYMAX(0, stof(parts[1])); lockSize(false); - if (!g_settings->getBool("enable_touch") && parts.size() == 3) { + if (!g_settings->getBool("touch_gui") && parts.size() == 3) { if (parts[2] == "true") { lockSize(true,v2u32(800,600)); } @@ -355,7 +356,7 @@ void GUIFormSpecMenu::parseContainerEnd(parserData* data, const std::string &) void GUIFormSpecMenu::parseScrollContainer(parserData *data, const std::string &element) { std::vector parts; - if (!precheckElement("scroll_container start", element, 4, 5, parts)) + if (!precheckElement("scroll_container start", element, 4, 6, parts)) return; std::vector v_pos = split(parts[0], ','); @@ -366,6 +367,12 @@ void GUIFormSpecMenu::parseScrollContainer(parserData *data, const std::string & if (parts.size() >= 5 && !parts[4].empty()) scroll_factor = stof(parts[4]); + std::optional content_padding_px; + if (parts.size() >= 6 && !parts[5].empty()) { + std::vector v_size = { parts[5], parts[5] }; + content_padding_px = getRealCoordinateGeometry(v_size)[orientation == "vertical" ? 1 : 0]; + } + MY_CHECKPOS("scroll_container", 0); MY_CHECKGEOM("scroll_container", 1); @@ -404,6 +411,7 @@ void GUIFormSpecMenu::parseScrollContainer(parserData *data, const std::string & GUIScrollContainer *mover = new GUIScrollContainer(Environment, clipper, spec_mover.fid, rect_mover, orientation, scroll_factor); + mover->setContentPadding(content_padding_px); data->current_parent = mover; @@ -2807,8 +2815,13 @@ void GUIFormSpecMenu::parseModel(parserData *data, const std::string &element) auto meshnode = e->setMesh(mesh); - for (u32 i = 0; i < textures.size() && i < meshnode->getMaterialCount(); ++i) - e->setTexture(i, m_tsrc->getTexture(unescape_string(textures[i]))); + for (u32 i = 0; i < meshnode->getMaterialCount(); ++i) { + const auto texture_idx = mesh->getTextureSlot(i); + if (texture_idx >= textures.size()) + warningstream << "Invalid model element: Not enough textures" << std::endl; + else + e->setTexture(i, m_tsrc->getTexture(unescape_string(textures[texture_idx]))); + } if (vec_rot.size() >= 2) e->setRotation(v2f(stof(vec_rot[0]), stof(vec_rot[1]))); @@ -3127,58 +3140,7 @@ void GUIFormSpecMenu::regenerateGui(v2u32 screensize) offset = v2s32(0,0); } - const double gui_scaling = g_settings->getFloat("gui_scaling", 0.5f, 42.0f); - const double screen_dpi = RenderingEngine::getDisplayDensity() * 96; - - double use_imgsize; - if (m_lock) { - // In fixed-size mode, inventory image size - // is 0.53 inch multiplied by the gui_scaling - // config parameter. This magic size is chosen - // to make the main menu (15.5 inventory images - // wide, including border) just fit into the - // default window (800 pixels wide) at 96 DPI - // and default scaling (1.00). - use_imgsize = 0.5555 * screen_dpi * gui_scaling; - } else { - // Variables for the maximum imgsize that can fit in the screen. - double fitx_imgsize; - double fity_imgsize; - - v2f padded_screensize( - mydata.screensize.X * (1.0f - mydata.padding.X * 2.0f), - mydata.screensize.Y * (1.0f - mydata.padding.Y * 2.0f) - ); - - if (mydata.real_coordinates) { - fitx_imgsize = padded_screensize.X / mydata.invsize.X; - fity_imgsize = padded_screensize.Y / mydata.invsize.Y; - } else { - // The maximum imgsize in the old coordinate system also needs to - // factor in padding and spacing along with 0.1 inventory slot spare - // and help text space, hence the magic numbers. - fitx_imgsize = padded_screensize.X / - ((5.0 / 4.0) * (0.5 + mydata.invsize.X)); - fity_imgsize = padded_screensize.Y / - ((15.0 / 13.0) * (0.85 + mydata.invsize.Y)); - } - - s32 min_screen_dim = std::min(padded_screensize.X, padded_screensize.Y); - - double prefer_imgsize; - if (g_settings->getBool("enable_touch")) { - // The preferred imgsize should be larger to accommodate the - // smaller screensize. - prefer_imgsize = min_screen_dim / 10 * gui_scaling; - } else { - // Desktop computers have more space, so try to fit 15 coordinates. - prefer_imgsize = min_screen_dim / 15 * gui_scaling; - } - // Try to use the preferred imgsize, but if that's bigger than the maximum - // size, use the maximum size. - use_imgsize = std::min(prefer_imgsize, - std::min(fitx_imgsize, fity_imgsize)); - } + double use_imgsize = calculateImgsize(mydata); // Everything else is scaled in proportion to the // inventory image size. The inventory slot spacing @@ -3653,7 +3615,7 @@ void GUIFormSpecMenu::showTooltip(const std::wstring &text, int tooltip_offset_x = m_btn_height; int tooltip_offset_y = m_btn_height; - if (m_pointer_type == PointerType::Touch) { + if (RenderingEngine::getLastPointerType() == PointerType::Touch) { tooltip_offset_x *= 3; tooltip_offset_y = 0; if (m_pointer.X > (s32)screenSize.X / 2) @@ -4021,10 +3983,9 @@ bool GUIFormSpecMenu::preprocessEvent(const SEvent& event) } if (event.EventType == irr::EET_JOYSTICK_INPUT_EVENT) { - /* TODO add a check like: - if (event.JoystickEvent != joystick_we_listen_for) + if (event.JoystickEvent.Joystick != m_joystick->getJoystickId()) return false; - */ + bool handled = m_joystick->handleEvent(event.JoystickEvent); if (handled) { if (m_joystick->wasKeyDown(KeyType::ESC)) { @@ -5066,3 +5027,68 @@ std::array GUIFormSpecMenu::getStyleForElement return ret; } + +double GUIFormSpecMenu::getFixedImgsize(double screen_dpi, double gui_scaling) +{ + // In fixed-size mode, inventory image size + // is 0.53 inch multiplied by the gui_scaling + // config parameter. This magic size is chosen + // to make the main menu (15.5 inventory images + // wide, including border) just fit into the + // default window (800 pixels wide) at 96 DPI + // and default scaling (1.00). + return 0.5555 * screen_dpi * gui_scaling; +} + +double GUIFormSpecMenu::getImgsize(v2u32 avail_screensize, double screen_dpi, double gui_scaling) +{ + double fixed_imgsize = getFixedImgsize(screen_dpi, gui_scaling); + + s32 min_screen_dim = std::min(avail_screensize.X, avail_screensize.Y); + double prefer_imgsize = min_screen_dim / 15 * gui_scaling; + // Use the available space more effectively on small windows/screens. + // This is especially important for mobile platforms. + prefer_imgsize = std::max(prefer_imgsize, fixed_imgsize); + return prefer_imgsize; +} + +double GUIFormSpecMenu::calculateImgsize(const parserData &data) +{ + // must stay in sync with ClientDynamicInfo::calculateMaxFSSize + + const double screen_dpi = RenderingEngine::getDisplayDensity() * 96; + const double gui_scaling = g_settings->getFloat("gui_scaling", 0.5f, 42.0f); + + // Fixed-size mode + if (m_lock) + return getFixedImgsize(screen_dpi, gui_scaling); + + // Variables for the maximum imgsize that can fit in the screen. + double fitx_imgsize; + double fity_imgsize; + + v2f padded_screensize( + data.screensize.X * (1.0f - data.padding.X * 2.0f), + data.screensize.Y * (1.0f - data.padding.Y * 2.0f) + ); + + if (data.real_coordinates) { + fitx_imgsize = padded_screensize.X / data.invsize.X; + fity_imgsize = padded_screensize.Y / data.invsize.Y; + } else { + // The maximum imgsize in the old coordinate system also needs to + // factor in padding and spacing along with 0.1 inventory slot spare + // and help text space, hence the magic numbers. + fitx_imgsize = padded_screensize.X / + ((5.0 / 4.0) * (0.5 + data.invsize.X)); + fity_imgsize = padded_screensize.Y / + ((15.0 / 13.0) * (0.85 + data.invsize.Y)); + } + + double prefer_imgsize = getImgsize(v2u32(padded_screensize.X, padded_screensize.Y), + screen_dpi, gui_scaling); + + // Try to use the preferred imgsize, but if that's bigger than the maximum + // size, use the maximum size. + return std::min(prefer_imgsize, std::min(fitx_imgsize, fity_imgsize)); +} diff --git a/src/gui/guiFormSpecMenu.h b/src/gui/guiFormSpecMenu.h index 8c4fda32e..7c4be4301 100644 --- a/src/gui/guiFormSpecMenu.h +++ b/src/gui/guiFormSpecMenu.h @@ -24,7 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include -#include "irrlichttypes_extrabloated.h" +#include "irrlichttypes_bloated.h" #include "irr_ptr.h" #include "inventory.h" #include "inventorymanager.h" @@ -32,11 +32,10 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "guiInventoryList.h" #include "guiScrollBar.h" #include "guiTable.h" -#include "network/networkprotocol.h" -#include "client/joystick_controller.h" #include "util/string.h" #include "util/enriched_string.h" #include "StyleSpec.h" +#include // gui::ECURSOR_ICON #include class InventoryManager; @@ -44,6 +43,7 @@ class ISimpleTextureSource; class Client; class GUIScrollContainer; class ISoundManager; +class JoystickController; enum FormspecFieldType { f_Button, @@ -296,6 +296,11 @@ public: void getAndroidUIInput(); #endif + // Returns the fixed formspec coordinate size for the given parameters. + static double getFixedImgsize(double screen_dpi, double gui_scaling); + // Returns the preferred non-fixed formspec coordinate size for the given parameters. + static double getImgsize(v2u32 avail_screensize, double screen_dpi, double gui_scaling); + protected: v2s32 getBasePos() const { @@ -514,6 +519,9 @@ private: // used by getAbsoluteRect s32 m_tabheader_upper_edge = 0; + + // Determines the size (in pixels) of formspec coordinate units. + double calculateImgsize(const parserData &data); }; class FormspecFormSource: public IFormSource diff --git a/src/gui/guiHyperText.cpp b/src/gui/guiHyperText.cpp index 45e5d6e98..44019ebe2 100644 --- a/src/gui/guiHyperText.cpp +++ b/src/gui/guiHyperText.cpp @@ -20,6 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "guiHyperText.h" #include "guiScrollBar.h" #include "client/fontengine.h" +#include "client/hud.h" // drawItemStack #include "IVideoDriver.h" #include "client/client.h" #include "client/renderingengine.h" @@ -1145,7 +1146,7 @@ bool GUIHyperText::OnEvent(const SEvent &event) } } - break; + return true; } } } diff --git a/src/gui/guiInventoryList.cpp b/src/gui/guiInventoryList.cpp index 02505d436..1dd36bfc9 100644 --- a/src/gui/guiInventoryList.cpp +++ b/src/gui/guiInventoryList.cpp @@ -21,6 +21,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "guiFormSpecMenu.h" #include "client/hud.h" #include "client/client.h" +#include "client/renderingengine.h" +#include GUIInventoryList::GUIInventoryList(gui::IGUIEnvironment *env, gui::IGUIElement *parent, @@ -153,7 +155,7 @@ void GUIInventoryList::draw() // Add hovering tooltip bool show_tooltip = !item.empty() && hovering && !selected_item; // Make it possible to see item tooltips on touchscreens - if (m_fs_menu->getPointerType() == PointerType::Touch) { + if (RenderingEngine::getLastPointerType() == PointerType::Touch) { show_tooltip |= hovering && selected && m_fs_menu->getSelectedAmount() != 0; } if (show_tooltip) { diff --git a/src/gui/guiItemImage.cpp b/src/gui/guiItemImage.cpp index a02285fcb..0c543e391 100644 --- a/src/gui/guiItemImage.cpp +++ b/src/gui/guiItemImage.cpp @@ -19,7 +19,9 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "guiItemImage.h" #include "client/client.h" +#include "client/hud.h" // drawItemStack #include "inventory.h" +#include GUIItemImage::GUIItemImage(gui::IGUIEnvironment *env, gui::IGUIElement *parent, s32 id, const core::rect &rectangle, const std::string &item_name, diff --git a/src/gui/guiKeyChangeMenu.cpp b/src/gui/guiKeyChangeMenu.cpp index 7e6c486e0..08ae5987e 100644 --- a/src/gui/guiKeyChangeMenu.cpp +++ b/src/gui/guiKeyChangeMenu.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include "settings.h" #include @@ -377,7 +378,7 @@ void GUIKeyChangeMenu::add_key(int id, std::wstring button_name, const std::stri key_settings.push_back(k); } -// compare with button_titles in touchscreengui.cpp +// compare with button_titles in touchcontrols.cpp void GUIKeyChangeMenu::init_keys() { this->add_key(GUI_ID_KEY_FORWARD_BUTTON, wstrgettext("Forward"), "keymap_forward"); diff --git a/src/gui/guiOpenURL.cpp b/src/gui/guiOpenURL.cpp index f91d31391..7ce79b0e5 100644 --- a/src/gui/guiOpenURL.cpp +++ b/src/gui/guiOpenURL.cpp @@ -20,6 +20,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #include "guiEditBoxWithScrollbar.h" #include #include +#include #include "client/renderingengine.h" #include "porting.h" #include "gettext.h" diff --git a/src/gui/guiPasswordChange.cpp b/src/gui/guiPasswordChange.cpp index 414ff36a8..a9b73dfe8 100644 --- a/src/gui/guiPasswordChange.cpp +++ b/src/gui/guiPasswordChange.cpp @@ -24,6 +24,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #include #include #include +#include #include "porting.h" #include "gettext.h" diff --git a/src/gui/guiPathSelectMenu.cpp b/src/gui/guiPathSelectMenu.cpp index 9c63e06b5..b4c3d36c3 100644 --- a/src/gui/guiPathSelectMenu.cpp +++ b/src/gui/guiPathSelectMenu.cpp @@ -18,6 +18,7 @@ */ #include "guiPathSelectMenu.h" +#include "guiFormSpecMenu.h" //required because of TextDest only !!! GUIFileSelectMenu::GUIFileSelectMenu(gui::IGUIEnvironment* env, gui::IGUIElement* parent, s32 id, IMenuManager *menumgr, diff --git a/src/gui/guiPathSelectMenu.h b/src/gui/guiPathSelectMenu.h index 11307d682..7757b2d7b 100644 --- a/src/gui/guiPathSelectMenu.h +++ b/src/gui/guiPathSelectMenu.h @@ -23,7 +23,8 @@ #include "modalMenu.h" #include "IGUIFileOpenDialog.h" -#include "guiFormSpecMenu.h" //required because of TextDest only !!! + +struct TextDest; class GUIFileSelectMenu : public GUIModalMenu { diff --git a/src/gui/guiScene.cpp b/src/gui/guiScene.cpp index 239fbe015..06784cd6e 100644 --- a/src/gui/guiScene.cpp +++ b/src/gui/guiScene.cpp @@ -21,6 +21,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include +#include #include "porting.h" GUIScene::GUIScene(gui::IGUIEnvironment *env, scene::ISceneManager *smgr, @@ -66,7 +67,6 @@ void GUIScene::setTexture(u32 idx, video::ITexture *texture) material.MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL; material.MaterialTypeParam = 0.5f; material.TextureLayers[0].Texture = texture; - material.Lighting = false; material.FogEnable = true; material.TextureLayers[0].MinFilter = video::ETMINF_NEAREST_MIPMAP_NEAREST; material.TextureLayers[0].MagFilter = video::ETMAGF_NEAREST; @@ -157,7 +157,7 @@ void GUIScene::setStyles(const std::array &sty /** * Sets the frame loop range for the mesh */ -void GUIScene::setFrameLoop(s32 begin, s32 end) +void GUIScene::setFrameLoop(f32 begin, f32 end) { if (m_mesh->getStartFrame() != begin || m_mesh->getEndFrame() != end) m_mesh->setFrameLoop(begin, end); @@ -225,8 +225,7 @@ void GUIScene::setCameraRotation(v3f rot) core::matrix4 mat; mat.setRotationDegrees(rot); - m_cam_pos = v3f(0.f, 0.f, m_cam_distance); - mat.rotateVect(m_cam_pos); + m_cam_pos = mat.rotateAndScaleVect(v3f(0.f, 0.f, m_cam_distance)); m_cam_pos += m_target_pos; m_cam->setPosition(m_cam_pos); diff --git a/src/gui/guiScene.h b/src/gui/guiScene.h index 0f5f3a891..0634669f7 100644 --- a/src/gui/guiScene.h +++ b/src/gui/guiScene.h @@ -36,7 +36,7 @@ public: scene::IAnimatedMeshSceneNode *setMesh(scene::IAnimatedMesh *mesh = nullptr); void setTexture(u32 idx, video::ITexture *texture); void setBackgroundColor(const video::SColor &color) noexcept { m_bgcolor = color; }; - void setFrameLoop(s32 begin, s32 end); + void setFrameLoop(f32 begin, f32 end); void setAnimationSpeed(f32 speed); void enableMouseControl(bool enable) noexcept { m_mouse_ctrl = enable; }; void setRotation(v2f rot) noexcept { m_custom_rot = rot; }; diff --git a/src/gui/guiScrollBar.h b/src/gui/guiScrollBar.h index 05e195aed..af3bc4652 100644 --- a/src/gui/guiScrollBar.h +++ b/src/gui/guiScrollBar.h @@ -45,6 +45,7 @@ public: s32 getSmallStep() const { return small_step; } s32 getPos() const; s32 getTargetPos() const; + bool isHorizontal() const { return is_horizontal; } void setMax(const s32 &max); void setMin(const s32 &min); diff --git a/src/gui/guiScrollContainer.cpp b/src/gui/guiScrollContainer.cpp index 2d71f3453..13ba5c35f 100644 --- a/src/gui/guiScrollContainer.cpp +++ b/src/gui/guiScrollContainer.cpp @@ -67,6 +67,50 @@ void GUIScrollContainer::draw() } } +void GUIScrollContainer::setScrollBar(GUIScrollBar *scrollbar) +{ + m_scrollbar = scrollbar; + + if (m_scrollbar && m_content_padding_px.has_value() && m_scrollfactor != 0.0f) { + // Set the scrollbar max value based on the content size. + + // Get content size based on elements + core::rect size; + for (gui::IGUIElement *e : Children) { + core::rect abs_rect = e->getAbsolutePosition(); + size.addInternalPoint(abs_rect.LowerRightCorner); + } + + s32 visible_content_px = ( + m_orientation == VERTICAL + ? AbsoluteClippingRect.getHeight() + : AbsoluteClippingRect.getWidth() + ); + + s32 total_content_px = *m_content_padding_px + ( + m_orientation == VERTICAL + ? (size.LowerRightCorner.Y - AbsoluteClippingRect.UpperLeftCorner.Y) + : (size.LowerRightCorner.X - AbsoluteClippingRect.UpperLeftCorner.X) + ); + + s32 hidden_content_px = std::max(0, total_content_px - visible_content_px); + m_scrollbar->setMin(0); + m_scrollbar->setMax(std::ceil(hidden_content_px / std::fabs(m_scrollfactor))); + + // Note: generally, the scrollbar has the same size as the scroll container. + // However, in case it isn't, proportional adjustments are needed. + s32 scrollbar_px = ( + m_scrollbar->isHorizontal() + ? m_scrollbar->getRelativePosition().getWidth() + : m_scrollbar->getRelativePosition().getHeight() + ); + + m_scrollbar->setPageSize((total_content_px * scrollbar_px) / visible_content_px); + } + + updateScrolling(); +} + void GUIScrollContainer::updateScrolling() { s32 pos = m_scrollbar->getPos(); diff --git a/src/gui/guiScrollContainer.h b/src/gui/guiScrollContainer.h index 9e3ec6e93..d6871a53e 100644 --- a/src/gui/guiScrollContainer.h +++ b/src/gui/guiScrollContainer.h @@ -34,17 +34,18 @@ public: virtual void draw() override; + inline void setContentPadding(std::optional padding) + { + m_content_padding_px = padding; + } + inline void onScrollEvent(gui::IGUIElement *caller) { if (caller == m_scrollbar) updateScrolling(); } - inline void setScrollBar(GUIScrollBar *scrollbar) - { - m_scrollbar = scrollbar; - updateScrolling(); - } + void setScrollBar(GUIScrollBar *scrollbar); private: enum OrientationEnum @@ -56,7 +57,8 @@ private: GUIScrollBar *m_scrollbar; OrientationEnum m_orientation; - f32 m_scrollfactor; + f32 m_scrollfactor; //< scrollbar pos * scrollfactor = scroll offset in pixels + std::optional m_content_padding_px; //< in pixels void updateScrolling(); }; diff --git a/src/gui/guiSkin.cpp b/src/gui/guiSkin.cpp deleted file mode 100644 index 0ecc80f02..000000000 --- a/src/gui/guiSkin.cpp +++ /dev/null @@ -1,1037 +0,0 @@ -// Copyright (C) 2002-2012 Nikolaus Gebhardt -// Copyright (C) 2019 Irrlick -// -// This file is part of the "Irrlicht Engine". -// For conditions of distribution and use, see copyright notice in irrlicht.h - -#include "guiSkin.h" - -#include "IGUIFont.h" -#include "IGUISpriteBank.h" -#include "IGUIElement.h" -#include "IVideoDriver.h" -#include "IAttributes.h" - -namespace irr -{ -namespace gui -{ - -GUISkin::GUISkin(EGUI_SKIN_TYPE type, video::IVideoDriver* driver) -: SpriteBank(0), Driver(driver), Type(type) -{ - #ifdef _DEBUG - setDebugName("GUISkin"); - #endif - - if ((Type == EGST_WINDOWS_CLASSIC) || (Type == EGST_WINDOWS_METALLIC)) - { - Colors[EGDC_3D_DARK_SHADOW] = video::SColor(101,50,50,50); - Colors[EGDC_3D_SHADOW] = video::SColor(101,130,130,130); - Colors[EGDC_3D_FACE] = video::SColor(220,100,100,100); - Colors[EGDC_3D_HIGH_LIGHT] = video::SColor(101,255,255,255); - Colors[EGDC_3D_LIGHT] = video::SColor(101,210,210,210); - Colors[EGDC_ACTIVE_BORDER] = video::SColor(101,16,14,115); - Colors[EGDC_ACTIVE_CAPTION] = video::SColor(255,255,255,255); - Colors[EGDC_APP_WORKSPACE] = video::SColor(101,100,100,100); - Colors[EGDC_BUTTON_TEXT] = video::SColor(240,10,10,10); - Colors[EGDC_GRAY_TEXT] = video::SColor(240,130,130,130); - Colors[EGDC_HIGH_LIGHT] = video::SColor(101,8,36,107); - Colors[EGDC_HIGH_LIGHT_TEXT] = video::SColor(240,255,255,255); - Colors[EGDC_INACTIVE_BORDER] = video::SColor(101,165,165,165); - Colors[EGDC_INACTIVE_CAPTION] = video::SColor(255,30,30,30); - Colors[EGDC_TOOLTIP] = video::SColor(200,0,0,0); - Colors[EGDC_TOOLTIP_BACKGROUND] = video::SColor(200,255,255,225); - Colors[EGDC_SCROLLBAR] = video::SColor(101,230,230,230); - Colors[EGDC_WINDOW] = video::SColor(101,255,255,255); - Colors[EGDC_WINDOW_SYMBOL] = video::SColor(200,10,10,10); - Colors[EGDC_ICON] = video::SColor(200,255,255,255); - Colors[EGDC_ICON_HIGH_LIGHT] = video::SColor(200,8,36,107); - Colors[EGDC_GRAY_WINDOW_SYMBOL] = video::SColor(240,100,100,100); - Colors[EGDC_EDITABLE] = video::SColor(255,255,255,255); - Colors[EGDC_GRAY_EDITABLE] = video::SColor(255,120,120,120); - Colors[EGDC_FOCUSED_EDITABLE] = video::SColor(255,240,240,255); - - - Sizes[EGDS_SCROLLBAR_SIZE] = 14; - Sizes[EGDS_MENU_HEIGHT] = 30; - Sizes[EGDS_WINDOW_BUTTON_WIDTH] = 15; - Sizes[EGDS_CHECK_BOX_WIDTH] = 18; - Sizes[EGDS_MESSAGE_BOX_WIDTH] = 500; - Sizes[EGDS_MESSAGE_BOX_HEIGHT] = 200; - Sizes[EGDS_BUTTON_WIDTH] = 80; - Sizes[EGDS_BUTTON_HEIGHT] = 30; - - Sizes[EGDS_TEXT_DISTANCE_X] = 2; - Sizes[EGDS_TEXT_DISTANCE_Y] = 0; - - Sizes[EGDS_TITLEBARTEXT_DISTANCE_X] = 2; - Sizes[EGDS_TITLEBARTEXT_DISTANCE_Y] = 0; - } - else - { - //0x80a6a8af - Colors[EGDC_3D_DARK_SHADOW] = 0x60767982; - //Colors[EGDC_3D_FACE] = 0xc0c9ccd4; // tab background - Colors[EGDC_3D_FACE] = 0xc0cbd2d9; // tab background - Colors[EGDC_3D_SHADOW] = 0x50e4e8f1; // tab background, and left-top highlight - Colors[EGDC_3D_HIGH_LIGHT] = 0x40c7ccdc; - Colors[EGDC_3D_LIGHT] = 0x802e313a; - Colors[EGDC_ACTIVE_BORDER] = 0x80404040; // window title - Colors[EGDC_ACTIVE_CAPTION] = 0xffd0d0d0; - Colors[EGDC_APP_WORKSPACE] = 0xc0646464; // unused - Colors[EGDC_BUTTON_TEXT] = 0xd0161616; - Colors[EGDC_GRAY_TEXT] = 0x3c141414; - Colors[EGDC_HIGH_LIGHT] = 0x6c606060; - Colors[EGDC_HIGH_LIGHT_TEXT] = 0xd0e0e0e0; - Colors[EGDC_INACTIVE_BORDER] = 0xf0a5a5a5; - Colors[EGDC_INACTIVE_CAPTION] = 0xffd2d2d2; - Colors[EGDC_TOOLTIP] = 0xf00f2033; - Colors[EGDC_TOOLTIP_BACKGROUND] = 0xc0cbd2d9; - Colors[EGDC_SCROLLBAR] = 0xf0e0e0e0; - Colors[EGDC_WINDOW] = 0xf0f0f0f0; - Colors[EGDC_WINDOW_SYMBOL] = 0xd0161616; - Colors[EGDC_ICON] = 0xd0161616; - Colors[EGDC_ICON_HIGH_LIGHT] = 0xd0606060; - Colors[EGDC_GRAY_WINDOW_SYMBOL] = 0x3c101010; - Colors[EGDC_EDITABLE] = 0xf0ffffff; - Colors[EGDC_GRAY_EDITABLE] = 0xf0cccccc; - Colors[EGDC_FOCUSED_EDITABLE] = 0xf0fffff0; - - Sizes[EGDS_SCROLLBAR_SIZE] = 14; - Sizes[EGDS_MENU_HEIGHT] = 48; - Sizes[EGDS_WINDOW_BUTTON_WIDTH] = 15; - Sizes[EGDS_CHECK_BOX_WIDTH] = 18; - Sizes[EGDS_MESSAGE_BOX_WIDTH] = 500; - Sizes[EGDS_MESSAGE_BOX_HEIGHT] = 200; - Sizes[EGDS_BUTTON_WIDTH] = 80; - Sizes[EGDS_BUTTON_HEIGHT] = 30; - - Sizes[EGDS_TEXT_DISTANCE_X] = 3; - Sizes[EGDS_TEXT_DISTANCE_Y] = 2; - - Sizes[EGDS_TITLEBARTEXT_DISTANCE_X] = 3; - Sizes[EGDS_TITLEBARTEXT_DISTANCE_Y] = 2; - } - - Sizes[EGDS_MESSAGE_BOX_GAP_SPACE] = 15; - Sizes[EGDS_MESSAGE_BOX_MIN_TEXT_WIDTH] = 0; - Sizes[EGDS_MESSAGE_BOX_MAX_TEXT_WIDTH] = 500; - Sizes[EGDS_MESSAGE_BOX_MIN_TEXT_HEIGHT] = 0; - Sizes[EGDS_MESSAGE_BOX_MAX_TEXT_HEIGHT] = 99999; - - Sizes[EGDS_BUTTON_PRESSED_IMAGE_OFFSET_X] = 1; - Sizes[EGDS_BUTTON_PRESSED_IMAGE_OFFSET_Y] = 1; - Sizes[EGDS_BUTTON_PRESSED_TEXT_OFFSET_X] = 0; - Sizes[EGDS_BUTTON_PRESSED_TEXT_OFFSET_Y] = 2; - - Texts[EGDT_MSG_BOX_OK] = L"OK"; - Texts[EGDT_MSG_BOX_CANCEL] = L"Cancel"; - Texts[EGDT_MSG_BOX_YES] = L"Yes"; - Texts[EGDT_MSG_BOX_NO] = L"No"; - Texts[EGDT_WINDOW_CLOSE] = L"Close"; - Texts[EGDT_WINDOW_RESTORE] = L"Restore"; - Texts[EGDT_WINDOW_MINIMIZE] = L"Minimize"; - Texts[EGDT_WINDOW_MAXIMIZE] = L"Maximize"; - - Icons[EGDI_WINDOW_MAXIMIZE] = 225; - Icons[EGDI_WINDOW_RESTORE] = 226; - Icons[EGDI_WINDOW_CLOSE] = 227; - Icons[EGDI_WINDOW_MINIMIZE] = 228; - Icons[EGDI_CURSOR_UP] = 229; - Icons[EGDI_CURSOR_DOWN] = 230; - Icons[EGDI_CURSOR_LEFT] = 231; - Icons[EGDI_CURSOR_RIGHT] = 232; - Icons[EGDI_MENU_MORE] = 232; - Icons[EGDI_CHECK_BOX_CHECKED] = 233; - Icons[EGDI_DROP_DOWN] = 234; - Icons[EGDI_SMALL_CURSOR_UP] = 235; - Icons[EGDI_SMALL_CURSOR_DOWN] = 236; - Icons[EGDI_RADIO_BUTTON_CHECKED] = 237; - Icons[EGDI_MORE_LEFT] = 238; - Icons[EGDI_MORE_RIGHT] = 239; - Icons[EGDI_MORE_UP] = 240; - Icons[EGDI_MORE_DOWN] = 241; - Icons[EGDI_WINDOW_RESIZE] = 242; - Icons[EGDI_EXPAND] = 243; - Icons[EGDI_COLLAPSE] = 244; - - Icons[EGDI_FILE] = 245; - Icons[EGDI_DIRECTORY] = 246; - - for (u32 i=0; idrop(); - } - - if (SpriteBank) - SpriteBank->drop(); -} - - -//! returns default color -video::SColor GUISkin::getColor(EGUI_DEFAULT_COLOR color) const -{ - if ((u32)color < EGDC_COUNT) - return Colors[color]; - else - return video::SColor(); -} - - -//! sets a default color -void GUISkin::setColor(EGUI_DEFAULT_COLOR which, video::SColor newColor) -{ - if ((u32)which < EGDC_COUNT) - Colors[which] = newColor; -} - - -//! returns size for the given size type -s32 GUISkin::getSize(EGUI_DEFAULT_SIZE size) const -{ - if ((u32)size < EGDS_COUNT) - return Sizes[size]; - else - return 0; -} - - -//! sets a default size -void GUISkin::setSize(EGUI_DEFAULT_SIZE which, s32 size) -{ - if ((u32)which < EGDS_COUNT) - Sizes[which] = size; -} - - -//! returns the default font -IGUIFont* GUISkin::getFont(EGUI_DEFAULT_FONT which) const -{ - if (((u32)which < EGDF_COUNT) && Fonts[which]) - return Fonts[which]; - else - return Fonts[EGDF_DEFAULT]; -} - - -//! sets a default font -void GUISkin::setFont(IGUIFont* font, EGUI_DEFAULT_FONT which) -{ - if ((u32)which >= EGDF_COUNT) - return; - - if (font) - { - font->grab(); - if (Fonts[which]) - Fonts[which]->drop(); - - Fonts[which] = font; - } -} - - -//! gets the sprite bank stored -IGUISpriteBank* GUISkin::getSpriteBank() const -{ - return SpriteBank; -} - - -//! set a new sprite bank or remove one by passing 0 -void GUISkin::setSpriteBank(IGUISpriteBank* bank) -{ - if (bank) - bank->grab(); - - if (SpriteBank) - SpriteBank->drop(); - - SpriteBank = bank; -} - - -//! Returns a default icon -u32 GUISkin::getIcon(EGUI_DEFAULT_ICON icon) const -{ - if ((u32)icon < EGDI_COUNT) - return Icons[icon]; - else - return 0; -} - - -//! Sets a default icon -void GUISkin::setIcon(EGUI_DEFAULT_ICON icon, u32 index) -{ - if ((u32)icon < EGDI_COUNT) - Icons[icon] = index; -} - - -//! Returns a default text. For example for Message box button captions: -//! "OK", "Cancel", "Yes", "No" and so on. -const wchar_t* GUISkin::getDefaultText(EGUI_DEFAULT_TEXT text) const -{ - if ((u32)text < EGDT_COUNT) - return Texts[text].c_str(); - else - return Texts[0].c_str(); -} - - -//! Sets a default text. For example for Message box button captions: -//! "OK", "Cancel", "Yes", "No" and so on. -void GUISkin::setDefaultText(EGUI_DEFAULT_TEXT which, const wchar_t* newText) -{ - if ((u32)which < EGDT_COUNT) - Texts[which] = newText; -} - - -//! draws a standard 3d button pane -/** Used for drawing for example buttons in normal state. -It uses the colors EGDC_3D_DARK_SHADOW, EGDC_3D_HIGH_LIGHT, EGDC_3D_SHADOW and -EGDC_3D_FACE for this. See EGUI_DEFAULT_COLOR for details. -\param rect: Defining area where to draw. -\param clip: Clip area. -\param element: Pointer to the element which wishes to draw this. This parameter -is usually not used by ISkin, but can be used for example by more complex -implementations to find out how to draw the part exactly. */ -// PATCH -void GUISkin::drawColored3DButtonPaneStandard(IGUIElement* element, - const core::rect& r, - const core::rect* clip, - const video::SColor* colors) -{ - if (!Driver) - return; - - if (!colors) - colors = Colors; - - core::rect rect = r; - - if ( Type == EGST_BURNING_SKIN ) - { - rect.UpperLeftCorner.X -= 1; - rect.UpperLeftCorner.Y -= 1; - rect.LowerRightCorner.X += 1; - rect.LowerRightCorner.Y += 1; - draw3DSunkenPane(element, - colors[ EGDC_WINDOW ].getInterpolated( 0xFFFFFFFF, 0.9f ) - ,false, true, rect, clip); - return; - } - - Driver->draw2DRectangle(colors[EGDC_3D_DARK_SHADOW], rect, clip); - - rect.LowerRightCorner.X -= 1; - rect.LowerRightCorner.Y -= 1; - Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], rect, clip); - - rect.UpperLeftCorner.X += 1; - rect.UpperLeftCorner.Y += 1; - Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], rect, clip); - - rect.LowerRightCorner.X -= 1; - rect.LowerRightCorner.Y -= 1; - - if (!UseGradient) - { - Driver->draw2DRectangle(colors[EGDC_3D_FACE], rect, clip); - } - else - { - const video::SColor c1 = colors[EGDC_3D_FACE]; - const video::SColor c2 = c1.getInterpolated(colors[EGDC_3D_DARK_SHADOW], 0.4f); - Driver->draw2DRectangle(rect, c1, c1, c2, c2, clip); - } -} -// END PATCH - - -//! draws a pressed 3d button pane -/** Used for drawing for example buttons in pressed state. -It uses the colors EGDC_3D_DARK_SHADOW, EGDC_3D_HIGH_LIGHT, EGDC_3D_SHADOW and -EGDC_3D_FACE for this. See EGUI_DEFAULT_COLOR for details. -\param rect: Defining area where to draw. -\param clip: Clip area. -\param element: Pointer to the element which wishes to draw this. This parameter -is usually not used by ISkin, but can be used for example by more complex -implementations to find out how to draw the part exactly. */ -// PATCH -void GUISkin::drawColored3DButtonPanePressed(IGUIElement* element, - const core::rect& r, - const core::rect* clip, - const video::SColor* colors) -{ - if (!Driver) - return; - - if (!colors) - colors = Colors; - - core::rect rect = r; - Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], rect, clip); - - rect.LowerRightCorner.X -= 1; - rect.LowerRightCorner.Y -= 1; - Driver->draw2DRectangle(colors[EGDC_3D_DARK_SHADOW], rect, clip); - - rect.UpperLeftCorner.X += 1; - rect.UpperLeftCorner.Y += 1; - Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], rect, clip); - - rect.UpperLeftCorner.X += 1; - rect.UpperLeftCorner.Y += 1; - - if (!UseGradient) - { - Driver->draw2DRectangle(colors[EGDC_3D_FACE], rect, clip); - } - else - { - const video::SColor c1 = colors[EGDC_3D_FACE]; - const video::SColor c2 = c1.getInterpolated(colors[EGDC_3D_DARK_SHADOW], 0.4f); - Driver->draw2DRectangle(rect, c1, c1, c2, c2, clip); - } -} -// END PATCH - - -//! draws a sunken 3d pane -/** Used for drawing the background of edit, combo or check boxes. -\param element: Pointer to the element which wishes to draw this. This parameter -is usually not used by ISkin, but can be used for example by more complex -implementations to find out how to draw the part exactly. -\param bgcolor: Background color. -\param flat: Specifies if the sunken pane should be flat or displayed as sunken -deep into the ground. -\param rect: Defining area where to draw. -\param clip: Clip area. */ -// PATCH -void GUISkin::drawColored3DSunkenPane(IGUIElement* element, video::SColor bgcolor, - bool flat, bool fillBackGround, - const core::rect& r, - const core::rect* clip, - const video::SColor* colors) -{ - if (!Driver) - return; - - if (!colors) - colors = Colors; - - core::rect rect = r; - - if (fillBackGround) - Driver->draw2DRectangle(bgcolor, rect, clip); - - if (flat) - { - // draw flat sunken pane - - rect.LowerRightCorner.Y = rect.UpperLeftCorner.Y + 1; - Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], rect, clip); // top - - ++rect.UpperLeftCorner.Y; - rect.LowerRightCorner.Y = r.LowerRightCorner.Y; - rect.LowerRightCorner.X = rect.UpperLeftCorner.X + 1; - Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], rect, clip); // left - - rect = r; - ++rect.UpperLeftCorner.Y; - rect.UpperLeftCorner.X = rect.LowerRightCorner.X - 1; - Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], rect, clip); // right - - rect = r; - ++rect.UpperLeftCorner.X; - rect.UpperLeftCorner.Y = r.LowerRightCorner.Y - 1; - --rect.LowerRightCorner.X; - Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], rect, clip); // bottom - } - else - { - // draw deep sunken pane - rect.LowerRightCorner.Y = rect.UpperLeftCorner.Y + 1; - Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], rect, clip); // top - ++rect.UpperLeftCorner.X; - ++rect.UpperLeftCorner.Y; - --rect.LowerRightCorner.X; - ++rect.LowerRightCorner.Y; - Driver->draw2DRectangle(colors[EGDC_3D_DARK_SHADOW], rect, clip); - - rect.UpperLeftCorner.X = r.UpperLeftCorner.X; - rect.UpperLeftCorner.Y = r.UpperLeftCorner.Y+1; - rect.LowerRightCorner.X = rect.UpperLeftCorner.X + 1; - rect.LowerRightCorner.Y = r.LowerRightCorner.Y; - Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], rect, clip); // left - ++rect.UpperLeftCorner.X; - ++rect.UpperLeftCorner.Y; - ++rect.LowerRightCorner.X; - --rect.LowerRightCorner.Y; - Driver->draw2DRectangle(colors[EGDC_3D_DARK_SHADOW], rect, clip); - - rect = r; - rect.UpperLeftCorner.X = rect.LowerRightCorner.X - 1; - ++rect.UpperLeftCorner.Y; - Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], rect, clip); // right - --rect.UpperLeftCorner.X; - ++rect.UpperLeftCorner.Y; - --rect.LowerRightCorner.X; - --rect.LowerRightCorner.Y; - Driver->draw2DRectangle(colors[EGDC_3D_LIGHT], rect, clip); - - rect = r; - ++rect.UpperLeftCorner.X; - rect.UpperLeftCorner.Y = r.LowerRightCorner.Y - 1; - --rect.LowerRightCorner.X; - Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], rect, clip); // bottom - ++rect.UpperLeftCorner.X; - --rect.UpperLeftCorner.Y; - --rect.LowerRightCorner.X; - --rect.LowerRightCorner.Y; - Driver->draw2DRectangle(colors[EGDC_3D_LIGHT], rect, clip); - } -} -// END PATCH - -//! draws a window background -// return where to draw title bar text. -// PATCH -core::rect GUISkin::drawColored3DWindowBackground(IGUIElement* element, - bool drawTitleBar, video::SColor titleBarColor, - const core::rect& r, - const core::rect* clip, - core::rect* checkClientArea, - const video::SColor* colors) -{ - if (!Driver) - { - if ( checkClientArea ) - { - *checkClientArea = r; - } - return r; - } - - if (!colors) - colors = Colors; - - core::rect rect = r; - - // top border - rect.LowerRightCorner.Y = rect.UpperLeftCorner.Y + 1; - if ( !checkClientArea ) - { - Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], rect, clip); - } - - // left border - rect.LowerRightCorner.Y = r.LowerRightCorner.Y; - rect.LowerRightCorner.X = rect.UpperLeftCorner.X + 1; - if ( !checkClientArea ) - { - Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], rect, clip); - } - - // right border dark outer line - rect.UpperLeftCorner.X = r.LowerRightCorner.X - 1; - rect.LowerRightCorner.X = r.LowerRightCorner.X; - rect.UpperLeftCorner.Y = r.UpperLeftCorner.Y; - rect.LowerRightCorner.Y = r.LowerRightCorner.Y; - if ( !checkClientArea ) - { - Driver->draw2DRectangle(colors[EGDC_3D_DARK_SHADOW], rect, clip); - } - - // right border bright innner line - rect.UpperLeftCorner.X -= 1; - rect.LowerRightCorner.X -= 1; - rect.UpperLeftCorner.Y += 1; - rect.LowerRightCorner.Y -= 1; - if ( !checkClientArea ) - { - Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], rect, clip); - } - - // bottom border dark outer line - rect.UpperLeftCorner.X = r.UpperLeftCorner.X; - rect.UpperLeftCorner.Y = r.LowerRightCorner.Y - 1; - rect.LowerRightCorner.Y = r.LowerRightCorner.Y; - rect.LowerRightCorner.X = r.LowerRightCorner.X; - if ( !checkClientArea ) - { - Driver->draw2DRectangle(colors[EGDC_3D_DARK_SHADOW], rect, clip); - } - - // bottom border bright inner line - rect.UpperLeftCorner.X += 1; - rect.LowerRightCorner.X -= 1; - rect.UpperLeftCorner.Y -= 1; - rect.LowerRightCorner.Y -= 1; - if ( !checkClientArea ) - { - Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], rect, clip); - } - - // client area for background - rect = r; - rect.UpperLeftCorner.X +=1; - rect.UpperLeftCorner.Y +=1; - rect.LowerRightCorner.X -= 2; - rect.LowerRightCorner.Y -= 2; - if (checkClientArea) - { - *checkClientArea = rect; - } - - if ( !checkClientArea ) - { - if (!UseGradient) - { - Driver->draw2DRectangle(colors[EGDC_3D_FACE], rect, clip); - } - else if ( Type == EGST_BURNING_SKIN ) - { - const video::SColor c1 = colors[EGDC_WINDOW].getInterpolated ( 0xFFFFFFFF, 0.9f ); - const video::SColor c2 = colors[EGDC_WINDOW].getInterpolated ( 0xFFFFFFFF, 0.8f ); - - Driver->draw2DRectangle(rect, c1, c1, c2, c2, clip); - } - else - { - const video::SColor c2 = colors[EGDC_3D_SHADOW]; - const video::SColor c1 = colors[EGDC_3D_FACE]; - Driver->draw2DRectangle(rect, c1, c1, c1, c2, clip); - } - } - - // title bar - rect = r; - rect.UpperLeftCorner.X += 2; - rect.UpperLeftCorner.Y += 2; - rect.LowerRightCorner.X -= 2; - rect.LowerRightCorner.Y = rect.UpperLeftCorner.Y + getSize(EGDS_WINDOW_BUTTON_WIDTH) + 2; - - if (drawTitleBar ) - { - if (checkClientArea) - { - (*checkClientArea).UpperLeftCorner.Y = rect.LowerRightCorner.Y; - } - else - { - // draw title bar - //if (!UseGradient) - // Driver->draw2DRectangle(titleBarColor, rect, clip); - //else - if ( Type == EGST_BURNING_SKIN ) - { - const video::SColor c = titleBarColor.getInterpolated( video::SColor(titleBarColor.getAlpha(),255,255,255), 0.8f); - Driver->draw2DRectangle(rect, titleBarColor, titleBarColor, c, c, clip); - } - else - { - const video::SColor c = titleBarColor.getInterpolated(video::SColor(titleBarColor.getAlpha(),0,0,0), 0.2f); - Driver->draw2DRectangle(rect, titleBarColor, c, titleBarColor, c, clip); - } - } - } - - return rect; -} -// END PATCH - - -//! draws a standard 3d menu pane -/** Used for drawing for menus and context menus. -It uses the colors EGDC_3D_DARK_SHADOW, EGDC_3D_HIGH_LIGHT, EGDC_3D_SHADOW and -EGDC_3D_FACE for this. See EGUI_DEFAULT_COLOR for details. -\param element: Pointer to the element which wishes to draw this. This parameter -is usually not used by ISkin, but can be used for example by more complex -implementations to find out how to draw the part exactly. -\param rect: Defining area where to draw. -\param clip: Clip area. */ -// PATCH -void GUISkin::drawColored3DMenuPane(IGUIElement* element, - const core::rect& r, const core::rect* clip, - const video::SColor* colors) -{ - if (!Driver) - return; - - if (!colors) - colors = Colors; - - core::rect rect = r; - - if ( Type == EGST_BURNING_SKIN ) - { - rect.UpperLeftCorner.Y -= 3; - draw3DButtonPaneStandard(element, rect, clip); - return; - } - - // in this skin, this is exactly what non pressed buttons look like, - // so we could simply call - // draw3DButtonPaneStandard(element, rect, clip); - // here. - // but if the skin is transparent, this doesn't look that nice. So - // We draw it a little bit better, with some more draw2DRectangle calls, - // but there aren't that much menus visible anyway. - - rect.LowerRightCorner.Y = rect.UpperLeftCorner.Y + 1; - Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], rect, clip); - - rect.LowerRightCorner.Y = r.LowerRightCorner.Y; - rect.LowerRightCorner.X = rect.UpperLeftCorner.X + 1; - Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], rect, clip); - - rect.UpperLeftCorner.X = r.LowerRightCorner.X - 1; - rect.LowerRightCorner.X = r.LowerRightCorner.X; - rect.UpperLeftCorner.Y = r.UpperLeftCorner.Y; - rect.LowerRightCorner.Y = r.LowerRightCorner.Y; - Driver->draw2DRectangle(colors[EGDC_3D_DARK_SHADOW], rect, clip); - - rect.UpperLeftCorner.X -= 1; - rect.LowerRightCorner.X -= 1; - rect.UpperLeftCorner.Y += 1; - rect.LowerRightCorner.Y -= 1; - Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], rect, clip); - - rect.UpperLeftCorner.X = r.UpperLeftCorner.X; - rect.UpperLeftCorner.Y = r.LowerRightCorner.Y - 1; - rect.LowerRightCorner.Y = r.LowerRightCorner.Y; - rect.LowerRightCorner.X = r.LowerRightCorner.X; - Driver->draw2DRectangle(colors[EGDC_3D_DARK_SHADOW], rect, clip); - - rect.UpperLeftCorner.X += 1; - rect.LowerRightCorner.X -= 1; - rect.UpperLeftCorner.Y -= 1; - rect.LowerRightCorner.Y -= 1; - Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], rect, clip); - - rect = r; - rect.UpperLeftCorner.X +=1; - rect.UpperLeftCorner.Y +=1; - rect.LowerRightCorner.X -= 2; - rect.LowerRightCorner.Y -= 2; - - if (!UseGradient) - Driver->draw2DRectangle(colors[EGDC_3D_FACE], rect, clip); - else - { - const video::SColor c1 = colors[EGDC_3D_FACE]; - const video::SColor c2 = colors[EGDC_3D_SHADOW]; - Driver->draw2DRectangle(rect, c1, c1, c2, c2, clip); - } -} -// END PATCH - - -//! draws a standard 3d tool bar -/** Used for drawing for toolbars and menus. -\param element: Pointer to the element which wishes to draw this. This parameter -is usually not used by ISkin, but can be used for example by more complex -implementations to find out how to draw the part exactly. -\param rect: Defining area where to draw. -\param clip: Clip area. */ -// PATCH -void GUISkin::drawColored3DToolBar(IGUIElement* element, - const core::rect& r, - const core::rect* clip, - const video::SColor* colors) -{ - if (!Driver) - return; - - if (!colors) - colors = Colors; - - core::rect rect = r; - - rect.UpperLeftCorner.X = r.UpperLeftCorner.X; - rect.UpperLeftCorner.Y = r.LowerRightCorner.Y - 1; - rect.LowerRightCorner.Y = r.LowerRightCorner.Y; - rect.LowerRightCorner.X = r.LowerRightCorner.X; - Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], rect, clip); - - rect = r; - rect.LowerRightCorner.Y -= 1; - - if (!UseGradient) - { - Driver->draw2DRectangle(colors[EGDC_3D_FACE], rect, clip); - } - else - if ( Type == EGST_BURNING_SKIN ) - { - const video::SColor c1 = 0xF0000000 | colors[EGDC_3D_FACE].color; - const video::SColor c2 = 0xF0000000 | colors[EGDC_3D_SHADOW].color; - - rect.LowerRightCorner.Y += 1; - Driver->draw2DRectangle(rect, c1, c2, c1, c2, clip); - } - else - { - const video::SColor c1 = colors[EGDC_3D_FACE]; - const video::SColor c2 = colors[EGDC_3D_SHADOW]; - Driver->draw2DRectangle(rect, c1, c1, c2, c2, clip); - } -} -// END PATCH - -//! draws a tab button -/** Used for drawing for tab buttons on top of tabs. -\param element: Pointer to the element which wishes to draw this. This parameter -is usually not used by ISkin, but can be used for example by more complex -implementations to find out how to draw the part exactly. -\param active: Specifies if the tab is currently active. -\param rect: Defining area where to draw. -\param clip: Clip area. */ -// PATCH -void GUISkin::drawColored3DTabButton(IGUIElement* element, bool active, - const core::rect& frameRect, const core::rect* clip, EGUI_ALIGNMENT alignment, - const video::SColor* colors) -{ - if (!Driver) - return; - - if (!colors) - colors = Colors; - - core::rect tr = frameRect; - - if ( alignment == EGUIA_UPPERLEFT ) - { - tr.LowerRightCorner.X -= 2; - tr.LowerRightCorner.Y = tr.UpperLeftCorner.Y + 1; - tr.UpperLeftCorner.X += 1; - Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], tr, clip); - - // draw left highlight - tr = frameRect; - tr.LowerRightCorner.X = tr.UpperLeftCorner.X + 1; - tr.UpperLeftCorner.Y += 1; - Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], tr, clip); - - // draw grey background - tr = frameRect; - tr.UpperLeftCorner.X += 1; - tr.UpperLeftCorner.Y += 1; - tr.LowerRightCorner.X -= 2; - Driver->draw2DRectangle(colors[EGDC_3D_FACE], tr, clip); - - // draw right middle gray shadow - tr.LowerRightCorner.X += 1; - tr.UpperLeftCorner.X = tr.LowerRightCorner.X - 1; - Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], tr, clip); - - tr.LowerRightCorner.X += 1; - tr.UpperLeftCorner.X += 1; - tr.UpperLeftCorner.Y += 1; - Driver->draw2DRectangle(colors[EGDC_3D_DARK_SHADOW], tr, clip); - } - else - { - tr.LowerRightCorner.X -= 2; - tr.UpperLeftCorner.Y = tr.LowerRightCorner.Y - 1; - tr.UpperLeftCorner.X += 1; - Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], tr, clip); - - // draw left highlight - tr = frameRect; - tr.LowerRightCorner.X = tr.UpperLeftCorner.X + 1; - tr.LowerRightCorner.Y -= 1; - Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], tr, clip); - - // draw grey background - tr = frameRect; - tr.UpperLeftCorner.X += 1; - tr.UpperLeftCorner.Y -= 1; - tr.LowerRightCorner.X -= 2; - tr.LowerRightCorner.Y -= 1; - Driver->draw2DRectangle(colors[EGDC_3D_FACE], tr, clip); - - // draw right middle gray shadow - tr.LowerRightCorner.X += 1; - tr.UpperLeftCorner.X = tr.LowerRightCorner.X - 1; - //tr.LowerRightCorner.Y -= 1; - Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], tr, clip); - - tr.LowerRightCorner.X += 1; - tr.UpperLeftCorner.X += 1; - tr.LowerRightCorner.Y -= 1; - Driver->draw2DRectangle(colors[EGDC_3D_DARK_SHADOW], tr, clip); - } -} -// END PATCH - - -//! draws a tab control body -/** \param element: Pointer to the element which wishes to draw this. This parameter -is usually not used by ISkin, but can be used for example by more complex -implementations to find out how to draw the part exactly. -\param border: Specifies if the border should be drawn. -\param background: Specifies if the background should be drawn. -\param rect: Defining area where to draw. -\param clip: Clip area. */ -// PATCH -void GUISkin::drawColored3DTabBody(IGUIElement* element, bool border, bool background, - const core::rect& rect, const core::rect* clip, s32 tabHeight, EGUI_ALIGNMENT alignment, - const video::SColor* colors) -{ - if (!Driver) - return; - - if (!colors) - colors = Colors; - - core::rect tr = rect; - - if ( tabHeight == -1 ) - tabHeight = getSize(gui::EGDS_BUTTON_HEIGHT); - - // draw border. - if (border) - { - if ( alignment == EGUIA_UPPERLEFT ) - { - // draw left hightlight - tr.UpperLeftCorner.Y += tabHeight + 2; - tr.LowerRightCorner.X = tr.UpperLeftCorner.X + 1; - Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], tr, clip); - - // draw right shadow - tr.UpperLeftCorner.X = rect.LowerRightCorner.X - 1; - tr.LowerRightCorner.X = tr.UpperLeftCorner.X + 1; - Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], tr, clip); - - // draw lower shadow - tr = rect; - tr.UpperLeftCorner.Y = tr.LowerRightCorner.Y - 1; - Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], tr, clip); - } - else - { - // draw left hightlight - tr.LowerRightCorner.Y -= tabHeight + 2; - tr.LowerRightCorner.X = tr.UpperLeftCorner.X + 1; - Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], tr, clip); - - // draw right shadow - tr.UpperLeftCorner.X = rect.LowerRightCorner.X - 1; - tr.LowerRightCorner.X = tr.UpperLeftCorner.X + 1; - Driver->draw2DRectangle(colors[EGDC_3D_SHADOW], tr, clip); - - // draw lower shadow - tr = rect; - tr.LowerRightCorner.Y = tr.UpperLeftCorner.Y + 1; - Driver->draw2DRectangle(colors[EGDC_3D_HIGH_LIGHT], tr, clip); - } - } - - if (background) - { - if ( alignment == EGUIA_UPPERLEFT ) - { - tr = rect; - tr.UpperLeftCorner.Y += tabHeight + 2; - tr.LowerRightCorner.X -= 1; - tr.UpperLeftCorner.X += 1; - tr.LowerRightCorner.Y -= 1; - } - else - { - tr = rect; - tr.UpperLeftCorner.X += 1; - tr.UpperLeftCorner.Y -= 1; - tr.LowerRightCorner.X -= 1; - tr.LowerRightCorner.Y -= tabHeight + 2; - //tr.UpperLeftCorner.X += 1; - } - - if (!UseGradient) - Driver->draw2DRectangle(colors[EGDC_3D_FACE], tr, clip); - else - { - video::SColor c1 = colors[EGDC_3D_FACE]; - video::SColor c2 = colors[EGDC_3D_SHADOW]; - Driver->draw2DRectangle(tr, c1, c1, c2, c2, clip); - } - } -} -// END PATCH - - -//! draws an icon, usually from the skin's sprite bank -/** \param parent: Pointer to the element which wishes to draw this icon. -This parameter is usually not used by IGUISkin, but can be used for example -by more complex implementations to find out how to draw the part exactly. -\param icon: Specifies the icon to be drawn. -\param position: The position to draw the icon -\param starttime: The time at the start of the animation -\param currenttime: The present time, used to calculate the frame number -\param loop: Whether the animation should loop or not -\param clip: Clip area. */ -// PATCH -void GUISkin::drawColoredIcon(IGUIElement* element, EGUI_DEFAULT_ICON icon, - const core::position2di position, - u32 starttime, u32 currenttime, - bool loop, const core::rect* clip, - const video::SColor* colors) -{ - if (!SpriteBank) - return; - - if (!colors) - colors = Colors; - - bool gray = element && !element->isEnabled(); - SpriteBank->draw2DSprite(Icons[icon], position, clip, - colors[gray? EGDC_GRAY_WINDOW_SYMBOL : EGDC_WINDOW_SYMBOL], starttime, currenttime, loop, true); -} -// END PATCH - - -EGUI_SKIN_TYPE GUISkin::getType() const -{ - return Type; -} - - -//! draws a 2d rectangle. -void GUISkin::draw2DRectangle(IGUIElement* element, - const video::SColor &color, const core::rect& pos, - const core::rect* clip) -{ - Driver->draw2DRectangle(color, pos, clip); -} - - -//! gets the colors -// PATCH -void GUISkin::getColors(video::SColor* colors) -{ - u32 i; - for (i=0; i -#include "ITexture.h" - -namespace irr -{ -namespace video -{ - class IVideoDriver; -} -namespace gui -{ - class GUISkin : public IGUISkin - { - public: - - GUISkin(EGUI_SKIN_TYPE type, video::IVideoDriver* driver); - - //! destructor - virtual ~GUISkin(); - - //! returns display density scaling factor - virtual float getScale() const { return Scale; } - - //! sets display density scaling factor - virtual void setScale(float scale) { Scale = scale; } - - //! returns default color - virtual video::SColor getColor(EGUI_DEFAULT_COLOR color) const; - - //! sets a default color - virtual void setColor(EGUI_DEFAULT_COLOR which, video::SColor newColor); - - //! returns size for the given size type - virtual s32 getSize(EGUI_DEFAULT_SIZE size) const; - - //! sets a default size - virtual void setSize(EGUI_DEFAULT_SIZE which, s32 size); - - //! returns the default font - virtual IGUIFont* getFont(EGUI_DEFAULT_FONT which=EGDF_DEFAULT) const; - - //! sets a default font - virtual void setFont(IGUIFont* font, EGUI_DEFAULT_FONT which=EGDF_DEFAULT); - - //! sets the sprite bank used for drawing icons - virtual void setSpriteBank(IGUISpriteBank* bank); - - //! gets the sprite bank used for drawing icons - virtual IGUISpriteBank* getSpriteBank() const; - - //! Returns a default icon - /** Returns the sprite index within the sprite bank */ - virtual u32 getIcon(EGUI_DEFAULT_ICON icon) const; - - //! Sets a default icon - /** Sets the sprite index used for drawing icons like arrows, - close buttons and ticks in checkboxes - \param icon: Enum specifying which icon to change - \param index: The sprite index used to draw this icon */ - virtual void setIcon(EGUI_DEFAULT_ICON icon, u32 index); - - //! Returns a default text. - /** For example for Message box button captions: - "OK", "Cancel", "Yes", "No" and so on. */ - virtual const wchar_t* getDefaultText(EGUI_DEFAULT_TEXT text) const; - - //! Sets a default text. - /** For example for Message box button captions: - "OK", "Cancel", "Yes", "No" and so on. */ - virtual void setDefaultText(EGUI_DEFAULT_TEXT which, const wchar_t* newText); - - //! draws a standard 3d button pane - /** Used for drawing for example buttons in normal state. - It uses the colors EGDC_3D_DARK_SHADOW, EGDC_3D_HIGH_LIGHT, EGDC_3D_SHADOW and - EGDC_3D_FACE for this. See EGUI_DEFAULT_COLOR for details. - \param rect: Defining area where to draw. - \param clip: Clip area. - \param element: Pointer to the element which wishes to draw this. This parameter - is usually not used by ISkin, but can be used for example by more complex - implementations to find out how to draw the part exactly. */ - virtual void draw3DButtonPaneStandard(IGUIElement* element, - const core::rect& rect, - const core::rect* clip=0) - { - drawColored3DButtonPaneStandard(element, rect,clip); - } - - virtual void drawColored3DButtonPaneStandard(IGUIElement* element, - const core::rect& rect, - const core::rect* clip=0, - const video::SColor* colors=0); - - //! draws a pressed 3d button pane - /** Used for drawing for example buttons in pressed state. - It uses the colors EGDC_3D_DARK_SHADOW, EGDC_3D_HIGH_LIGHT, EGDC_3D_SHADOW and - EGDC_3D_FACE for this. See EGUI_DEFAULT_COLOR for details. - \param rect: Defining area where to draw. - \param clip: Clip area. - \param element: Pointer to the element which wishes to draw this. This parameter - is usually not used by ISkin, but can be used for example by more complex - implementations to find out how to draw the part exactly. */ - virtual void draw3DButtonPanePressed(IGUIElement* element, - const core::rect& rect, - const core::rect* clip=0) - { - drawColored3DButtonPanePressed(element, rect, clip); - } - - virtual void drawColored3DButtonPanePressed(IGUIElement* element, - const core::rect& rect, - const core::rect* clip=0, - const video::SColor* colors=0); - - //! draws a sunken 3d pane - /** Used for drawing the background of edit, combo or check boxes. - \param element: Pointer to the element which wishes to draw this. This parameter - is usually not used by ISkin, but can be used for example by more complex - implementations to find out how to draw the part exactly. - \param bgcolor: Background color. - \param flat: Specifies if the sunken pane should be flat or displayed as sunken - deep into the ground. - \param rect: Defining area where to draw. - \param clip: Clip area. */ - virtual void draw3DSunkenPane(IGUIElement* element, - video::SColor bgcolor, bool flat, - bool fillBackGround, - const core::rect& rect, - const core::rect* clip=0) - { - drawColored3DSunkenPane(element, bgcolor, flat, fillBackGround, rect, clip); - } - - virtual void drawColored3DSunkenPane(IGUIElement* element, - video::SColor bgcolor, bool flat, - bool fillBackGround, - const core::rect& rect, - const core::rect* clip=0, - const video::SColor* colors=0); - - //! draws a window background - /** Used for drawing the background of dialogs and windows. - \param element: Pointer to the element which wishes to draw this. This parameter - is usually not used by ISkin, but can be used for example by more complex - implementations to find out how to draw the part exactly. - \param titleBarColor: Title color. - \param drawTitleBar: True to enable title drawing. - \param rect: Defining area where to draw. - \param clip: Clip area. - \param checkClientArea: When set to non-null the function will not draw anything, - but will instead return the clientArea which can be used for drawing by the calling window. - That is the area without borders and without titlebar. - \return Returns rect where it would be good to draw title bar text. This will - work even when checkClientArea is set to a non-null value.*/ - virtual core::rect draw3DWindowBackground(IGUIElement* element, - bool drawTitleBar, video::SColor titleBarColor, - const core::rect& rect, - const core::rect* clip, - core::rect* checkClientArea) - { - return drawColored3DWindowBackground(element, drawTitleBar, titleBarColor, - rect, clip, checkClientArea); - } - - virtual core::rect drawColored3DWindowBackground(IGUIElement* element, - bool drawTitleBar, video::SColor titleBarColor, - const core::rect& rect, - const core::rect* clip, - core::rect* checkClientArea, - const video::SColor* colors=0); - - //! draws a standard 3d menu pane - /** Used for drawing for menus and context menus. - It uses the colors EGDC_3D_DARK_SHADOW, EGDC_3D_HIGH_LIGHT, EGDC_3D_SHADOW and - EGDC_3D_FACE for this. See EGUI_DEFAULT_COLOR for details. - \param element: Pointer to the element which wishes to draw this. This parameter - is usually not used by ISkin, but can be used for example by more complex - implementations to find out how to draw the part exactly. - \param rect: Defining area where to draw. - \param clip: Clip area. */ - virtual void draw3DMenuPane(IGUIElement* element, - const core::rect& rect, - const core::rect* clip=0) - { - drawColored3DMenuPane(element, rect, clip); - } - - virtual void drawColored3DMenuPane(IGUIElement* element, - const core::rect& rect, - const core::rect* clip=0, - const video::SColor* colors=0); - - //! draws a standard 3d tool bar - /** Used for drawing for toolbars and menus. - \param element: Pointer to the element which wishes to draw this. This parameter - is usually not used by ISkin, but can be used for example by more complex - implementations to find out how to draw the part exactly. - \param rect: Defining area where to draw. - \param clip: Clip area. */ - virtual void draw3DToolBar(IGUIElement* element, - const core::rect& rect, - const core::rect* clip=0) - { - drawColored3DToolBar(element, rect, clip); - } - - virtual void drawColored3DToolBar(IGUIElement* element, - const core::rect& rect, - const core::rect* clip=0, - const video::SColor* colors=0); - - //! draws a tab button - /** Used for drawing for tab buttons on top of tabs. - \param element: Pointer to the element which wishes to draw this. This parameter - is usually not used by ISkin, but can be used for example by more complex - implementations to find out how to draw the part exactly. - \param active: Specifies if the tab is currently active. - \param rect: Defining area where to draw. - \param clip: Clip area. */ - virtual void draw3DTabButton(IGUIElement* element, bool active, - const core::rect& rect, const core::rect* clip=0, EGUI_ALIGNMENT alignment=EGUIA_UPPERLEFT) - { - drawColored3DTabButton(element, active, rect, clip, alignment); - } - - virtual void drawColored3DTabButton(IGUIElement* element, bool active, - const core::rect& rect, const core::rect* clip=0, EGUI_ALIGNMENT alignment=EGUIA_UPPERLEFT, - const video::SColor* colors=0); - - //! draws a tab control body - /** \param element: Pointer to the element which wishes to draw this. This parameter - is usually not used by ISkin, but can be used for example by more complex - implementations to find out how to draw the part exactly. - \param border: Specifies if the border should be drawn. - \param background: Specifies if the background should be drawn. - \param rect: Defining area where to draw. - \param clip: Clip area. */ - virtual void draw3DTabBody(IGUIElement* element, bool border, bool background, - const core::rect& rect, const core::rect* clip=0, s32 tabHeight=-1, EGUI_ALIGNMENT alignment=EGUIA_UPPERLEFT) - { - drawColored3DTabBody(element, border, background, rect, clip, tabHeight, alignment); - } - - virtual void drawColored3DTabBody(IGUIElement* element, bool border, bool background, - const core::rect& rect, const core::rect* clip=0, s32 tabHeight=-1, EGUI_ALIGNMENT alignment=EGUIA_UPPERLEFT, - const video::SColor* colors=0); - - //! draws an icon, usually from the skin's sprite bank - /** \param element: Pointer to the element which wishes to draw this icon. - This parameter is usually not used by IGUISkin, but can be used for example - by more complex implementations to find out how to draw the part exactly. - \param icon: Specifies the icon to be drawn. - \param position: The position to draw the icon - \param starttime: The time at the start of the animation - \param currenttime: The present time, used to calculate the frame number - \param loop: Whether the animation should loop or not - \param clip: Clip area. */ - virtual void drawIcon(IGUIElement* element, EGUI_DEFAULT_ICON icon, - const core::position2di position, - u32 starttime=0, u32 currenttime=0, - bool loop=false, const core::rect* clip=0) - { - drawColoredIcon(element, icon, position, starttime, currenttime, loop, clip); - } - - virtual void drawColoredIcon(IGUIElement* element, EGUI_DEFAULT_ICON icon, - const core::position2di position, - u32 starttime=0, u32 currenttime=0, - bool loop=false, const core::rect* clip=0, - const video::SColor* colors=0); - - //! draws a 2d rectangle. - /** \param element: Pointer to the element which wishes to draw this icon. - This parameter is usually not used by IGUISkin, but can be used for example - by more complex implementations to find out how to draw the part exactly. - \param color: Color of the rectangle to draw. The alpha component specifies how - transparent the rectangle will be. - \param pos: Position of the rectangle. - \param clip: Pointer to rectangle against which the rectangle will be clipped. - If the pointer is null, no clipping will be performed. */ - virtual void draw2DRectangle(IGUIElement* element, const video::SColor &color, - const core::rect& pos, const core::rect* clip = 0); - - - //! get the type of this skin - virtual EGUI_SKIN_TYPE getType() const; - - //! gets the colors - virtual void getColors(video::SColor* colors); // ::PATCH: - - private: - - float Scale = 1.0f; - video::SColor Colors[EGDC_COUNT]; - s32 Sizes[EGDS_COUNT]; - u32 Icons[EGDI_COUNT]; - IGUIFont* Fonts[EGDF_COUNT]; - IGUISpriteBank* SpriteBank; - core::stringw Texts[EGDT_COUNT]; - video::IVideoDriver* Driver; - bool UseGradient; - - EGUI_SKIN_TYPE Type; - }; - - #define set3DSkinColors(skin, button_color) \ - { \ - skin->setColor(EGDC_3D_FACE, button_color); \ - skin->setColor(EGDC_3D_DARK_SHADOW, button_color, 0.25f); \ - skin->setColor(EGDC_3D_SHADOW, button_color, 0.5f); \ - skin->setColor(EGDC_3D_LIGHT, button_color); \ - skin->setColor(EGDC_3D_HIGH_LIGHT, button_color, 1.5f); \ - } - - #define getElementSkinColor(color) \ - { \ - if (!Colors) \ - { \ - IGUISkin* skin = Environment->getSkin(); \ - if (skin) \ - return skin->getColor(color); \ - } \ - return Colors[color]; \ - } - - #define setElementSkinColor(which, newColor, shading) \ - { \ - if (!Colors) \ - { \ - Colors = new video::SColor[EGDC_COUNT]; \ - GUISkin* skin = (GUISkin *)Environment->getSkin(); \ - if (skin) \ - skin->getColors(Colors); \ - } \ - Colors[which] = newColor; \ - setShading(Colors[which],shading); \ - } -} // end namespace gui -//! Sets the shading -inline void setShading(video::SColor &color,f32 s) // :PATCH: -{ - if (s < 1.0f) - { - color.setRed(color.getRed() * s); - color.setGreen(color.getGreen() * s); - color.setBlue(color.getBlue() * s); - } - else if (s > 1.0f) - { - s -= 1.0f; - - color.setRed(color.getRed() + (255 - color.getRed()) * s); - color.setGreen(color.getGreen() + (255 - color.getGreen()) * s); - color.setBlue(color.getBlue() + (255 - color.getBlue()) * s); - } -} -} // end namespace irr - -#endif diff --git a/src/gui/guiVolumeChange.cpp b/src/gui/guiVolumeChange.cpp index a6608dd18..4d1139dce 100644 --- a/src/gui/guiVolumeChange.cpp +++ b/src/gui/guiVolumeChange.cpp @@ -27,6 +27,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #include #include #include +#include #include "settings.h" #include "gettext.h" diff --git a/src/gui/modalMenu.cpp b/src/gui/modalMenu.cpp index 83d5036f9..ad4839170 100644 --- a/src/gui/modalMenu.cpp +++ b/src/gui/modalMenu.cpp @@ -28,7 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "gui/guiInventoryList.h" #include "porting.h" #include "settings.h" -#include "touchscreengui.h" +#include "touchcontrols.h" PointerAction PointerAction::fromEvent(const SEvent &event) { switch (event.EventType) { @@ -111,8 +111,8 @@ void GUIModalMenu::quitMenu() Environment->removeFocus(this); m_menumgr->deletingMenu(this); this->remove(); - if (g_touchscreengui) - g_touchscreengui->show(); + if (g_touchcontrols) + g_touchcontrols->show(); } static bool isChild(gui::IGUIElement *tocheck, gui::IGUIElement *parent) @@ -187,6 +187,7 @@ bool GUIModalMenu::simulateMouseEvent(ETOUCH_INPUT_EVENT touch_event, bool secon mouse_event.EventType = EET_MOUSE_INPUT_EVENT; mouse_event.MouseInput.X = m_pointer.X; mouse_event.MouseInput.Y = m_pointer.Y; + mouse_event.MouseInput.Simulated = true; switch (touch_event) { case ETIE_PRESSED_DOWN: mouse_event.MouseInput.Event = EMIE_LMOUSE_PRESSED_DOWN; @@ -210,7 +211,6 @@ bool GUIModalMenu::simulateMouseEvent(ETOUCH_INPUT_EVENT touch_event, bool secon } bool retval; - m_simulated_mouse = true; do { if (preprocessEvent(mouse_event)) { retval = true; @@ -222,7 +222,6 @@ bool GUIModalMenu::simulateMouseEvent(ETOUCH_INPUT_EVENT touch_event, bool secon } retval = target->OnEvent(mouse_event); } while (false); - m_simulated_mouse = false; if (!retval && !second_try) return simulateMouseEvent(touch_event, true); @@ -330,7 +329,6 @@ bool GUIModalMenu::preprocessEvent(const SEvent &event) holder.grab(this); // keep this alive until return (it might be dropped downstream [?]) if (event.TouchInput.touchedCount == 1) { - m_pointer_type = PointerType::Touch; m_pointer = v2s32(event.TouchInput.X, event.TouchInput.Y); gui::IGUIElement *hovered = Environment->getRootGUIElement()->getElementFromPoint(core::position2d(m_pointer)); @@ -373,9 +371,8 @@ bool GUIModalMenu::preprocessEvent(const SEvent &event) } if (event.EventType == EET_MOUSE_INPUT_EVENT) { - if (!m_simulated_mouse) { - // Only set the pointer type to mouse if this is a real mouse event. - m_pointer_type = PointerType::Mouse; + if (!event.MouseInput.Simulated) { + // Only process if this is a real mouse event. m_pointer = v2s32(event.MouseInput.X, event.MouseInput.Y); m_touch_hovered.reset(); } diff --git a/src/gui/modalMenu.h b/src/gui/modalMenu.h index 071024120..2f770f9f5 100644 --- a/src/gui/modalMenu.h +++ b/src/gui/modalMenu.h @@ -26,11 +26,6 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #endif -enum class PointerType { - Mouse, - Touch, -}; - struct PointerAction { v2s32 pos; u64 time; // ms @@ -74,14 +69,10 @@ public: porting::AndroidDialogState getAndroidUIInputState(); #endif - PointerType getPointerType() { return m_pointer_type; }; - protected: virtual std::wstring getLabelByID(s32 id) = 0; virtual std::string getNameByID(s32 id) = 0; - // Stores the last known pointer type. - PointerType m_pointer_type = PointerType::Mouse; // Stores the last known pointer position. // If the last input event was a mouse event, it's the cursor position. // If the last input event was a touch event, it's the finger position. @@ -102,9 +93,6 @@ protected: // This is set to true if the menu is currently processing a second-touch event. bool m_second_touch = false; - // This is set to true if the menu is currently processing a mouse event - // that was synthesized by the menu itself from a touch event. - bool m_simulated_mouse = false; private: IMenuManager *m_menumgr; diff --git a/src/gui/profilergraph.cpp b/src/gui/profilergraph.cpp index e6fdf9ae8..ab4796cb9 100644 --- a/src/gui/profilergraph.cpp +++ b/src/gui/profilergraph.cpp @@ -20,6 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "porting.h" #include "profilergraph.h" +#include "IVideoDriver.h" #include "util/string.h" void ProfilerGraph::put(const Profiler::GraphValues &values) diff --git a/src/gui/profilergraph.h b/src/gui/profilergraph.h index 6354ac9ef..c92cbf837 100644 --- a/src/gui/profilergraph.h +++ b/src/gui/profilergraph.h @@ -23,9 +23,12 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include -#include #include "profiler.h" +namespace irr::video { + class IVideoDriver; +} + /* Profiler display */ class ProfilerGraph { diff --git a/src/gui/touchscreengui.cpp b/src/gui/touchcontrols.cpp similarity index 91% rename from src/gui/touchscreengui.cpp rename to src/gui/touchcontrols.cpp index c428ef52e..f3301a64d 100644 --- a/src/gui/touchscreengui.cpp +++ b/src/gui/touchcontrols.cpp @@ -20,7 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "touchscreengui.h" +#include "touchcontrols.h" #include "gettime.h" #include "irr_v2d.h" @@ -34,12 +34,13 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "gettext.h" #include "IGUIStaticText.h" #include "IGUIFont.h" +#include #include #include #include -TouchScreenGUI *g_touchscreengui; +TouchControls *g_touchcontrols; static const char *button_image_names[] = { "jump_btn.png", @@ -266,14 +267,14 @@ static EKEY_CODE id_to_keycode(touch_gui_button_id id) code = keyname_to_keycode(resolved.c_str()); } catch (UnknownKeycode &e) { code = KEY_UNKNOWN; - warningstream << "TouchScreenGUI: Unknown key '" << resolved + warningstream << "TouchControls: Unknown key '" << resolved << "' for '" << key << "', hiding button." << std::endl; } return code; } -TouchScreenGUI::TouchScreenGUI(IrrlichtDevice *device, ISimpleTextureSource *tsrc): +TouchControls::TouchControls(IrrlichtDevice *device, ISimpleTextureSource *tsrc): m_device(device), m_guienv(device->getGUIEnvironment()), m_receiver(device->getEventReceiver()), @@ -411,9 +412,18 @@ TouchScreenGUI::TouchScreenGUI(IrrlichtDevice *device, ISimpleTextureSource *tsr pos.X += spacing.X; } + + m_status_text = grab_gui_element( + m_guienv->addStaticText(L"", recti(), false, false)); + m_status_text->setVisible(false); } -void TouchScreenGUI::addButton(std::vector &buttons, touch_gui_button_id id, +TouchControls::~TouchControls() +{ + releaseAll(); +} + +void TouchControls::addButton(std::vector &buttons, touch_gui_button_id id, const std::string &image, const recti &rect, bool visible) { IGUIImage *btn_gui_button = m_guienv->addImage(rect, nullptr, id); @@ -426,7 +436,7 @@ void TouchScreenGUI::addButton(std::vector &buttons, touch_gui_butt btn.gui_button = grab_gui_element(btn_gui_button); } -void TouchScreenGUI::addToggleButton(std::vector &buttons, touch_gui_button_id id, +void TouchControls::addToggleButton(std::vector &buttons, touch_gui_button_id id, const std::string &image_1, const std::string &image_2, const recti &rect, bool visible) { addButton(buttons, id, image_1, rect, visible); @@ -436,7 +446,7 @@ void TouchScreenGUI::addToggleButton(std::vector &buttons, touch_gu btn.toggle_textures[1] = image_2; } -IGUIImage *TouchScreenGUI::makeButtonDirect(touch_gui_button_id id, +IGUIImage *TouchControls::makeButtonDirect(touch_gui_button_id id, const recti &rect, bool visible) { IGUIImage *btn_gui_button = m_guienv->addImage(rect, nullptr, id); @@ -447,7 +457,7 @@ IGUIImage *TouchScreenGUI::makeButtonDirect(touch_gui_button_id id, return btn_gui_button; } -bool TouchScreenGUI::isHotbarButton(const SEvent &event) +bool TouchControls::isHotbarButton(const SEvent &event) { const v2s32 touch_pos = v2s32(event.TouchInput.X, event.TouchInput.Y); // check if hotbar item is pressed @@ -462,30 +472,27 @@ bool TouchScreenGUI::isHotbarButton(const SEvent &event) return false; } -std::optional TouchScreenGUI::getHotbarSelection() +std::optional TouchControls::getHotbarSelection() { auto selection = m_hotbar_selection; m_hotbar_selection = std::nullopt; return selection; } -void TouchScreenGUI::handleReleaseEvent(size_t pointer_id) +void TouchControls::handleReleaseEvent(size_t pointer_id) { // By the way: Android reuses pointer IDs, so m_pointer_pos[pointer_id] // will be overwritten soon anyway. m_pointer_downpos.erase(pointer_id); m_pointer_pos.erase(pointer_id); - if (m_overflow_open) { - buttons_handleRelease(m_overflow_buttons, pointer_id, m_device->getVideoDriver(), - m_receiver, m_texturesource); - return; - } - // handle buttons if (buttons_handleRelease(m_buttons, pointer_id, m_device->getVideoDriver(), m_receiver, m_texturesource)) return; + if (buttons_handleRelease(m_overflow_buttons, pointer_id, m_device->getVideoDriver(), + m_receiver, m_texturesource)) + return; if (m_has_move_id && pointer_id == m_move_id) { // handle the point used for moving view @@ -512,19 +519,17 @@ void TouchScreenGUI::handleReleaseEvent(size_t pointer_id) m_joystick_status_aux1 = false; applyJoystickStatus(); - m_joystick_btn_off->setVisible(true); - m_joystick_btn_bg->setVisible(false); - m_joystick_btn_center->setVisible(false); + updateVisibility(); } else { - infostream << "TouchScreenGUI::translateEvent released unknown button: " + infostream << "TouchControls::translateEvent released unknown button: " << pointer_id << std::endl; } } -void TouchScreenGUI::translateEvent(const SEvent &event) +void TouchControls::translateEvent(const SEvent &event) { if (!m_visible) { - infostream << "TouchScreenGUI::translateEvent got event but is not visible!" + infostream << "TouchControls::translateEvent got event but is not visible!" << std::endl; return; } @@ -571,9 +576,6 @@ void TouchScreenGUI::translateEvent(const SEvent &event) toggleOverflowMenu(); // refresh since visibility of buttons has changed element = m_guienv->getRootGUIElement()->getElementFromPoint(touch_pos); - // restore after releaseAll in toggleOverflowMenu - m_pointer_downpos[pointer_id] = touch_pos; - m_pointer_pos[pointer_id] = touch_pos; // continue processing, but avoid accidentally placing a node // when closing the overflow menu prevent_short_tap = true; @@ -599,9 +601,7 @@ void TouchScreenGUI::translateEvent(const SEvent &event) m_joystick_id = pointer_id; m_joystick_has_really_moved = false; - m_joystick_btn_off->setVisible(false); - m_joystick_btn_bg->setVisible(true); - m_joystick_btn_center->setVisible(true); + updateVisibility(); // If it's a fixed joystick, don't move the joystick "button". if (!m_fixed_joystick) @@ -632,9 +632,6 @@ void TouchScreenGUI::translateEvent(const SEvent &event) } else { assert(event.TouchInput.Event == ETIE_MOVED); - if (m_overflow_open) - return; - if (!(m_has_joystick_id && m_fixed_joystick) && m_pointer_pos[event.TouchInput.ID] == touch_pos) return; @@ -702,7 +699,7 @@ void TouchScreenGUI::translateEvent(const SEvent &event) } } -void TouchScreenGUI::applyJoystickStatus() +void TouchControls::applyJoystickStatus() { if (m_joystick_triggers_aux1) { SEvent translated{}; @@ -718,15 +715,11 @@ void TouchScreenGUI::applyJoystickStatus() } } -void TouchScreenGUI::step(float dtime) +void TouchControls::step(float dtime) { - if (m_overflow_open) { - buttons_step(m_overflow_buttons, dtime, m_device->getVideoDriver(), m_receiver, m_texturesource); - return; - } - // simulate keyboard repeats buttons_step(m_buttons, dtime, m_device->getVideoDriver(), m_receiver, m_texturesource); + buttons_step(m_overflow_buttons, dtime, m_device->getVideoDriver(), m_receiver, m_texturesource); // joystick applyJoystickStatus(); @@ -757,23 +750,22 @@ void TouchScreenGUI::step(float dtime) m_had_move_id = false; } -void TouchScreenGUI::resetHotbarRects() +void TouchControls::resetHotbarRects() { m_hotbar_rects.clear(); } -void TouchScreenGUI::registerHotbarRect(u16 index, const recti &rect) +void TouchControls::registerHotbarRect(u16 index, const recti &rect) { m_hotbar_rects[index] = rect; } -void TouchScreenGUI::setVisible(bool visible) +void TouchControls::setVisible(bool visible) { if (m_visible == visible) return; m_visible = visible; - // order matters if (!visible) { releaseAll(); m_overflow_open = false; @@ -781,20 +773,24 @@ void TouchScreenGUI::setVisible(bool visible) updateVisibility(); } -void TouchScreenGUI::toggleOverflowMenu() +void TouchControls::toggleOverflowMenu() { - releaseAll(); // must be done first + // no releaseAll here so that you can e.g. continue holding the joystick + // while the overflow menu is open m_overflow_open = !m_overflow_open; updateVisibility(); } -void TouchScreenGUI::updateVisibility() +void TouchControls::updateVisibility() { bool regular_visible = m_visible && !m_overflow_open; for (auto &button : m_buttons) button.gui_button->setVisible(regular_visible); m_overflow_btn->setVisible(regular_visible); - m_joystick_btn_off->setVisible(regular_visible); + + m_joystick_btn_off->setVisible(regular_visible && !m_has_joystick_id); + m_joystick_btn_bg->setVisible(regular_visible && m_has_joystick_id); + m_joystick_btn_center->setVisible(regular_visible && m_has_joystick_id); bool overflow_visible = m_visible && m_overflow_open; m_overflow_bg->setVisible(overflow_visible); @@ -804,7 +800,7 @@ void TouchScreenGUI::updateVisibility() text->setVisible(overflow_visible); } -void TouchScreenGUI::releaseAll() +void TouchControls::releaseAll() { while (!m_pointer_pos.empty()) handleReleaseEvent(m_pointer_pos.begin()->first); @@ -821,17 +817,17 @@ void TouchScreenGUI::releaseAll() } } -void TouchScreenGUI::hide() +void TouchControls::hide() { setVisible(false); } -void TouchScreenGUI::show() +void TouchControls::show() { setVisible(true); } -v2s32 TouchScreenGUI::getPointerPos() +v2s32 TouchControls::getPointerPos() { if (m_draw_crosshair) return v2s32(m_screensize.X / 2, m_screensize.Y / 2); @@ -840,7 +836,7 @@ v2s32 TouchScreenGUI::getPointerPos() return m_move_pos; } -void TouchScreenGUI::emitMouseEvent(EMOUSE_INPUT_EVENT type) +void TouchControls::emitMouseEvent(EMOUSE_INPUT_EVENT type) { v2s32 pointer_pos = getPointerPos(); @@ -852,10 +848,11 @@ void TouchScreenGUI::emitMouseEvent(EMOUSE_INPUT_EVENT type) event.MouseInput.Control = false; event.MouseInput.ButtonStates = 0; event.MouseInput.Event = type; + event.MouseInput.Simulated = true; m_receiver->OnEvent(event); } -void TouchScreenGUI::applyContextControls(const TouchInteractionMode &mode) +void TouchControls::applyContextControls(const TouchInteractionMode &mode) { // Since the pointed thing has already been determined when this function // is called, we cannot use this function to update the shootline. diff --git a/src/gui/touchscreengui.h b/src/gui/touchcontrols.h similarity index 92% rename from src/gui/touchscreengui.h rename to src/gui/touchcontrols.h index 2da9d8151..98ec806bd 100644 --- a/src/gui/touchscreengui.h +++ b/src/gui/touchcontrols.h @@ -25,7 +25,6 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include -#include #include #include @@ -34,6 +33,12 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "itemdef.h" #include "client/game.h" +#include "util/basic_macros.h" + +namespace irr +{ + class IrrlichtDevice; +} using namespace irr; using namespace irr::core; @@ -128,10 +133,12 @@ struct button_info }; -class TouchScreenGUI +class TouchControls { public: - TouchScreenGUI(IrrlichtDevice *device, ISimpleTextureSource *tsrc); + TouchControls(IrrlichtDevice *device, ISimpleTextureSource *tsrc); + ~TouchControls(); + DISABLE_CLASS_COPY(TouchControls); void translateEvent(const SEvent &event); void applyContextControls(const TouchInteractionMode &mode); @@ -159,8 +166,8 @@ public: */ line3d getShootline() { return m_shootline; } - float getMovementDirection() { return m_joystick_direction; } - float getMovementSpeed() { return m_joystick_speed; } + float getJoystickDirection() { return m_joystick_direction; } + float getJoystickSpeed() { return m_joystick_speed; } void step(float dtime); inline void setUseCrosshair(bool use_crosshair) { m_draw_crosshair = use_crosshair; } @@ -173,6 +180,9 @@ public: void registerHotbarRect(u16 index, const recti &rect); std::optional getHotbarSelection(); + bool isStatusTextOverriden() { return m_overflow_open; } + IGUIStaticText *getStatusText() { return m_status_text.get(); } + private: IrrlichtDevice *m_device = nullptr; IGUIEnvironment *m_guienv = nullptr; @@ -182,7 +192,7 @@ private: s32 m_button_size; double m_touchscreen_threshold; u16 m_long_tap_delay; - bool m_visible = true; // is the whole touch screen gui visible + bool m_visible = true; std::unordered_map m_hotbar_rects; std::optional m_hotbar_selection = std::nullopt; @@ -231,6 +241,8 @@ private: std::vector> m_overflow_button_titles; std::vector m_overflow_button_rects; + std::shared_ptr m_status_text; + void toggleOverflowMenu(); void updateVisibility(); void releaseAll(); @@ -273,4 +285,4 @@ private: u64 m_place_pressed_until = 0; }; -extern TouchScreenGUI *g_touchscreengui; +extern TouchControls *g_touchcontrols; diff --git a/src/hud.cpp b/src/hud.cpp index 9b336cdef..e66eff1e5 100644 --- a/src/hud.cpp +++ b/src/hud.cpp @@ -30,6 +30,7 @@ const struct EnumString es_HudElementType[] = {HUD_ELEM_IMAGE_WAYPOINT, "image_waypoint"}, {HUD_ELEM_COMPASS, "compass"}, {HUD_ELEM_MINIMAP, "minimap"}, + {HUD_ELEM_HOTBAR, "hotbar"}, {0, NULL}, }; diff --git a/src/hud.h b/src/hud.h index 5540828d4..ac79aa750 100644 --- a/src/hud.h +++ b/src/hud.h @@ -20,7 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once -#include "irrlichttypes_extrabloated.h" +#include "irrlichttypes_bloated.h" #include #include "common/c_types.h" @@ -67,7 +67,8 @@ enum HudElementType { HUD_ELEM_WAYPOINT = 4, HUD_ELEM_IMAGE_WAYPOINT = 5, HUD_ELEM_COMPASS = 6, - HUD_ELEM_MINIMAP = 7 + HUD_ELEM_MINIMAP = 7, + HUD_ELEM_HOTBAR = 8, }; enum HudElementStat : u8 { diff --git a/src/irrlicht_changes/CGUITTFont.cpp b/src/irrlicht_changes/CGUITTFont.cpp index 40d2bb405..022ab46b0 100644 --- a/src/irrlicht_changes/CGUITTFont.cpp +++ b/src/irrlicht_changes/CGUITTFont.cpp @@ -513,12 +513,15 @@ void CGUITTFont::setFontHinting(const bool enable, const bool enable_auto_hintin void CGUITTFont::draw(const core::stringw& text, const core::rect& position, video::SColor color, bool hcenter, bool vcenter, const core::rect* clip) { - draw(EnrichedString(std::wstring(text.c_str()), color), position, hcenter, vcenter, clip); + // Allow colors to work for strings that have passed through irrlicht by catching + // them here and converting them to enriched just before drawing. + EnrichedString s(text.c_str(), color); + draw(s, position, hcenter, vcenter, clip); } void CGUITTFont::draw(const EnrichedString &text, const core::rect& position, bool hcenter, bool vcenter, const core::rect* clip) { - const std::vector &colors = text.getColors(); + const auto &colors = text.getColors(); if (!Driver) return; @@ -1095,13 +1098,9 @@ core::array CGUITTFont::addTextSceneNode(const wchar_t* text // the default font material SMaterial mat; - mat.Lighting = true; mat.ZWriteEnable = video::EZW_OFF; - mat.NormalizeNormals = true; - mat.ColorMaterial = video::ECM_NONE; mat.MaterialType = use_transparency ? video::EMT_TRANSPARENT_ALPHA_CHANNEL : video::EMT_SOLID; mat.MaterialTypeParam = 0.01f; - mat.DiffuseColor = color; wchar_t current_char = 0, previous_char = 0; u32 n = 0; diff --git a/src/irrlicht_changes/static_text.h b/src/irrlicht_changes/static_text.h index 636760f6c..15a976c74 100644 --- a/src/irrlicht_changes/static_text.h +++ b/src/irrlicht_changes/static_text.h @@ -61,7 +61,7 @@ namespace gui static irr::gui::IGUIStaticText *add( irr::gui::IGUIEnvironment *guienv, - const wchar_t *text, + std::wstring_view text, const core::rect< s32 > &rectangle, bool border = false, bool wordWrap = true, @@ -204,7 +204,7 @@ inline void setStaticText(irr::gui::IGUIStaticText *static_text, const EnrichedS } } -inline void setStaticText(irr::gui::IGUIStaticText *static_text, const wchar_t *text) +inline void setStaticText(irr::gui::IGUIStaticText *static_text, std::wstring_view text) { setStaticText(static_text, EnrichedString(text, static_text->getOverrideColor())); } diff --git a/src/irrlichttypes_extrabloated.h b/src/irrlichttypes_extrabloated.h index b03ba7955..a3de2c3c8 100644 --- a/src/irrlichttypes_extrabloated.h +++ b/src/irrlichttypes_extrabloated.h @@ -24,7 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc., #ifndef SERVER #include #include -#include #include #include #include diff --git a/src/itemdef.cpp b/src/itemdef.cpp index ad2ed4847..220c6fbb6 100644 --- a/src/itemdef.cpp +++ b/src/itemdef.cpp @@ -45,7 +45,8 @@ TouchInteraction::TouchInteraction() pointed_object = TouchInteractionMode_USER; } -TouchInteractionMode TouchInteraction::getMode(PointedThingType pointed_type) const +TouchInteractionMode TouchInteraction::getMode(const ItemDefinition &selected_def, + PointedThingType pointed_type) const { TouchInteractionMode result; switch (pointed_type) { @@ -63,7 +64,9 @@ TouchInteractionMode TouchInteraction::getMode(PointedThingType pointed_type) co } if (result == TouchInteractionMode_USER) { - if (pointed_type == POINTEDTHING_OBJECT) + if (pointed_type == POINTEDTHING_OBJECT && !selected_def.usable) + // Only apply when we're actually able to punch the object, i.e. when + // the selected item has no on_use callback defined. result = g_settings->get("touch_punch_gesture") == "long_tap" ? LONG_DIG_SHORT_PLACE : SHORT_DIG_LONG_PLACE; else diff --git a/src/itemdef.h b/src/itemdef.h index 782dad816..44fab8d91 100644 --- a/src/itemdef.h +++ b/src/itemdef.h @@ -20,7 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once -#include "irrlichttypes_extrabloated.h" +#include "irrlichttypes_bloated.h" #include #include #include @@ -71,7 +71,8 @@ struct TouchInteraction TouchInteraction(); // Returns the right mode for the pointed thing and resolves any occurrence // of TouchInteractionMode_USER into an actual mode. - TouchInteractionMode getMode(PointedThingType pointed_type) const; + TouchInteractionMode getMode(const ItemDefinition &selected_def, + PointedThingType pointed_type) const; void serialize(std::ostream &os) const; void deSerialize(std::istream &is); }; diff --git a/src/itemstackmetadata.cpp b/src/itemstackmetadata.cpp index be1715e1a..a2fc67c46 100644 --- a/src/itemstackmetadata.cpp +++ b/src/itemstackmetadata.cpp @@ -89,11 +89,11 @@ void ItemStackMetadata::deSerialize(std::istream &is) while (!fnd.at_end()) { std::string name = fnd.next(DESERIALIZE_KV_DELIM_STR); std::string var = fnd.next(DESERIALIZE_PAIR_DELIM_STR); - m_stringvars[name] = var; + m_stringvars[name] = std::move(var); } } else { // BACKWARDS COMPATIBILITY - m_stringvars[""] = in; + m_stringvars[""] = std::move(in); } } updateToolCapabilities(); diff --git a/src/lighting.h b/src/lighting.h index 262a48b5d..b0ba714b9 100644 --- a/src/lighting.h +++ b/src/lighting.h @@ -18,7 +18,9 @@ with this program; if not, write to the Free Software Foundation, Inc., */ #pragma once +#include "SColor.h" +using namespace irr; /** * Parameters for automatic exposure compensation @@ -54,4 +56,8 @@ struct Lighting float shadow_intensity {0.0f}; float saturation {1.0f}; float volumetric_light_strength {0.0f}; + video::SColor shadow_tint {255, 0, 0, 0}; + float bloom_intensity {0.05f}; + float bloom_strength_factor {1.0f}; + float bloom_radius {1.0f}; }; diff --git a/src/log.cpp b/src/log.cpp index 98939c9bf..ae3d9a9ab 100644 --- a/src/log.cpp +++ b/src/log.cpp @@ -17,7 +17,7 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "log.h" +#include "log_internal.h" #include "threading/mutex_auto_lock.h" #include "debug.h" @@ -27,7 +27,6 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "config.h" #include "exceptions.h" #include "util/numeric.h" -#include "log.h" #include "filesys.h" #ifdef __ANDROID__ @@ -55,7 +54,7 @@ public: return m_logger.hasOutput(m_level); } - virtual void log(const std::string &buf) override { + virtual void log(std::string_view buf) override { if (!m_raw) { m_logger.log(m_level, buf); } else { @@ -106,7 +105,7 @@ thread_local LogStream dout_con(trace_target); // Android #ifdef __ANDROID__ -static unsigned int g_level_to_android[] = { +constexpr static unsigned int g_level_to_android[] = { ANDROID_LOG_INFO, // LL_NONE ANDROID_LOG_ERROR, // LL_ERROR ANDROID_LOG_WARN, // LL_WARNING @@ -116,11 +115,12 @@ static unsigned int g_level_to_android[] = { ANDROID_LOG_VERBOSE, // LL_TRACE }; -void AndroidLogOutput::logRaw(LogLevel lev, const std::string &line) +void AndroidLogOutput::logRaw(LogLevel lev, std::string_view line) { static_assert(ARRLEN(g_level_to_android) == LL_MAX, "mismatch between android and internal loglevels"); - __android_log_write(g_level_to_android[lev], PROJECT_NAME_C, line.c_str()); + __android_log_print(g_level_to_android[lev], PROJECT_NAME_C, "%.*s", + line.size(), line.data()); } #endif @@ -131,7 +131,7 @@ void AndroidLogOutput::logRaw(LogLevel lev, const std::string &line) //// Logger //// -LogLevel Logger::stringToLevel(const std::string &name) +LogLevel Logger::stringToLevel(std::string_view name) { if (name == "none") return LL_NONE; @@ -202,7 +202,7 @@ void Logger::setLevelSilenced(LogLevel lev, bool silenced) m_silenced_levels[lev] = silenced; } -void Logger::registerThread(const std::string &name) +void Logger::registerThread(std::string_view name) { std::thread::id id = std::this_thread::get_id(); MutexAutoLock lock(m_mutex); @@ -252,7 +252,7 @@ const std::string &Logger::getThreadName() return fallback_name; } -void Logger::log(LogLevel lev, const std::string &text) +void Logger::log(LogLevel lev, std::string_view text) { if (isLevelSilenced(lev)) return; @@ -268,7 +268,7 @@ void Logger::log(LogLevel lev, const std::string &text) logToOutputs(lev, line, timestamp, thread_name, text); } -void Logger::logRaw(LogLevel lev, const std::string &text) +void Logger::logRaw(LogLevel lev, std::string_view text) { if (isLevelSilenced(lev)) return; @@ -276,7 +276,7 @@ void Logger::logRaw(LogLevel lev, const std::string &text) logToOutputsRaw(lev, text); } -void Logger::logToOutputsRaw(LogLevel lev, const std::string &line) +void Logger::logToOutputsRaw(LogLevel lev, std::string_view line) { MutexAutoLock lock(m_mutex); for (size_t i = 0; i != m_outputs[lev].size(); i++) @@ -285,7 +285,7 @@ void Logger::logToOutputsRaw(LogLevel lev, const std::string &line) void Logger::logToOutputs(LogLevel lev, const std::string &combined, const std::string &time, const std::string &thread_name, - const std::string &payload_text) + std::string_view payload_text) { MutexAutoLock lock(m_mutex); for (size_t i = 0; i != m_outputs[lev].size(); i++) @@ -334,7 +334,7 @@ StreamLogOutput::StreamLogOutput(std::ostream &stream) : #endif } -void StreamLogOutput::logRaw(LogLevel lev, const std::string &line) +void StreamLogOutput::logRaw(LogLevel lev, std::string_view line) { bool colored_message = (Logger::color_mode == LOG_COLOR_ALWAYS) || (Logger::color_mode == LOG_COLOR_AUTO && is_tty); @@ -371,42 +371,15 @@ void StreamLogOutput::logRaw(LogLevel lev, const std::string &line) } } -void LogOutputBuffer::updateLogLevel() +void StreamProxy::fix_stream_state(std::ostream &os) { - const std::string &conf_loglev = g_settings->get("chat_log_level"); - LogLevel log_level = Logger::stringToLevel(conf_loglev); - if (log_level == LL_MAX) { - warningstream << "Supplied unrecognized chat_log_level; " - "showing none." << std::endl; - log_level = LL_NONE; - } - - m_logger.removeOutput(this); - m_logger.addOutputMaxLevel(this, log_level); -} - -void LogOutputBuffer::logRaw(LogLevel lev, const std::string &line) -{ - std::string color; - - if (!g_settings->getBool("disable_escape_sequences")) { - switch (lev) { - case LL_ERROR: // red - color = "\x1b(c@#F00)"; - break; - case LL_WARNING: // yellow - color = "\x1b(c@#EE0)"; - break; - case LL_INFO: // grey - color = "\x1b(c@#BBB)"; - break; - case LL_VERBOSE: // dark grey - case LL_TRACE: - color = "\x1b(c@#888)"; - break; - default: break; - } - } - MutexAutoLock lock(m_buffer_mutex); - m_buffer.emplace(color.append(line)); + std::ios::iostate state = os.rdstate(); + // clear error state so the stream works again + os.clear(); + if (state & std::ios::eofbit) + os << "(ostream:eofbit)"; + if (state & std::ios::badbit) + os << "(ostream:badbit)"; + if (state & std::ios::failbit) + os << "(ostream:failbit)"; } diff --git a/src/log.h b/src/log.h index 9ac4e5767..0e34b2c83 100644 --- a/src/log.h +++ b/src/log.h @@ -1,198 +1,9 @@ -/* -Minetest -Copyright (C) 2013 celeron55, Perttu Ahola - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation; either version 2.1 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once -#include -#include -#include -#include -#include -#include -#include -#include "threading/mutex_auto_lock.h" #include "util/basic_macros.h" #include "util/stream.h" -#include "irrlichttypes.h" - -class ILogOutput; - -enum LogLevel { - LL_NONE, // Special level that is always printed - LL_ERROR, - LL_WARNING, - LL_ACTION, // In-game actions - LL_INFO, - LL_VERBOSE, - LL_TRACE, - LL_MAX, -}; - -enum LogColor { - LOG_COLOR_NEVER, - LOG_COLOR_ALWAYS, - LOG_COLOR_AUTO, -}; - -typedef u8 LogLevelMask; -#define LOGLEVEL_TO_MASKLEVEL(x) (1 << x) - -class Logger { -public: - void addOutput(ILogOutput *out); - void addOutput(ILogOutput *out, LogLevel lev); - void addOutputMasked(ILogOutput *out, LogLevelMask mask); - void addOutputMaxLevel(ILogOutput *out, LogLevel lev); - LogLevelMask removeOutput(ILogOutput *out); - void setLevelSilenced(LogLevel lev, bool silenced); - - void registerThread(const std::string &name); - void deregisterThread(); - - void log(LogLevel lev, const std::string &text); - // Logs without a prefix - void logRaw(LogLevel lev, const std::string &text); - - static LogLevel stringToLevel(const std::string &name); - static const char *getLevelLabel(LogLevel lev); - - bool hasOutput(LogLevel level) { - return m_has_outputs[level].load(std::memory_order_relaxed); - } - - bool isLevelSilenced(LogLevel level) { - return m_silenced_levels[level].load(std::memory_order_relaxed); - } - - static LogColor color_mode; - -private: - void logToOutputsRaw(LogLevel, const std::string &line); - void logToOutputs(LogLevel, const std::string &combined, - const std::string &time, const std::string &thread_name, - const std::string &payload_text); - - const std::string &getThreadName(); - - std::vector m_outputs[LL_MAX]; - std::atomic m_has_outputs[LL_MAX]; - std::atomic m_silenced_levels[LL_MAX]; - std::map m_thread_names; - mutable std::mutex m_mutex; -}; - -class ILogOutput { -public: - virtual void logRaw(LogLevel, const std::string &line) = 0; - virtual void log(LogLevel, const std::string &combined, - const std::string &time, const std::string &thread_name, - const std::string &payload_text) = 0; -}; - -class ICombinedLogOutput : public ILogOutput { -public: - void log(LogLevel lev, const std::string &combined, - const std::string &time, const std::string &thread_name, - const std::string &payload_text) - { - logRaw(lev, combined); - } -}; - -class StreamLogOutput : public ICombinedLogOutput { -public: - StreamLogOutput(std::ostream &stream); - - void logRaw(LogLevel lev, const std::string &line); - -private: - std::ostream &m_stream; - bool is_tty = false; -}; - -class FileLogOutput : public ICombinedLogOutput { -public: - void setFile(const std::string &filename, s64 file_size_max); - - void logRaw(LogLevel lev, const std::string &line) - { - m_stream << line << std::endl; - } - -private: - std::ofstream m_stream; -}; - -class LogOutputBuffer : public ICombinedLogOutput { -public: - LogOutputBuffer(Logger &logger) : - m_logger(logger) - { - updateLogLevel(); - }; - - virtual ~LogOutputBuffer() - { - m_logger.removeOutput(this); - } - - void updateLogLevel(); - - void logRaw(LogLevel lev, const std::string &line); - - void clear() - { - MutexAutoLock lock(m_buffer_mutex); - m_buffer = std::queue(); - } - - bool empty() const - { - MutexAutoLock lock(m_buffer_mutex); - return m_buffer.empty(); - } - - std::string get() - { - MutexAutoLock lock(m_buffer_mutex); - if (m_buffer.empty()) - return ""; - std::string s = std::move(m_buffer.front()); - m_buffer.pop(); - return s; - } - -private: - // g_logger serializes calls to logRaw() with a mutex, but that - // doesn't prevent get() / clear() from being called on top of it. - // This mutex prevents that. - mutable std::mutex m_buffer_mutex; - std::queue m_buffer; - Logger &m_logger; -}; - -#ifdef __ANDROID__ -class AndroidLogOutput : public ICombinedLogOutput { -public: - void logRaw(LogLevel lev, const std::string &line); -}; -#endif /* * LogTarget @@ -206,7 +17,7 @@ class LogTarget { public: // Must be thread-safe. These can be called from any thread. virtual bool hasOutput() = 0; - virtual void log(const std::string &buf) = 0; + virtual void log(std::string_view buf) = 0; }; @@ -221,21 +32,60 @@ class StreamProxy { public: StreamProxy(std::ostream *os) : m_os(os) { } + static void fix_stream_state(std::ostream &os); + template - StreamProxy& operator<<(T&& arg) { + StreamProxy& operator<<(T&& arg) + { if (m_os) { + if (!m_os->good()) + fix_stream_state(*m_os); *m_os << std::forward(arg); } return *this; } - StreamProxy& operator<<(std::ostream& (*manip)(std::ostream&)) { + StreamProxy& operator<<(std::ostream& (*manip)(std::ostream&)) + { if (m_os) { + if (!m_os->good()) + fix_stream_state(*m_os); *m_os << manip; } return *this; } +private: + template + StreamProxy& emit_with_null_check(T&& arg) + { + // These calls explicitly use the templated version of operator<<, + // so that they won't use the overloads created by ADD_NULL_CHECK. + if (arg == nullptr) + return this->operator<< ("(null)"); + else + return this->operator<< (std::forward(arg)); + } + +public: + // Add specific overrides for operator<< which check for NULL string + // pointers. This is undefined behavior in the C++ spec, so emit "(null)" + // instead. These are method overloads, rather than template specializations. +#define ADD_NULL_CHECK(_type) \ + StreamProxy& operator<<(_type arg) \ + { \ + return emit_with_null_check(std::forward<_type>(arg)); \ + } + + ADD_NULL_CHECK(char*) + ADD_NULL_CHECK(unsigned char*) + ADD_NULL_CHECK(signed char*) + ADD_NULL_CHECK(const char*) + ADD_NULL_CHECK(const unsigned char*) + ADD_NULL_CHECK(const signed char*) + +#undef ADD_NULL_CHECK + private: std::ostream *m_os; }; @@ -304,7 +154,7 @@ public: return m_target.hasOutput(); } - void internalFlush(const std::string &buf) { + void internalFlush(std::string_view buf) { m_target.log(buf); } @@ -325,16 +175,6 @@ private: }; -#ifdef __ANDROID__ -extern AndroidLogOutput stdout_output; -extern AndroidLogOutput stderr_output; -#else -extern StreamLogOutput stdout_output; -extern StreamLogOutput stderr_output; -#endif - -extern Logger g_logger; - /* * By making the streams thread_local, each thread has its own * private buffer. Two or more threads can write to the same stream diff --git a/src/log_internal.h b/src/log_internal.h new file mode 100644 index 000000000..512a57949 --- /dev/null +++ b/src/log_internal.h @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "threading/mutex_auto_lock.h" +#include "util/basic_macros.h" +#include "util/stream.h" +#include "irrlichttypes.h" +#include "log.h" + +class ILogOutput; + +enum LogLevel { + LL_NONE, // Special level that is always printed + LL_ERROR, + LL_WARNING, + LL_ACTION, // In-game actions + LL_INFO, + LL_VERBOSE, + LL_TRACE, + LL_MAX, +}; + +enum LogColor { + LOG_COLOR_NEVER, + LOG_COLOR_ALWAYS, + LOG_COLOR_AUTO, +}; + +typedef u8 LogLevelMask; +#define LOGLEVEL_TO_MASKLEVEL(x) (1 << x) + +class Logger { +public: + void addOutput(ILogOutput *out); + void addOutput(ILogOutput *out, LogLevel lev); + void addOutputMasked(ILogOutput *out, LogLevelMask mask); + void addOutputMaxLevel(ILogOutput *out, LogLevel lev); + LogLevelMask removeOutput(ILogOutput *out); + void setLevelSilenced(LogLevel lev, bool silenced); + + void registerThread(std::string_view name); + void deregisterThread(); + + void log(LogLevel lev, std::string_view text); + // Logs without a prefix + void logRaw(LogLevel lev, std::string_view text); + + static LogLevel stringToLevel(std::string_view name); + static const char *getLevelLabel(LogLevel lev); + + bool hasOutput(LogLevel level) { + return m_has_outputs[level].load(std::memory_order_relaxed); + } + + bool isLevelSilenced(LogLevel level) { + return m_silenced_levels[level].load(std::memory_order_relaxed); + } + + static LogColor color_mode; + +private: + void logToOutputsRaw(LogLevel, std::string_view line); + void logToOutputs(LogLevel, const std::string &combined, + const std::string &time, const std::string &thread_name, + std::string_view payload_text); + + const std::string &getThreadName(); + + std::vector m_outputs[LL_MAX]; + std::atomic m_has_outputs[LL_MAX]; + std::atomic m_silenced_levels[LL_MAX]; + std::map m_thread_names; + mutable std::mutex m_mutex; +}; + +class ILogOutput { +public: + virtual void logRaw(LogLevel, std::string_view line) = 0; + virtual void log(LogLevel, const std::string &combined, + const std::string &time, const std::string &thread_name, + std::string_view payload_text) = 0; +}; + +class ICombinedLogOutput : public ILogOutput { +public: + void log(LogLevel lev, const std::string &combined, + const std::string &time, const std::string &thread_name, + std::string_view payload_text) + { + logRaw(lev, combined); + } +}; + +class StreamLogOutput : public ICombinedLogOutput { +public: + StreamLogOutput(std::ostream &stream); + + void logRaw(LogLevel lev, std::string_view line); + +private: + std::ostream &m_stream; + bool is_tty = false; +}; + +class FileLogOutput : public ICombinedLogOutput { +public: + void setFile(const std::string &filename, s64 file_size_max); + + void logRaw(LogLevel lev, std::string_view line) + { + m_stream << line << std::endl; + } + +private: + std::ofstream m_stream; +}; + +struct LogEntry { + LogLevel level; + std::string timestamp; + std::string thread_name; + std::string text; + // "timestamp: level[thread_name]: text" + std::string combined; +}; + +class CaptureLogOutput : public ILogOutput { +public: + CaptureLogOutput() = delete; + DISABLE_CLASS_COPY(CaptureLogOutput); + + CaptureLogOutput(Logger &logger) : m_logger(logger) + { + m_logger.addOutput(this); + } + + ~CaptureLogOutput() + { + m_logger.removeOutput(this); + } + + void setLogLevel(LogLevel level) + { + m_logger.removeOutput(this); + m_logger.addOutputMaxLevel(this, level); + } + + void logRaw(LogLevel lev, std::string_view line) override + { + MutexAutoLock lock(m_mutex); + m_entries.emplace_back(LogEntry{lev, "", "", std::string(line), std::string(line)}); + } + + void log(LogLevel lev, const std::string &combined, + const std::string &time, const std::string &thread_name, + std::string_view payload_text) override + { + MutexAutoLock lock(m_mutex); + m_entries.emplace_back(LogEntry{lev, time, thread_name, std::string(payload_text), combined}); + } + + // Take the log entries currently stored, clearing the buffer. + std::vector take() + { + std::vector entries; + MutexAutoLock lock(m_mutex); + std::swap(m_entries, entries); + return entries; + } + +private: + Logger &m_logger; + // g_logger serializes calls to log/logRaw with a mutex, but that + // doesn't prevent take() from being called on top of it. + // This mutex prevents that. + std::mutex m_mutex; + std::vector m_entries; +}; + + +#ifdef __ANDROID__ +class AndroidLogOutput : public ICombinedLogOutput { +public: + void logRaw(LogLevel lev, std::string_view line); +}; +#endif + +#ifdef __ANDROID__ +extern AndroidLogOutput stdout_output; +extern AndroidLogOutput stderr_output; +#else +extern StreamLogOutput stdout_output; +extern StreamLogOutput stderr_output; +#endif + +extern Logger g_logger; diff --git a/src/main.cpp b/src/main.cpp index 9f358bb66..803f3c6b0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -31,6 +31,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "migratesettings.h" #include "gettext.h" #include "log.h" +#include "log_internal.h" #include "util/quicktune.h" #include "httpfetch.h" #include "gameparams.h" @@ -531,7 +532,6 @@ static bool setup_log_params(const Settings &cmd_args) if (cmd_args.getFlag("trace")) { dstream << _("Enabling trace level debug output") << std::endl; g_logger.addOutput(&stderr_output, LL_TRACE); - socket_enable_debug_output = true; } return true; @@ -729,7 +729,7 @@ static void startup_message() print_version(infostream); infostream << "SER_FMT_VER_HIGHEST_READ=" << TOSTRING(SER_FMT_VER_HIGHEST_READ) << - " LATEST_PROTOCOL_VERSION=" << TOSTRING(LATEST_PROTOCOL_VERSION) + " LATEST_PROTOCOL_VERSION=" << LATEST_PROTOCOL_VERSION << std::endl; } @@ -1279,8 +1279,7 @@ static bool recompress_map_database(const GameParams &game_params, const Setting { MapBlock mb(v3s16(0,0,0), &server); - u8 ver = readU8(iss); - mb.deSerialize(iss, ver, true); + ServerMap::deSerializeBlock(&mb, iss); oss.str(""); oss.clear(); diff --git a/src/map.cpp b/src/map.cpp index d54ed1270..85ff0e84a 100644 --- a/src/map.cpp +++ b/src/map.cpp @@ -822,17 +822,9 @@ void MMVManip::initialEmerge(v3s16 blockpos_min, v3s16 blockpos_max, } else { flags |= VMANIP_BLOCK_DATA_INEXIST; - /* - Mark area inexistent - */ + // Mark area inexistent VoxelArea a(p*MAP_BLOCKSIZE, (p+1)*MAP_BLOCKSIZE-v3s16(1,1,1)); - // Fill with VOXELFLAG_NO_DATA - for(s32 z=a.MinEdge.Z; z<=a.MaxEdge.Z; z++) - for(s32 y=a.MinEdge.Y; y<=a.MaxEdge.Y; y++) - { - s32 i = m_area.index(a.MinEdge.X,y,z); - memset(&m_flags[i], VOXELFLAG_NO_DATA, MAP_BLOCKSIZE); - } + setFlags(a, VOXELFLAG_NO_DATA); } } /*else if (block->getNode(0, 0, 0).getContent() == CONTENT_IGNORE) @@ -848,9 +840,9 @@ void MMVManip::initialEmerge(v3s16 blockpos_min, v3s16 blockpos_max, } void MMVManip::blitBackAll(std::map *modified_blocks, - bool overwrite_generated) + bool overwrite_generated) const { - if(m_area.getExtent() == v3s16(0,0,0)) + if (m_area.hasEmptyExtent()) return; assert(m_map); diff --git a/src/map.h b/src/map.h index a4bf45314..e3624a68d 100644 --- a/src/map.h +++ b/src/map.h @@ -333,7 +333,7 @@ public: // This is much faster with big chunks of generated data void blitBackAll(std::map * modified_blocks, - bool overwrite_generated = true); + bool overwrite_generated = true) const; /* Creates a copy of this VManip including contents, the copy will not be diff --git a/src/mapblock.cpp b/src/mapblock.cpp index 9a27a9f3d..714b47ec1 100644 --- a/src/mapblock.cpp +++ b/src/mapblock.cpp @@ -178,13 +178,13 @@ void MapBlock::copyTo(VoxelManipulator &dst) getPosRelative(), data_size); } -void MapBlock::copyFrom(VoxelManipulator &dst) +void MapBlock::copyFrom(const VoxelManipulator &src) { v3s16 data_size(MAP_BLOCKSIZE, MAP_BLOCKSIZE, MAP_BLOCKSIZE); VoxelArea data_area(v3s16(0,0,0), data_size - v3s16(1,1,1)); // Copy from VoxelManipulator to data - dst.copyTo(data, data_area, v3s16(0,0,0), + src.copyTo(data, data_area, v3s16(0,0,0), getPosRelative(), data_size); } diff --git a/src/mapblock.h b/src/mapblock.h index 843daf6ef..044c104bc 100644 --- a/src/mapblock.h +++ b/src/mapblock.h @@ -307,8 +307,8 @@ public: // Copies data to VoxelManipulator to getPosRelative() void copyTo(VoxelManipulator &dst); - // Copies data from VoxelManipulator getPosRelative() - void copyFrom(VoxelManipulator &dst); + // Copies data from VoxelManipulator to getPosRelative() + void copyFrom(const VoxelManipulator &src); // Update is air flag. // Sets m_is_air to appropriate value. diff --git a/src/mapgen/cavegen.cpp b/src/mapgen/cavegen.cpp index 47272142f..e6ab66980 100644 --- a/src/mapgen/cavegen.cpp +++ b/src/mapgen/cavegen.cpp @@ -82,8 +82,6 @@ void CavesNoiseIntersection::generateCaves(MMVManip *vm, const v3s16 &em = vm->m_area.getExtent(); u32 index2d = 0; // Biomemap index - s16 *biome_transitions = m_bmgn->getBiomeTransitions(); - for (s16 z = nmin.Z; z <= nmax.Z; z++) for (s16 x = nmin.X; x <= nmax.X; x++, index2d++) { bool column_is_open = false; // Is column open to overground @@ -101,8 +99,7 @@ void CavesNoiseIntersection::generateCaves(MMVManip *vm, u16 depth_riverbed = biome->depth_riverbed; u16 nplaced = 0; - int cur_biome_depth = 0; - s16 biome_y_min = biome_transitions[cur_biome_depth]; + s16 biome_y_min = m_bmgn->getNextTransitionY(nmax.Y); // Don't excavate the overgenerated stone at nmax.Y + 1, // this creates a 'roof' over the tunnel, preventing light in @@ -114,15 +111,12 @@ void CavesNoiseIntersection::generateCaves(MMVManip *vm, // We need this check to make sure that biomes don't generate too far down if (y < biome_y_min) { biome = m_bmgn->getBiomeAtIndex(index2d, v3s16(x, y, z)); + biome_y_min = m_bmgn->getNextTransitionY(y); - // Finding the height of the next biome - // On first iteration this may loop a couple times after than it should just run once - while (y < biome_y_min) { - biome_y_min = biome_transitions[++cur_biome_depth]; + if (x == nmin.X && z == nmin.Z && false) { + dstream << "cavegen: biome at " << y << " is " << biome->name + << ", next at " << biome_y_min << std::endl; } - - /*if (x == nmin.X && z == nmin.Z) - printf("Cave: check @ %i -> %s -> again at %i\n", y, biome->name.c_str(), biome_y_min);*/ } content_t c = vm->m_data[vi].getContent(); diff --git a/src/mapgen/dungeongen.cpp b/src/mapgen/dungeongen.cpp index 1d439abeb..0369bdac3 100644 --- a/src/mapgen/dungeongen.cpp +++ b/src/mapgen/dungeongen.cpp @@ -91,7 +91,7 @@ void DungeonGen::generate(MMVManip *vm, u32 bseed, v3s16 nmin, v3s16 nmax) random.seed(bseed + 2); // Dungeon generator doesn't modify places which have this set - vm->clearFlag(VMANIP_FLAG_DUNGEON_INSIDE | VMANIP_FLAG_DUNGEON_PRESERVE); + vm->clearFlags(vm->m_area, VMANIP_FLAG_DUNGEON_INSIDE | VMANIP_FLAG_DUNGEON_PRESERVE); if (dp.only_in_ground) { // Set all air and liquid drawtypes to be untouchable to make dungeons generate diff --git a/src/mapgen/mapgen.cpp b/src/mapgen/mapgen.cpp index 80ffebc9e..0b821e02e 100644 --- a/src/mapgen/mapgen.cpp +++ b/src/mapgen/mapgen.cpp @@ -649,8 +649,6 @@ void MapgenBasic::generateBiomes() noise_filler_depth->perlinMap2D(node_min.X, node_min.Z); - s16 *biome_transitions = biomegen->getBiomeTransitions(); - for (s16 z = node_min.Z; z <= node_max.Z; z++) for (s16 x = node_min.X; x <= node_max.X; x++, index++) { Biome *biome = NULL; @@ -661,8 +659,7 @@ void MapgenBasic::generateBiomes() u16 depth_riverbed = 0; u32 vi = vm->m_area.index(x, node_max.Y, z); - int cur_biome_depth = 0; - s16 biome_y_min = biome_transitions[cur_biome_depth]; + s16 biome_y_min = biomegen->getNextTransitionY(node_max.Y); // Check node at base of mapchunk above, either a node of a previously // generated mapchunk or if not, a node of overgenerated base terrain. @@ -695,15 +692,7 @@ void MapgenBasic::generateBiomes() if (!biome || y < biome_y_min) { // (Re)calculate biome biome = biomegen->getBiomeAtIndex(index, v3s16(x, y, z)); - - // Finding the height of the next biome - // On first iteration this may loop a couple times after than it should just run once - while (y < biome_y_min) { - biome_y_min = biome_transitions[++cur_biome_depth]; - } - - /*if (x == node_min.X && z == node_min.Z) - printf("Map: check @ %i -> %s -> again at %i\n", y, biome->name.c_str(), biome_y_min);*/ + biome_y_min = biomegen->getNextTransitionY(y); } // Add biome to biomemap at first stone surface detected diff --git a/src/mapgen/mg_biome.cpp b/src/mapgen/mg_biome.cpp index 83ecbd691..b270a5413 100644 --- a/src/mapgen/mg_biome.cpp +++ b/src/mapgen/mg_biome.cpp @@ -149,47 +149,36 @@ BiomeGenOriginal::BiomeGenOriginal(BiomeManager *biomemgr, // is disabled. memset(biomemap, 0, sizeof(biome_t) * m_csize.X * m_csize.Z); - // Calculating the bounding position of each biome so we know when we might switch - // First gathering all heights where we might switch - std::vector temp_transition_heights; - temp_transition_heights.reserve(m_bmgr->getNumObjects() * 2); + // Calculate cache of Y transition points + std::vector values; + values.reserve(m_bmgr->getNumObjects() * 2); for (size_t i = 0; i < m_bmgr->getNumObjects(); i++) { Biome *b = (Biome *)m_bmgr->getRaw(i); - temp_transition_heights.push_back(b->max_pos.Y); - temp_transition_heights.push_back(b->min_pos.Y); + values.push_back(b->max_pos.Y); + values.push_back(b->min_pos.Y); } - // Sorting the biome transition points - std::sort(temp_transition_heights.begin(), temp_transition_heights.end(), std::greater()); + std::sort(values.begin(), values.end(), std::greater<>()); + values.erase(std::unique(values.begin(), values.end()), values.end()); - // Getting rid of duplicate biome transition points - s16 last = temp_transition_heights[0]; - size_t out_pos = 1; - for (size_t i = 1; i < temp_transition_heights.size(); i++){ - if (temp_transition_heights[i] != last) { - last = temp_transition_heights[i]; - temp_transition_heights[out_pos++] = last; - } - } - - biome_transitions = new s16[out_pos]; - memcpy(biome_transitions, temp_transition_heights.data(), sizeof(s16) * out_pos); + m_transitions_y = std::move(values); } BiomeGenOriginal::~BiomeGenOriginal() { delete []biomemap; - delete []biome_transitions; delete noise_heat; delete noise_humidity; delete noise_heat_blend; delete noise_humidity_blend; } -s16* BiomeGenOriginal::getBiomeTransitions() const +s16 BiomeGenOriginal::getNextTransitionY(s16 y) const { - return biome_transitions; + // Find first value that is less than y using binary search + auto it = std::lower_bound(m_transitions_y.begin(), m_transitions_y.end(), y, std::greater_equal<>()); + return (it == m_transitions_y.end()) ? S16_MIN : *it; } BiomeGen *BiomeGenOriginal::clone(BiomeManager *biomemgr) const diff --git a/src/mapgen/mg_biome.h b/src/mapgen/mg_biome.h index 567a0fe81..389b36ee9 100644 --- a/src/mapgen/mg_biome.h +++ b/src/mapgen/mg_biome.h @@ -128,11 +128,14 @@ public: // Same as above, but uses a raw numeric index correlating to the (x,z) position. virtual Biome *getBiomeAtIndex(size_t index, v3s16 pos) const = 0; - virtual s16 *getBiomeTransitions() const = 0; + // Returns the next lower y position at which the biome could change. + // You can use this to optimize calls to getBiomeAtIndex(). + virtual s16 getNextTransitionY(s16 y) const { + return y == S16_MIN ? y : (y - 1); + }; // Result of calcBiomes bulk computation. biome_t *biomemap = nullptr; - s16 *biome_transitions = nullptr; protected: BiomeManager *m_bmgr = nullptr; @@ -167,7 +170,7 @@ struct BiomeParamsOriginal : public BiomeParams { NoiseParams np_humidity_blend; }; -class BiomeGenOriginal : public BiomeGen { +class BiomeGenOriginal final : public BiomeGen { public: BiomeGenOriginal(BiomeManager *biomemgr, const BiomeParamsOriginal *params, v3s16 chunksize); @@ -189,7 +192,7 @@ public: Biome *getBiomeAtIndex(size_t index, v3s16 pos) const; Biome *calcBiomeFromNoise(float heat, float humidity, v3s16 pos) const; - s16 *getBiomeTransitions() const; + s16 getNextTransitionY(s16 y) const; float *heatmap; float *humidmap; @@ -201,6 +204,9 @@ private: Noise *noise_humidity; Noise *noise_heat_blend; Noise *noise_humidity_blend; + + // ordered descending + std::vector m_transitions_y; }; diff --git a/src/mapnode.cpp b/src/mapnode.cpp index 6fe169e90..4e3c60192 100644 --- a/src/mapnode.cpp +++ b/src/mapnode.cpp @@ -17,7 +17,7 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "irrlichttypes_extrabloated.h" +#include "irrlichttypes_bloated.h" #include "mapnode.h" #include "porting.h" #include "nodedef.h" diff --git a/src/migratesettings.h b/src/migratesettings.h index 60435f18a..5f6396914 100644 --- a/src/migratesettings.h +++ b/src/migratesettings.h @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "settings.h" +#include "server.h" void migrate_settings() { @@ -11,4 +12,20 @@ void migrate_settings() g_settings->getBool("opaque_water") ? "false" : "true"); g_settings->remove("opaque_water"); } + + // Converts enable_touch to touch_controls/touch_gui + if (g_settings->existsLocal("enable_touch")) { + bool value = g_settings->getBool("enable_touch"); + g_settings->setBool("touch_controls", value); + g_settings->setBool("touch_gui", value); + g_settings->remove("enable_touch"); + } + + // Disables anticheat + if (g_settings->existsLocal("disable_anticheat")) { + if (g_settings->getBool("disable_anticheat")) { + g_settings->setFlagStr("anticheat_flags", 0, flagdesc_anticheat); + } + g_settings->remove("disable_anticheat"); + } } diff --git a/src/nameidmapping.h b/src/nameidmapping.h index 47f424cbc..b200e3e7c 100644 --- a/src/nameidmapping.h +++ b/src/nameidmapping.h @@ -23,7 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include -#include "irrlichttypes_bloated.h" +#include "irrlichttypes.h" typedef std::unordered_map IdToNameMap; typedef std::unordered_map NameToIdMap; diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt index d2e2f52e9..6291e23af 100644 --- a/src/network/CMakeLists.txt +++ b/src/network/CMakeLists.txt @@ -1,10 +1,12 @@ set(common_network_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/address.cpp ${CMAKE_CURRENT_SOURCE_DIR}/connection.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/connectionthreads.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/mtp/impl.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/mtp/threads.cpp ${CMAKE_CURRENT_SOURCE_DIR}/networkpacket.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/serverpackethandler.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/networkprotocol.cpp ${CMAKE_CURRENT_SOURCE_DIR}/serveropcodes.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/serverpackethandler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/socket.cpp PARENT_SCOPE ) diff --git a/src/network/clientopcodes.cpp b/src/network/clientopcodes.cpp index d426d3fe7..7d0840754 100644 --- a/src/network/clientopcodes.cpp +++ b/src/network/clientopcodes.cpp @@ -81,7 +81,7 @@ const ToClientCommandHandler toClientCommandTable[TOCLIENT_NUM_MSG_TYPES] = { "TOCLIENT_MOVE_PLAYER", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_MovePlayer }, // 0x34 { "TOCLIENT_ACCESS_DENIED_LEGACY", TOCLIENT_STATE_NOT_CONNECTED, &Client::handleCommand_AccessDenied }, // 0x35 { "TOCLIENT_FOV", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_Fov }, // 0x36 - { "TOCLIENT_DEATHSCREEN", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_DeathScreen }, // 0x37 + { "TOCLIENT_DEATHSCREEN_LEGACY", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_DeathScreenLegacy }, // 0x37 { "TOCLIENT_MEDIA", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_Media }, // 0x38 null_command_handler, { "TOCLIENT_NODEDEF", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_NodeDef }, // 0x3a @@ -198,7 +198,7 @@ const ServerCommandFactory serverCommandFactoryTable[TOSERVER_NUM_MSG_TYPES] = { "TOSERVER_DAMAGE", 0, true }, // 0x35 null_command_factory, // 0x36 { "TOSERVER_PLAYERITEM", 0, true }, // 0x37 - { "TOSERVER_RESPAWN", 0, true }, // 0x38 + { "TOSERVER_RESPAWN_LEGACY", 0, true }, // 0x38 { "TOSERVER_INTERACT", 0, true }, // 0x39 { "TOSERVER_REMOVED_SOUNDS", 2, true }, // 0x3a { "TOSERVER_NODEMETA_FIELDS", 0, true }, // 0x3b diff --git a/src/network/clientpackethandler.cpp b/src/network/clientpackethandler.cpp index e2bcb51b5..310e10e6e 100644 --- a/src/network/clientpackethandler.cpp +++ b/src/network/clientpackethandler.cpp @@ -19,6 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "client/client.h" +#include "irr_v2d.h" #include "util/base64.h" #include "client/camera.h" #include "client/mesh_generator_thread.h" @@ -48,6 +49,22 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "particles.h" #include +const char *accessDeniedStrings[SERVER_ACCESSDENIED_MAX] = { + N_("Invalid password"), + N_("Your client sent something the server didn't expect. Try reconnecting or updating your client."), + N_("The server is running in singleplayer mode. You cannot connect."), + N_("Your client's version is not supported.\nPlease contact the server administrator."), + N_("Player name contains disallowed characters"), + N_("Player name not allowed"), + N_("Too many users"), + N_("Empty passwords are disallowed. Set a password and try again."), + N_("Another client is connected with this name. If your client closed unexpectedly, try again in a minute."), + N_("Internal server error"), + "", + N_("Server shutting down"), + N_("The server has experienced an internal error. You will now be disconnected.") +}; + void Client::handleCommand_Deprecated(NetworkPacket* pkt) { infostream << "Got deprecated command " @@ -62,11 +79,11 @@ void Client::handleCommand_Hello(NetworkPacket* pkt) u8 serialization_ver; u16 proto_ver; - u16 compression_mode; + u16 unused_compression_mode; u32 auth_mechs; - std::string username_legacy; // for case insensitivity - *pkt >> serialization_ver >> compression_mode >> proto_ver - >> auth_mechs >> username_legacy; + std::string unused; + *pkt >> serialization_ver >> unused_compression_mode >> proto_ver + >> auth_mechs >> unused; // Chose an auth method we support AuthMechanism chosen_auth_mechanism = choseAuthMech(auth_mechs); @@ -75,7 +92,6 @@ void Client::handleCommand_Hello(NetworkPacket* pkt) << "serialization_ver=" << (u32)serialization_ver << ", auth_mechs=" << auth_mechs << ", proto_ver=" << proto_ver - << ", compression_mode=" << compression_mode << ". Doing auth with mech " << chosen_auth_mechanism << std::endl; if (!ser_ver_supported(serialization_ver)) { @@ -87,10 +103,6 @@ void Client::handleCommand_Hello(NetworkPacket* pkt) m_server_ser_ver = serialization_ver; m_proto_ver = proto_ver; - //TODO verify that username_legacy matches sent username, only - // differs in casing (make both uppercase and compare) - // This is only necessary though when we actually want to add casing support - if (m_chosen_auth_mech != AUTH_MECHANISM_NONE) { // we received a TOCLIENT_HELLO while auth was already going on errorstream << "Client: TOCLIENT_HELLO while auth was already going on" @@ -193,7 +205,6 @@ void Client::handleCommand_AccessDenied(NetworkPacket* pkt) // to be processed even if the serialization format has // not been agreed yet, the same as TOCLIENT_INIT. m_access_denied = true; - m_access_denied_reason = "Unknown"; if (pkt->getCommand() != TOCLIENT_ACCESS_DENIED) { // Legacy code from 0.4.12 and older but is still used @@ -212,29 +223,23 @@ void Client::handleCommand_AccessDenied(NetworkPacket* pkt) u8 denyCode; *pkt >> denyCode; - if (denyCode == SERVER_ACCESSDENIED_SHUTDOWN || - denyCode == SERVER_ACCESSDENIED_CRASH) { + if (pkt->getRemainingBytes() > 0) *pkt >> m_access_denied_reason; - if (m_access_denied_reason.empty()) - m_access_denied_reason = accessDeniedStrings[denyCode]; + + if (m_access_denied_reason.empty()) { + if (denyCode >= SERVER_ACCESSDENIED_MAX) { + m_access_denied_reason = gettext("Unknown disconnect reason."); + } else if (denyCode != SERVER_ACCESSDENIED_CUSTOM_STRING) { + m_access_denied_reason = gettext(accessDeniedStrings[denyCode]); + } + } + + if (denyCode == SERVER_ACCESSDENIED_TOO_MANY_USERS) { + m_access_denied_reconnect = true; + } else if (pkt->getRemainingBytes() > 0) { u8 reconnect; *pkt >> reconnect; m_access_denied_reconnect = reconnect & 1; - } else if (denyCode == SERVER_ACCESSDENIED_CUSTOM_STRING) { - *pkt >> m_access_denied_reason; - } else if (denyCode == SERVER_ACCESSDENIED_TOO_MANY_USERS) { - m_access_denied_reason = accessDeniedStrings[denyCode]; - m_access_denied_reconnect = true; - } else if (denyCode < SERVER_ACCESSDENIED_MAX) { - m_access_denied_reason = accessDeniedStrings[denyCode]; - } else { - // Allow us to add new error messages to the - // protocol without raising the protocol version, if we want to. - // Until then (which may be never), this is outside - // of the defined protocol. - *pkt >> m_access_denied_reason; - if (m_access_denied_reason.empty()) - m_access_denied_reason = "Unknown"; } } @@ -649,20 +654,10 @@ void Client::handleCommand_MovePlayerRel(NetworkPacket *pkt) player->addPosition(added_pos); } -void Client::handleCommand_DeathScreen(NetworkPacket* pkt) +void Client::handleCommand_DeathScreenLegacy(NetworkPacket* pkt) { - bool set_camera_point_target; - v3f camera_point_target; - - *pkt >> set_camera_point_target; - *pkt >> camera_point_target; - ClientEvent *event = new ClientEvent(); - event->type = CE_DEATHSCREEN; - event->deathscreen.set_camera_point_target = set_camera_point_target; - event->deathscreen.camera_point_target_x = camera_point_target.X; - event->deathscreen.camera_point_target_y = camera_point_target.Y; - event->deathscreen.camera_point_target_z = camera_point_target.Z; + event->type = CE_DEATHSCREEN_LEGACY; m_client_event_queue.push(event); } @@ -1472,6 +1467,7 @@ void Client::handleCommand_CloudParams(NetworkPacket* pkt) f32 density; video::SColor color_bright; video::SColor color_ambient; + video::SColor color_shadow = video::SColor(255, 204, 204, 204); f32 height; f32 thickness; v2f speed; @@ -1479,6 +1475,10 @@ void Client::handleCommand_CloudParams(NetworkPacket* pkt) *pkt >> density >> color_bright >> color_ambient >> height >> thickness >> speed; + if (pkt->getRemainingBytes() >= 4) { + *pkt >> color_shadow; + } + ClientEvent *event = new ClientEvent(); event->type = CE_CLOUD_PARAMS; event->cloud_params.density = density; @@ -1487,6 +1487,7 @@ void Client::handleCommand_CloudParams(NetworkPacket* pkt) // we avoid using new() and delete() for no good reason event->cloud_params.color_bright = color_bright.color; event->cloud_params.color_ambient = color_ambient.color; + event->cloud_params.color_shadow = color_shadow.color; event->cloud_params.height = height; event->cloud_params.thickness = thickness; // same here: deconstruct to skip constructor @@ -1516,10 +1517,16 @@ void Client::handleCommand_LocalPlayerAnimations(NetworkPacket* pkt) LocalPlayer *player = m_env.getLocalPlayer(); assert(player != NULL); - *pkt >> player->local_animations[0]; - *pkt >> player->local_animations[1]; - *pkt >> player->local_animations[2]; - *pkt >> player->local_animations[3]; + for (int i = 0; i < 4; ++i) { + if (getProtoVersion() >= 46) { + *pkt >> player->local_animations[i]; + } else { + v2s32 local_animation; + *pkt >> local_animation; + player->local_animations[i] = v2f::from(local_animation); + } + } + *pkt >> player->local_animation_speed; player->last_animation = LocalPlayerAnimation::NO_ANIM; @@ -1817,4 +1824,11 @@ void Client::handleCommand_SetLighting(NetworkPacket *pkt) } if (pkt->getRemainingBytes() >= 4) *pkt >> lighting.volumetric_light_strength; + if (pkt->getRemainingBytes() >= 4) + *pkt >> lighting.shadow_tint; + if (pkt->getRemainingBytes() >= 12) { + *pkt >> lighting.bloom_intensity + >> lighting.bloom_strength_factor + >> lighting.bloom_radius; + } } diff --git a/src/network/connection.cpp b/src/network/connection.cpp index 00b4fe4b0..3ffd06fc7 100644 --- a/src/network/connection.cpp +++ b/src/network/connection.cpp @@ -1,1672 +1,17 @@ -/* -Minetest -Copyright (C) 2013 celeron55, Perttu Ahola +// Minetest +// SPDX-License-Identifier: LGPL-2.1-or-later -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. -*/ - -#include -#include -#include -#include -#include "connection_internal.h" -#include "serialization.h" -#include "log.h" -#include "porting.h" -#include "network/connectionthreads.h" -#include "network/networkpacket.h" -#include "network/peerhandler.h" -#include "util/serialize.h" -#include "util/numeric.h" -#include "util/string.h" -#include "settings.h" -#include "profiler.h" +#include "network/connection.h" +#include "network/mtp/impl.h" namespace con { -/******************************************************************************/ -/* defines used for debugging and profiling */ -/******************************************************************************/ -#ifdef NDEBUG - #define PROFILE(a) -#else - #define PROFILE(a) a -#endif - -// TODO: Clean this up. -#define LOG(a) a - -#define PING_TIMEOUT 5.0f - -// exponent base -#define RESEND_SCALE_BASE 1.5f - -// since spacing is exponential the numbers here shouldn't be too high -// (it's okay to start out quick) -#define RESEND_TIMEOUT_MIN 0.1f -#define RESEND_TIMEOUT_MAX 2.0f -#define RESEND_TIMEOUT_FACTOR 2 - -u16 BufferedPacket::getSeqnum() const -{ - if (size() < BASE_HEADER_SIZE + 3) - return 0; // should never happen - - return readU16(&data[BASE_HEADER_SIZE + 1]); -} - -BufferedPacketPtr makePacket(const Address &address, const SharedBuffer &data, - u32 protocol_id, session_t sender_peer_id, u8 channel) -{ - u32 packet_size = data.getSize() + BASE_HEADER_SIZE; - - auto p = std::make_shared(packet_size); - p->address = address; - - writeU32(&p->data[0], protocol_id); - writeU16(&p->data[4], sender_peer_id); - writeU8(&p->data[6], channel); - - memcpy(&p->data[BASE_HEADER_SIZE], *data, data.getSize()); - - return p; -} - -SharedBuffer makeOriginalPacket(const SharedBuffer &data) -{ - u32 header_size = 1; - u32 packet_size = data.getSize() + header_size; - SharedBuffer b(packet_size); - - writeU8(&(b[0]), PACKET_TYPE_ORIGINAL); - if (data.getSize() > 0) { - memcpy(&(b[header_size]), *data, data.getSize()); - } - return b; -} - -// Split data in chunks and add TYPE_SPLIT headers to them -void makeSplitPacket(const SharedBuffer &data, u32 chunksize_max, u16 seqnum, - std::list> *chunks) -{ - // Chunk packets, containing the TYPE_SPLIT header - const u32 chunk_header_size = 7; - const u32 maximum_data_size = chunksize_max - chunk_header_size; - u32 start = 0, end = 0; - u16 chunk_num = 0; - do { - end = start + maximum_data_size - 1; - if (end > data.getSize() - 1) - end = data.getSize() - 1; - - u32 payload_size = end - start + 1; - u32 packet_size = chunk_header_size + payload_size; - - SharedBuffer chunk(packet_size); - - writeU8(&chunk[0], PACKET_TYPE_SPLIT); - writeU16(&chunk[1], seqnum); - // [3] u16 chunk_count is written at next stage - writeU16(&chunk[5], chunk_num); - memcpy(&chunk[chunk_header_size], &data[start], payload_size); - - chunks->push_back(chunk); - - start = end + 1; - sanity_check(chunk_num < 0xFFFF); // overflow - chunk_num++; - } - while (end != data.getSize() - 1); - - for (auto &chunk : *chunks) { - // Write chunk_count - writeU16(&chunk[3], chunk_num); - } -} - -void makeAutoSplitPacket(const SharedBuffer &data, u32 chunksize_max, - u16 &split_seqnum, std::list> *list) -{ - u32 original_header_size = 1; - - if (data.getSize() + original_header_size > chunksize_max) { - makeSplitPacket(data, chunksize_max, split_seqnum, list); - split_seqnum++; - return; - } - - list->push_back(makeOriginalPacket(data)); -} - -SharedBuffer makeReliablePacket(const SharedBuffer &data, u16 seqnum) -{ - u32 header_size = 3; - u32 packet_size = data.getSize() + header_size; - SharedBuffer b(packet_size); - - writeU8(&b[0], PACKET_TYPE_RELIABLE); - writeU16(&b[1], seqnum); - - memcpy(&b[header_size], *data, data.getSize()); - - return b; -} - -/* - ReliablePacketBuffer -*/ - -void ReliablePacketBuffer::print() -{ - MutexAutoLock listlock(m_list_mutex); - LOG(dout_con<<"Dump of ReliablePacketBuffer:" << std::endl); - unsigned int index = 0; - for (BufferedPacketPtr &packet : m_list) { - LOG(dout_con<getSeqnum() << std::endl); - index++; - } -} - -bool ReliablePacketBuffer::empty() -{ - MutexAutoLock listlock(m_list_mutex); - return m_list.empty(); -} - -u32 ReliablePacketBuffer::size() -{ - MutexAutoLock listlock(m_list_mutex); - return m_list.size(); -} - -ReliablePacketBuffer::FindResult ReliablePacketBuffer::findPacketNoLock(u16 seqnum) -{ - for (auto it = m_list.begin(); it != m_list.end(); ++it) { - if ((*it)->getSeqnum() == seqnum) - return it; - } - return m_list.end(); -} - -bool ReliablePacketBuffer::getFirstSeqnum(u16& result) -{ - MutexAutoLock listlock(m_list_mutex); - if (m_list.empty()) - return false; - result = m_list.front()->getSeqnum(); - return true; -} - -BufferedPacketPtr ReliablePacketBuffer::popFirst() -{ - MutexAutoLock listlock(m_list_mutex); - if (m_list.empty()) - throw NotFoundException("Buffer is empty"); - - BufferedPacketPtr p(m_list.front()); - m_list.pop_front(); - - if (m_list.empty()) { - m_oldest_non_answered_ack = 0; - } else { - m_oldest_non_answered_ack = m_list.front()->getSeqnum(); - } - return p; -} - -BufferedPacketPtr ReliablePacketBuffer::popSeqnum(u16 seqnum) +IConnection *createMTP(float timeout, bool ipv6, PeerHandler *handler) { - MutexAutoLock listlock(m_list_mutex); - auto r = findPacketNoLock(seqnum); - if (r == m_list.end()) { - LOG(dout_con<<"Sequence number: " << seqnum - << " not found in reliable buffer"<getSeqnum(); - } - return p; + // safe minimum across internet networks for ipv4 and ipv6 + constexpr u32 MAX_PACKET_SIZE = 512; + return new con::Connection(MAX_PACKET_SIZE, timeout, ipv6, handler); } -void ReliablePacketBuffer::insert(BufferedPacketPtr &p_ptr, u16 next_expected) -{ - MutexAutoLock listlock(m_list_mutex); - const BufferedPacket &p = *p_ptr; - - if (p.size() < BASE_HEADER_SIZE + 3) { - errorstream << "ReliablePacketBuffer::insert(): Invalid data size for " - "reliable packet" << std::endl; - return; - } - u8 type = readU8(&p.data[BASE_HEADER_SIZE + 0]); - if (type != PACKET_TYPE_RELIABLE) { - errorstream << "ReliablePacketBuffer::insert(): type is not reliable" - << std::endl; - return; - } - const u16 seqnum = p.getSeqnum(); - - if (!seqnum_in_window(seqnum, next_expected, MAX_RELIABLE_WINDOW_SIZE)) { - errorstream << "ReliablePacketBuffer::insert(): seqnum is outside of " - "expected window " << std::endl; - return; - } - if (seqnum == next_expected) { - errorstream << "ReliablePacketBuffer::insert(): seqnum is next expected" - << std::endl; - return; - } - - sanity_check(m_list.size() <= SEQNUM_MAX); // FIXME: Handle the error? - - // Find the right place for the packet and insert it there - // If list is empty, just add it - if (m_list.empty()) { - m_list.push_back(p_ptr); - m_oldest_non_answered_ack = seqnum; - // Done. - return; - } - - // Otherwise find the right place - auto it = m_list.begin(); - // Find the first packet in the list which has a higher seqnum - u16 s = (*it)->getSeqnum(); - - /* case seqnum is smaller then next_expected seqnum */ - /* this is true e.g. on wrap around */ - if (seqnum < next_expected) { - while(((s < seqnum) || (s >= next_expected)) && (it != m_list.end())) { - ++it; - if (it != m_list.end()) - s = (*it)->getSeqnum(); - } - } - /* non wrap around case (at least for incoming and next_expected */ - else - { - while(((s < seqnum) && (s >= next_expected)) && (it != m_list.end())) { - ++it; - if (it != m_list.end()) - s = (*it)->getSeqnum(); - } - } - - if (s == seqnum) { - /* nothing to do this seems to be a resent packet */ - /* for paranoia reason data should be compared */ - auto &i = *it; - if ( - (i->getSeqnum() != seqnum) || - (i->size() != p.size()) || - (i->address != p.address) - ) - { - /* if this happens your maximum transfer window may be to big */ - char buf[200]; - snprintf(buf, sizeof(buf), - "Duplicated seqnum %d non matching packet detected:\n", - seqnum); - warningstream << buf; - snprintf(buf, sizeof(buf), - "Old: seqnum: %05d size: %04zu, address: %s\n", - i->getSeqnum(), i->size(), - i->address.serializeString().c_str()); - warningstream << buf; - snprintf(buf, sizeof(buf), - "New: seqnum: %05d size: %04zu, address: %s\n", - p.getSeqnum(), p.size(), - p.address.serializeString().c_str()); - warningstream << buf << std::flush; - throw IncomingDataCorruption("duplicated packet isn't same as original one"); - } - } - /* insert or push back */ - else if (it != m_list.end()) { - m_list.insert(it, p_ptr); - } else { - m_list.push_back(p_ptr); - } - - /* update last packet number */ - m_oldest_non_answered_ack = m_list.front()->getSeqnum(); } - -void ReliablePacketBuffer::incrementTimeouts(float dtime) -{ - MutexAutoLock listlock(m_list_mutex); - for (auto &packet : m_list) { - packet->time += dtime; - packet->totaltime += dtime; - } -} - -u32 ReliablePacketBuffer::getTimedOuts(float timeout) -{ - MutexAutoLock listlock(m_list_mutex); - u32 count = 0; - for (auto &packet : m_list) { - if (packet->totaltime >= timeout) - count++; - } - return count; -} - -std::vector> - ReliablePacketBuffer::getResend(float timeout, u32 max_packets) -{ - MutexAutoLock listlock(m_list_mutex); - std::vector> timed_outs; - for (auto &packet : m_list) { - // resend time scales exponentially with each cycle - const float pkt_timeout = timeout * powf(RESEND_SCALE_BASE, packet->resend_count); - - if (packet->time < pkt_timeout) - continue; - - // caller will resend packet so reset time and increase counter - packet->time = 0.0f; - packet->resend_count++; - - timed_outs.emplace_back(packet); - - if (timed_outs.size() >= max_packets) - break; - } - return timed_outs; -} - -/* - IncomingSplitPacket -*/ - -bool IncomingSplitPacket::insert(u32 chunk_num, SharedBuffer &chunkdata) -{ - sanity_check(chunk_num < chunk_count); - - // If chunk already exists, ignore it. - // Sometimes two identical packets may arrive when there is network - // lag and the server re-sends stuff. - if (chunks.find(chunk_num) != chunks.end()) - return false; - - // Set chunk data in buffer - chunks[chunk_num] = chunkdata; - - return true; -} - -SharedBuffer IncomingSplitPacket::reassemble() -{ - sanity_check(allReceived()); - - // Calculate total size - u32 totalsize = 0; - for (const auto &chunk : chunks) - totalsize += chunk.second.getSize(); - - SharedBuffer fulldata(totalsize); - - // Copy chunks to data buffer - u32 start = 0; - for (u32 chunk_i = 0; chunk_i < chunk_count; chunk_i++) { - const SharedBuffer &buf = chunks[chunk_i]; - memcpy(&fulldata[start], *buf, buf.getSize()); - start += buf.getSize(); - } - - return fulldata; -} - -/* - IncomingSplitBuffer -*/ - -IncomingSplitBuffer::~IncomingSplitBuffer() -{ - MutexAutoLock listlock(m_map_mutex); - for (auto &i : m_buf) { - delete i.second; - } -} - -SharedBuffer IncomingSplitBuffer::insert(BufferedPacketPtr &p_ptr, bool reliable) -{ - MutexAutoLock listlock(m_map_mutex); - const BufferedPacket &p = *p_ptr; - - u32 headersize = BASE_HEADER_SIZE + 7; - if (p.size() < headersize) { - errorstream << "Invalid data size for split packet" << std::endl; - return SharedBuffer(); - } - u8 type = readU8(&p.data[BASE_HEADER_SIZE+0]); - u16 seqnum = readU16(&p.data[BASE_HEADER_SIZE+1]); - u16 chunk_count = readU16(&p.data[BASE_HEADER_SIZE+3]); - u16 chunk_num = readU16(&p.data[BASE_HEADER_SIZE+5]); - - if (type != PACKET_TYPE_SPLIT) { - errorstream << "IncomingSplitBuffer::insert(): type is not split" - << std::endl; - return SharedBuffer(); - } - if (chunk_num >= chunk_count) { - errorstream << "IncomingSplitBuffer::insert(): chunk_num=" << chunk_num - << " >= chunk_count=" << chunk_count << std::endl; - return SharedBuffer(); - } - - // Add if doesn't exist - IncomingSplitPacket *sp; - if (m_buf.find(seqnum) == m_buf.end()) { - sp = new IncomingSplitPacket(chunk_count, reliable); - m_buf[seqnum] = sp; - } else { - sp = m_buf[seqnum]; - } - - if (chunk_count != sp->chunk_count) { - errorstream << "IncomingSplitBuffer::insert(): chunk_count=" - << chunk_count << " != sp->chunk_count=" << sp->chunk_count - << std::endl; - return SharedBuffer(); - } - if (reliable != sp->reliable) - LOG(derr_con<<"Connection: WARNING: reliable="<reliable="<reliable - < chunkdata(chunkdatasize); - memcpy(*chunkdata, &(p.data[headersize]), chunkdatasize); - - if (!sp->insert(chunk_num, chunkdata)) - return SharedBuffer(); - - // If not all chunks are received, return empty buffer - if (!sp->allReceived()) - return SharedBuffer(); - - SharedBuffer fulldata = sp->reassemble(); - - // Remove sp from buffer - m_buf.erase(seqnum); - delete sp; - - return fulldata; -} - -void IncomingSplitBuffer::removeUnreliableTimedOuts(float dtime, float timeout) -{ - MutexAutoLock listlock(m_map_mutex); - std::vector remove_queue; - { - for (const auto &i : m_buf) { - IncomingSplitPacket *p = i.second; - // Reliable ones are not removed by timeout - if (p->reliable) - continue; - p->time += dtime; - if (p->time >= timeout) - remove_queue.push_back(i.first); - } - } - for (u16 j : remove_queue) { - LOG(dout_con<<"NOTE: Removing timed out unreliable split packet"<second; - m_buf.erase(it); - } -} - -/* - ConnectionCommand - */ - -ConnectionCommandPtr ConnectionCommand::create(ConnectionCommandType type) -{ - return ConnectionCommandPtr(new ConnectionCommand(type)); -} - -ConnectionCommandPtr ConnectionCommand::serve(Address address) -{ - auto c = create(CONNCMD_SERVE); - c->address = address; - return c; -} - -ConnectionCommandPtr ConnectionCommand::connect(Address address) -{ - auto c = create(CONNCMD_CONNECT); - c->address = address; - return c; -} - -ConnectionCommandPtr ConnectionCommand::disconnect() -{ - return create(CONNCMD_DISCONNECT); -} - -ConnectionCommandPtr ConnectionCommand::disconnect_peer(session_t peer_id) -{ - auto c = create(CONNCMD_DISCONNECT_PEER); - c->peer_id = peer_id; - return c; -} - -ConnectionCommandPtr ConnectionCommand::resend_one(session_t peer_id) -{ - auto c = create(CONNCMD_RESEND_ONE); - c->peer_id = peer_id; - c->channelnum = 0; // must be same as createPeer - c->reliable = true; - return c; -} - -ConnectionCommandPtr ConnectionCommand::send(session_t peer_id, u8 channelnum, - NetworkPacket *pkt, bool reliable) -{ - auto c = create(CONNCMD_SEND); - c->peer_id = peer_id; - c->channelnum = channelnum; - c->reliable = reliable; - c->data = pkt->oldForgePacket(); - return c; -} - -ConnectionCommandPtr ConnectionCommand::ack(session_t peer_id, u8 channelnum, const Buffer &data) -{ - auto c = create(CONCMD_ACK); - c->peer_id = peer_id; - c->channelnum = channelnum; - c->reliable = false; - data.copyTo(c->data); - return c; -} - -ConnectionCommandPtr ConnectionCommand::createPeer(session_t peer_id, const Buffer &data) -{ - auto c = create(CONCMD_CREATE_PEER); - c->peer_id = peer_id; - c->channelnum = 0; - c->reliable = true; - c->raw = true; - data.copyTo(c->data); - return c; -} - -/* - Channel -*/ - -u16 Channel::readNextIncomingSeqNum() -{ - MutexAutoLock internal(m_internal_mutex); - return next_incoming_seqnum; -} - -u16 Channel::incNextIncomingSeqNum() -{ - MutexAutoLock internal(m_internal_mutex); - u16 retval = next_incoming_seqnum; - next_incoming_seqnum++; - return retval; -} - -u16 Channel::readNextSplitSeqNum() -{ - MutexAutoLock internal(m_internal_mutex); - return next_outgoing_split_seqnum; -} -void Channel::setNextSplitSeqNum(u16 seqnum) -{ - MutexAutoLock internal(m_internal_mutex); - next_outgoing_split_seqnum = seqnum; -} - -u16 Channel::getOutgoingSequenceNumber(bool& successful) -{ - MutexAutoLock internal(m_internal_mutex); - - u16 retval = next_outgoing_seqnum; - successful = false; - - /* shortcut if there ain't any packet in outgoing list */ - if (outgoing_reliables_sent.empty()) { - successful = true; - next_outgoing_seqnum++; - return retval; - } - - u16 lowest_unacked_seqnumber; - if (outgoing_reliables_sent.getFirstSeqnum(lowest_unacked_seqnumber)) { - if (lowest_unacked_seqnumber < next_outgoing_seqnum) { - // ugly cast but this one is required in order to tell compiler we - // know about difference of two unsigned may be negative in general - // but we already made sure it won't happen in this case - if (((u16)(next_outgoing_seqnum - lowest_unacked_seqnumber)) > m_window_size) { - return 0; - } - } else { - // ugly cast but this one is required in order to tell compiler we - // know about difference of two unsigned may be negative in general - // but we already made sure it won't happen in this case - if ((next_outgoing_seqnum + (u16)(SEQNUM_MAX - lowest_unacked_seqnumber)) > - m_window_size) { - return 0; - } - } - } - - successful = true; - next_outgoing_seqnum++; - return retval; -} - -u16 Channel::readOutgoingSequenceNumber() -{ - MutexAutoLock internal(m_internal_mutex); - return next_outgoing_seqnum; -} - -bool Channel::putBackSequenceNumber(u16 seqnum) -{ - if (((seqnum + 1) % (SEQNUM_MAX+1)) == next_outgoing_seqnum) { - - next_outgoing_seqnum = seqnum; - return true; - } - return false; -} - -void Channel::UpdateBytesSent(unsigned int bytes, unsigned int packets) -{ - MutexAutoLock internal(m_internal_mutex); - current_bytes_transfered += bytes; - current_packet_successful += packets; -} - -void Channel::UpdateBytesReceived(unsigned int bytes) { - MutexAutoLock internal(m_internal_mutex); - current_bytes_received += bytes; -} - -void Channel::UpdateBytesLost(unsigned int bytes) -{ - MutexAutoLock internal(m_internal_mutex); - current_bytes_lost += bytes; -} - - -void Channel::UpdatePacketLossCounter(unsigned int count) -{ - MutexAutoLock internal(m_internal_mutex); - current_packet_loss += count; -} - -void Channel::UpdatePacketTooLateCounter() -{ - MutexAutoLock internal(m_internal_mutex); - current_packet_too_late++; -} - -void Channel::UpdateTimers(float dtime) -{ - bpm_counter += dtime; - packet_loss_counter += dtime; - - if (packet_loss_counter > 1.0f) { - packet_loss_counter -= 1.0f; - - unsigned int packet_loss = 11; /* use a neutral value for initialization */ - unsigned int packets_successful = 0; - //unsigned int packet_too_late = 0; - - bool reasonable_amount_of_data_transmitted = false; - - { - MutexAutoLock internal(m_internal_mutex); - packet_loss = current_packet_loss; - //packet_too_late = current_packet_too_late; - packets_successful = current_packet_successful; - - if (current_bytes_transfered > (unsigned int) (m_window_size*512/2)) { - reasonable_amount_of_data_transmitted = true; - } - current_packet_loss = 0; - current_packet_too_late = 0; - current_packet_successful = 0; - } - - /* dynamic window size */ - float successful_to_lost_ratio = 0.0f; - bool done = false; - - if (packets_successful > 0) { - successful_to_lost_ratio = packet_loss/packets_successful; - } else if (packet_loss > 0) { - setWindowSize(m_window_size - 10); - done = true; - } - - if (!done) { - if (successful_to_lost_ratio < 0.01f) { - /* don't even think about increasing if we didn't even - * use major parts of our window */ - if (reasonable_amount_of_data_transmitted) - setWindowSize(m_window_size + 100); - } else if (successful_to_lost_ratio < 0.05f) { - /* don't even think about increasing if we didn't even - * use major parts of our window */ - if (reasonable_amount_of_data_transmitted) - setWindowSize(m_window_size + 50); - } else if (successful_to_lost_ratio > 0.15f) { - setWindowSize(m_window_size - 100); - } else if (successful_to_lost_ratio > 0.1f) { - setWindowSize(m_window_size - 50); - } - } - } - - if (bpm_counter > 10.0f) { - { - MutexAutoLock internal(m_internal_mutex); - cur_kbps = - (((float) current_bytes_transfered)/bpm_counter)/1024.0f; - current_bytes_transfered = 0; - cur_kbps_lost = - (((float) current_bytes_lost)/bpm_counter)/1024.0f; - current_bytes_lost = 0; - cur_incoming_kbps = - (((float) current_bytes_received)/bpm_counter)/1024.0f; - current_bytes_received = 0; - bpm_counter = 0.0f; - } - - if (cur_kbps > max_kbps) { - max_kbps = cur_kbps; - } - - if (cur_kbps_lost > max_kbps_lost) { - max_kbps_lost = cur_kbps_lost; - } - - if (cur_incoming_kbps > max_incoming_kbps) { - max_incoming_kbps = cur_incoming_kbps; - } - - rate_samples = MYMIN(rate_samples+1,10); - float old_fraction = ((float) (rate_samples-1) )/( (float) rate_samples); - avg_kbps = avg_kbps * old_fraction + - cur_kbps * (1.0 - old_fraction); - avg_kbps_lost = avg_kbps_lost * old_fraction + - cur_kbps_lost * (1.0 - old_fraction); - avg_incoming_kbps = avg_incoming_kbps * old_fraction + - cur_incoming_kbps * (1.0 - old_fraction); - } -} - - -/* - Peer -*/ - -PeerHelper::~PeerHelper() -{ - if (m_peer) - m_peer->DecUseCount(); - - m_peer = nullptr; -} - -PeerHelper& PeerHelper::operator=(Peer* peer) -{ - if (m_peer) - m_peer->DecUseCount(); - m_peer = peer; - if (peer && !peer->IncUseCount()) - m_peer = nullptr; - return *this; -} - -bool Peer::IncUseCount() -{ - MutexAutoLock lock(m_exclusive_access_mutex); - - if (!m_pending_deletion) { - this->m_usage++; - return true; - } - - return false; -} - -void Peer::DecUseCount() -{ - { - MutexAutoLock lock(m_exclusive_access_mutex); - sanity_check(m_usage > 0); - m_usage--; - - if (!((m_pending_deletion) && (m_usage == 0))) - return; - } - delete this; -} - -void Peer::RTTStatistics(float rtt, const std::string &profiler_id, - unsigned int num_samples) { - - if (m_last_rtt > 0) { - /* set min max values */ - if (rtt < m_rtt.min_rtt) - m_rtt.min_rtt = rtt; - if (rtt >= m_rtt.max_rtt) - m_rtt.max_rtt = rtt; - - /* do average calculation */ - if (m_rtt.avg_rtt < 0.0) - m_rtt.avg_rtt = rtt; - else - m_rtt.avg_rtt = m_rtt.avg_rtt * (num_samples/(num_samples-1)) + - rtt * (1/num_samples); - - /* do jitter calculation */ - - //just use some neutral value at beginning - float jitter = m_rtt.jitter_min; - - if (rtt > m_last_rtt) - jitter = rtt-m_last_rtt; - - if (rtt <= m_last_rtt) - jitter = m_last_rtt - rtt; - - if (jitter < m_rtt.jitter_min) - m_rtt.jitter_min = jitter; - if (jitter >= m_rtt.jitter_max) - m_rtt.jitter_max = jitter; - - if (m_rtt.jitter_avg < 0.0) - m_rtt.jitter_avg = jitter; - else - m_rtt.jitter_avg = m_rtt.jitter_avg * (num_samples/(num_samples-1)) + - jitter * (1/num_samples); - - if (!profiler_id.empty()) { - g_profiler->graphAdd(profiler_id + " RTT [ms]", rtt * 1000.f); - g_profiler->graphAdd(profiler_id + " jitter [ms]", jitter * 1000.f); - } - } - /* save values required for next loop */ - m_last_rtt = rtt; -} - -bool Peer::isTimedOut(float timeout, std::string &reason) -{ - MutexAutoLock lock(m_exclusive_access_mutex); - - { - u64 current_time = porting::getTimeMs(); - float dtime = CALC_DTIME(m_last_timeout_check, current_time); - m_last_timeout_check = current_time; - m_timeout_counter += dtime; - } - if (m_timeout_counter > timeout) { - reason = "timeout counter"; - return true; - } - - return false; -} - -void Peer::Drop() -{ - { - MutexAutoLock usage_lock(m_exclusive_access_mutex); - m_pending_deletion = true; - if (m_usage != 0) - return; - } - - PROFILE(std::stringstream peerIdentifier1); - PROFILE(peerIdentifier1 << "runTimeouts[" << m_connection->getDesc() - << ";" << id << ";RELIABLE]"); - PROFILE(g_profiler->remove(peerIdentifier1.str())); - PROFILE(std::stringstream peerIdentifier2); - PROFILE(peerIdentifier2 << "sendPackets[" << m_connection->getDesc() - << ";" << id << ";RELIABLE]"); - PROFILE(ScopeProfiler peerprofiler(g_profiler, peerIdentifier2.str(), SPT_AVG)); - - delete this; -} - -UDPPeer::UDPPeer(session_t id, const Address &address, Connection *connection) : - Peer(id, address, connection) -{ - for (Channel &channel : channels) - channel.setWindowSize(START_RELIABLE_WINDOW_SIZE); -} - -bool UDPPeer::isTimedOut(float timeout, std::string &reason) -{ - if (Peer::isTimedOut(timeout, reason)) - return true; - - MutexAutoLock lock(m_exclusive_access_mutex); - - for (int i = 0; i < CHANNEL_COUNT; i++) { - Channel &channel = channels[i]; - if (channel.outgoing_reliables_sent.getTimedOuts(timeout) > 0) { - reason = "outgoing reliables channel=" + itos(i); - return true; - } - } - - return false; -} - -void UDPPeer::reportRTT(float rtt) -{ - if (rtt < 0.0) { - return; - } - RTTStatistics(rtt,"rudp",MAX_RELIABLE_WINDOW_SIZE*10); - - // use this value to decide the resend timeout - float timeout = getStat(AVG_RTT) * RESEND_TIMEOUT_FACTOR; - if (timeout < RESEND_TIMEOUT_MIN) - timeout = RESEND_TIMEOUT_MIN; - if (timeout > RESEND_TIMEOUT_MAX) - timeout = RESEND_TIMEOUT_MAX; - - setResendTimeout(timeout); -} - -bool UDPPeer::Ping(float dtime,SharedBuffer& data) -{ - m_ping_timer += dtime; - if (!isHalfOpen() && m_ping_timer >= PING_TIMEOUT) - { - // Create and send PING packet - writeU8(&data[0], PACKET_TYPE_CONTROL); - writeU8(&data[1], CONTROLTYPE_PING); - m_ping_timer = 0.0f; - return true; - } - return false; -} - -void UDPPeer::PutReliableSendCommand(ConnectionCommandPtr &c, - unsigned int max_packet_size) -{ - if (m_pending_disconnect) - return; - - Channel &chan = channels[c->channelnum]; - - if (chan.queued_commands.empty() && - /* don't queue more packets then window size */ - (chan.queued_reliables.size() + 1 < chan.getWindowSize() / 2)) { - LOG(dout_con<getDesc() - <<" processing reliable command for peer id: " << c->peer_id - <<" data size: " << c->data.getSize() << std::endl); - if (processReliableSendCommand(c, max_packet_size)) - return; - } else { - LOG(dout_con<getDesc() - <<" Queueing reliable command for peer id: " << c->peer_id - <<" data size: " << c->data.getSize() <= chan.getWindowSize() / 2) { - LOG(derr_con << m_connection->getDesc() - << "Possible packet stall to peer id: " << c->peer_id - << " queued_commands=" << chan.queued_commands.size() - << std::endl); - } - } - chan.queued_commands.push_back(c); -} - -bool UDPPeer::processReliableSendCommand( - ConnectionCommandPtr &c_ptr, - unsigned int max_packet_size) -{ - if (m_pending_disconnect) - return true; - - const auto &c = *c_ptr; - Channel &chan = channels[c.channelnum]; - - const u32 chunksize_max = max_packet_size - - BASE_HEADER_SIZE - - RELIABLE_HEADER_SIZE; - - std::list> originals; - - if (c.raw) { - originals.emplace_back(c.data); - } else { - u16 split_seqnum = chan.readNextSplitSeqNum(); - makeAutoSplitPacket(c.data, chunksize_max, split_seqnum, &originals); - chan.setNextSplitSeqNum(split_seqnum); - } - - sanity_check(originals.size() < MAX_RELIABLE_WINDOW_SIZE); - - bool have_sequence_number = false; - bool have_initial_sequence_number = false; - std::queue toadd; - u16 initial_sequence_number = 0; - - for (SharedBuffer &original : originals) { - u16 seqnum = chan.getOutgoingSequenceNumber(have_sequence_number); - - /* oops, we don't have enough sequence numbers to send this packet */ - if (!have_sequence_number) - break; - - if (!have_initial_sequence_number) - { - initial_sequence_number = seqnum; - have_initial_sequence_number = true; - } - - SharedBuffer reliable = makeReliablePacket(original, seqnum); - - // Add base headers and make a packet - BufferedPacketPtr p = con::makePacket(address, reliable, - m_connection->GetProtocolID(), m_connection->GetPeerID(), - c.channelnum); - - toadd.push(p); - } - - if (have_sequence_number) { - while (!toadd.empty()) { - BufferedPacketPtr p = toadd.front(); - toadd.pop(); -// LOG(dout_con<getDesc() -// << " queuing reliable packet for peer_id: " << c.peer_id -// << " channel: " << (c.channelnum&0xFF) -// << " seqnum: " << readU16(&p.data[BASE_HEADER_SIZE+1]) -// << std::endl) - chan.queued_reliables.push(p); - } - sanity_check(chan.queued_reliables.size() < 0xFFFF); - return true; - } - - u16 packets_available = toadd.size(); - /* we didn't get a single sequence number no need to fill queue */ - if (!have_initial_sequence_number) { - LOG(derr_con << m_connection->getDesc() << "Ran out of sequence numbers!" << std::endl); - return false; - } - - while (!toadd.empty()) { - /* remove packet */ - toadd.pop(); - - bool successfully_put_back_sequence_number - = chan.putBackSequenceNumber( - (initial_sequence_number+toadd.size() % (SEQNUM_MAX+1))); - - FATAL_ERROR_IF(!successfully_put_back_sequence_number, "error"); - } - - u32 n_queued = chan.outgoing_reliables_sent.size(); - - LOG(dout_con<getDesc() - << " Windowsize exceeded on reliable sending " - << c.data.getSize() << " bytes" - << std::endl << "\t\tinitial_sequence_number: " - << initial_sequence_number - << std::endl << "\t\tgot at most : " - << packets_available << " packets" - << std::endl << "\t\tpackets queued : " - << n_queued - << std::endl); - - return false; -} - -void UDPPeer::RunCommandQueues( - unsigned int max_packet_size, - unsigned int maxtransfer) -{ - - for (Channel &channel : channels) { - - if ((!channel.queued_commands.empty()) && - (channel.queued_reliables.size() < maxtransfer)) { - try { - ConnectionCommandPtr c = channel.queued_commands.front(); - - LOG(dout_con << m_connection->getDesc() - << " processing queued reliable command " << std::endl); - - // Packet is processed, remove it from queue - if (processReliableSendCommand(c, max_packet_size)) { - channel.queued_commands.pop_front(); - } else { - LOG(dout_con << m_connection->getDesc() - << " Failed to queue packets for peer_id: " << c->peer_id - << ", delaying sending of " << c->data.getSize() - << " bytes" << std::endl); - } - } - catch (ItemNotFoundException &e) { - // intentionally empty - } - } - } -} - -u16 UDPPeer::getNextSplitSequenceNumber(u8 channel) -{ - assert(channel < CHANNEL_COUNT); // Pre-condition - return channels[channel].readNextSplitSeqNum(); -} - -void UDPPeer::setNextSplitSequenceNumber(u8 channel, u16 seqnum) -{ - assert(channel < CHANNEL_COUNT); // Pre-condition - channels[channel].setNextSplitSeqNum(seqnum); -} - -SharedBuffer UDPPeer::addSplitPacket(u8 channel, BufferedPacketPtr &toadd, - bool reliable) -{ - assert(channel < CHANNEL_COUNT); // Pre-condition - return channels[channel].incoming_splits.insert(toadd, reliable); -} - -/* - ConnectionEvent -*/ - -const char *ConnectionEvent::describe() const -{ - switch(type) { - case CONNEVENT_NONE: - return "CONNEVENT_NONE"; - case CONNEVENT_DATA_RECEIVED: - return "CONNEVENT_DATA_RECEIVED"; - case CONNEVENT_PEER_ADDED: - return "CONNEVENT_PEER_ADDED"; - case CONNEVENT_PEER_REMOVED: - return "CONNEVENT_PEER_REMOVED"; - case CONNEVENT_BIND_FAILED: - return "CONNEVENT_BIND_FAILED"; - } - return "Invalid ConnectionEvent"; -} - - -ConnectionEventPtr ConnectionEvent::create(ConnectionEventType type) -{ - return std::shared_ptr(new ConnectionEvent(type)); -} - -ConnectionEventPtr ConnectionEvent::dataReceived(session_t peer_id, const Buffer &data) -{ - auto e = create(CONNEVENT_DATA_RECEIVED); - e->peer_id = peer_id; - data.copyTo(e->data); - return e; -} - -ConnectionEventPtr ConnectionEvent::peerAdded(session_t peer_id, Address address) -{ - auto e = create(CONNEVENT_PEER_ADDED); - e->peer_id = peer_id; - e->address = address; - return e; -} - -ConnectionEventPtr ConnectionEvent::peerRemoved(session_t peer_id, bool is_timeout, Address address) -{ - auto e = create(CONNEVENT_PEER_REMOVED); - e->peer_id = peer_id; - e->timeout = is_timeout; - e->address = address; - return e; -} - -ConnectionEventPtr ConnectionEvent::bindFailed() -{ - return create(CONNEVENT_BIND_FAILED); -} - -/* - Connection -*/ - -Connection::Connection(u32 protocol_id, u32 max_packet_size, float timeout, - bool ipv6, PeerHandler *peerhandler) : - m_udpSocket(ipv6), - m_protocol_id(protocol_id), - m_sendThread(new ConnectionSendThread(max_packet_size, timeout)), - m_receiveThread(new ConnectionReceiveThread()), - m_bc_peerhandler(peerhandler) - -{ - /* Amount of time Receive() will wait for data, this is entirely different - * from the connection timeout */ - m_udpSocket.setTimeoutMs(500); - - m_sendThread->setParent(this); - m_receiveThread->setParent(this); - - m_sendThread->start(); - m_receiveThread->start(); -} - - -Connection::~Connection() -{ - m_shutting_down = true; - // request threads to stop - m_sendThread->stop(); - m_receiveThread->stop(); - - //TODO for some unkonwn reason send/receive threads do not exit as they're - // supposed to be but wait on peer timeout. To speed up shutdown we reduce - // timeout to half a second. - m_sendThread->setPeerTimeout(0.5); - - // wait for threads to finish - m_sendThread->wait(); - m_receiveThread->wait(); - - // Delete peers - for (auto &peer : m_peers) { - delete peer.second; - } -} - -/* Internal stuff */ - -void Connection::putEvent(ConnectionEventPtr e) -{ - assert(e->type != CONNEVENT_NONE); // Pre-condition - m_event_queue.push_back(e); -} - -void Connection::TriggerSend() -{ - m_sendThread->Trigger(); -} - -PeerHelper Connection::getPeerNoEx(session_t peer_id) -{ - MutexAutoLock peerlock(m_peers_mutex); - std::map::iterator node = m_peers.find(peer_id); - - if (node == m_peers.end()) { - return PeerHelper(NULL); - } - - // Error checking - FATAL_ERROR_IF(node->second->id != peer_id, "Invalid peer id"); - - return PeerHelper(node->second); -} - -/* find peer_id for address */ -session_t Connection::lookupPeer(const Address& sender) -{ - MutexAutoLock peerlock(m_peers_mutex); - for (auto &it: m_peers) { - Peer *peer = it.second; - if (peer->isPendingDeletion()) - continue; - - if (peer->getAddress() == sender) - return peer->id; - } - - return PEER_ID_INEXISTENT; -} - -u32 Connection::getActiveCount() -{ - MutexAutoLock peerlock(m_peers_mutex); - u32 count = 0; - for (auto &it : m_peers) { - Peer *peer = it.second; - if (peer->isPendingDeletion()) - continue; - if (peer->isHalfOpen()) - continue; - count++; - } - return count; -} - -bool Connection::deletePeer(session_t peer_id, bool timeout) -{ - Peer *peer = 0; - - /* lock list as short as possible */ - { - MutexAutoLock peerlock(m_peers_mutex); - if (m_peers.find(peer_id) == m_peers.end()) - return false; - peer = m_peers[peer_id]; - m_peers.erase(peer_id); - auto it = std::find(m_peer_ids.begin(), m_peer_ids.end(), peer_id); - m_peer_ids.erase(it); - } - - // Create event - putEvent(ConnectionEvent::peerRemoved(peer_id, timeout, peer->getAddress())); - - peer->Drop(); - return true; -} - -/* Interface */ - -ConnectionEventPtr Connection::waitEvent(u32 timeout_ms) -{ - try { - return m_event_queue.pop_front(timeout_ms); - } catch(ItemNotFoundException &ex) { - return ConnectionEvent::create(CONNEVENT_NONE); - } -} - -void Connection::putCommand(ConnectionCommandPtr c) -{ - if (!m_shutting_down) { - m_command_queue.push_back(c); - m_sendThread->Trigger(); - } -} - -void Connection::Serve(Address bind_addr) -{ - putCommand(ConnectionCommand::serve(bind_addr)); -} - -void Connection::Connect(Address address) -{ - putCommand(ConnectionCommand::connect(address)); -} - -bool Connection::Connected() -{ - MutexAutoLock peerlock(m_peers_mutex); - - if (m_peers.size() != 1) - return false; - - std::map::iterator node = m_peers.find(PEER_ID_SERVER); - if (node == m_peers.end()) - return false; - - if (m_peer_id == PEER_ID_INEXISTENT) - return false; - - return true; -} - -void Connection::Disconnect() -{ - putCommand(ConnectionCommand::disconnect()); -} - -bool Connection::ReceiveTimeoutMs(NetworkPacket *pkt, u32 timeout_ms) -{ - /* - Note that this function can potentially wait infinitely if non-data - events keep happening before the timeout expires. - This is not considered to be a problem (is it?) - */ - for(;;) { - ConnectionEventPtr e_ptr = waitEvent(timeout_ms); - const ConnectionEvent &e = *e_ptr; - - if (e.type != CONNEVENT_NONE) { - LOG(dout_con << getDesc() << ": Receive: got event: " - << e.describe() << std::endl); - } - - switch (e.type) { - case CONNEVENT_NONE: - return false; - case CONNEVENT_DATA_RECEIVED: - // Data size is lesser than command size, ignoring packet - if (e.data.getSize() < 2) { - continue; - } - - pkt->putRawPacket(*e.data, e.data.getSize(), e.peer_id); - return true; - case CONNEVENT_PEER_ADDED: { - UDPPeer tmp(e.peer_id, e.address, this); - if (m_bc_peerhandler) - m_bc_peerhandler->peerAdded(&tmp); - continue; - } - case CONNEVENT_PEER_REMOVED: { - UDPPeer tmp(e.peer_id, e.address, this); - if (m_bc_peerhandler) - m_bc_peerhandler->deletingPeer(&tmp, e.timeout); - continue; - } - case CONNEVENT_BIND_FAILED: - throw ConnectionBindFailed("Failed to bind socket " - "(port already in use?)"); - } - } - return false; -} - -void Connection::Receive(NetworkPacket *pkt) -{ - bool any = ReceiveTimeoutMs(pkt, m_bc_receive_timeout); - if (!any) - throw NoIncomingDataException("No incoming data"); -} - -bool Connection::TryReceive(NetworkPacket *pkt) -{ - return ReceiveTimeoutMs(pkt, 0); -} - -void Connection::Send(session_t peer_id, u8 channelnum, - NetworkPacket *pkt, bool reliable) -{ - assert(channelnum < CHANNEL_COUNT); // Pre-condition - - // approximate check similar to UDPPeer::processReliableSendCommand() - // to get nicer errors / backtraces if this happens. - if (reliable && pkt->getSize() > MAX_RELIABLE_WINDOW_SIZE*512) { - std::ostringstream oss; - oss << "Packet too big for window, peer_id=" << peer_id - << " command=" << pkt->getCommand() << " size=" << pkt->getSize(); - FATAL_ERROR(oss.str().c_str()); - } - - putCommand(ConnectionCommand::send(peer_id, channelnum, pkt, reliable)); -} - -Address Connection::GetPeerAddress(session_t peer_id) -{ - PeerHelper peer = getPeerNoEx(peer_id); - - if (!peer) - throw PeerNotFoundException("No address for peer found!"); - return peer->getAddress(); -} - -float Connection::getPeerStat(session_t peer_id, rtt_stat_type type) -{ - PeerHelper peer = getPeerNoEx(peer_id); - if (!peer) - return -1; - return peer->getStat(type); -} - -float Connection::getLocalStat(rate_stat_type type) -{ - PeerHelper peer = getPeerNoEx(PEER_ID_SERVER); - - FATAL_ERROR_IF(!peer, "Connection::getLocalStat we couldn't get our own peer? are you serious???"); - - float retval = 0; - - for (Channel &channel : dynamic_cast(&peer)->channels) { - switch(type) { - case CUR_DL_RATE: - retval += channel.getCurrentDownloadRateKB(); - break; - case AVG_DL_RATE: - retval += channel.getAvgDownloadRateKB(); - break; - case CUR_INC_RATE: - retval += channel.getCurrentIncomingRateKB(); - break; - case AVG_INC_RATE: - retval += channel.getAvgIncomingRateKB(); - break; - case AVG_LOSS_RATE: - retval += channel.getAvgLossRateKB(); - break; - case CUR_LOSS_RATE: - retval += channel.getCurrentLossRateKB(); - break; - default: - FATAL_ERROR("Connection::getLocalStat Invalid stat type"); - } - } - return retval; -} - -session_t Connection::createPeer(const Address &sender, int fd) -{ - // Somebody wants to make a new connection - - // Get a unique peer id - const session_t minimum = 2; - const session_t overflow = MAX_UDP_PEERS; - - /* - Find an unused peer id - */ - - MutexAutoLock lock(m_peers_mutex); - session_t peer_id_new; - for (int tries = 0; tries < 100; tries++) { - peer_id_new = myrand_range(minimum, overflow - 1); - if (m_peers.find(peer_id_new) == m_peers.end()) - break; - } - if (m_peers.find(peer_id_new) != m_peers.end()) { - errorstream << getDesc() << " ran out of peer ids" << std::endl; - return PEER_ID_INEXISTENT; - } - - // Create a peer - Peer *peer = 0; - peer = new UDPPeer(peer_id_new, sender, this); - - m_peers[peer->id] = peer; - m_peer_ids.push_back(peer->id); - - LOG(dout_con << getDesc() - << "createPeer(): giving peer_id=" << peer_id_new << std::endl); - - { - Buffer reply(4); - writeU8(&reply[0], PACKET_TYPE_CONTROL); - writeU8(&reply[1], CONTROLTYPE_SET_PEER_ID); - writeU16(&reply[2], peer_id_new); - putCommand(ConnectionCommand::createPeer(peer_id_new, reply)); - } - - // Create peer addition event - putEvent(ConnectionEvent::peerAdded(peer_id_new, sender)); - - // We're now talking to a valid peer_id - return peer_id_new; -} - -const std::string Connection::getDesc() -{ - MutexAutoLock _(m_info_mutex); - return std::string("con(")+ - itos(m_udpSocket.GetHandle())+"/"+itos(m_peer_id)+")"; -} - -void Connection::DisconnectPeer(session_t peer_id) -{ - putCommand(ConnectionCommand::disconnect_peer(peer_id)); -} - -void Connection::doResendOne(session_t peer_id) -{ - assert(peer_id != PEER_ID_INEXISTENT); - putCommand(ConnectionCommand::resend_one(peer_id)); -} - -void Connection::sendAck(session_t peer_id, u8 channelnum, u16 seqnum) -{ - assert(channelnum < CHANNEL_COUNT); // Pre-condition - - LOG(dout_con< ack(4); - writeU8(&ack[0], PACKET_TYPE_CONTROL); - writeU8(&ack[1], CONTROLTYPE_ACK); - writeU16(&ack[2], seqnum); - - putCommand(ConnectionCommand::ack(peer_id, channelnum, ack)); - m_sendThread->Trigger(); -} - -UDPPeer* Connection::createServerPeer(const Address &address) -{ - if (ConnectedToServer()) - throw ConnectionException("Already connected to a server"); - - UDPPeer *peer = new UDPPeer(PEER_ID_SERVER, address, this); - peer->SetFullyOpen(); - - { - MutexAutoLock lock(m_peers_mutex); - m_peers[peer->id] = peer; - m_peer_ids.push_back(peer->id); - } - - return peer; -} - -} // namespace diff --git a/src/network/connection.h b/src/network/connection.h index a9e5a18c5..bec6f98f0 100644 --- a/src/network/connection.h +++ b/src/network/connection.h @@ -1,44 +1,26 @@ -/* -Minetest -Copyright (C) 2013 celeron55, Perttu Ahola - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation; either version 2.1 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ +// Minetest +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "irrlichttypes.h" -#include "peerhandler.h" #include "socket.h" -#include "constants.h" -#include "util/pointer.h" -#include "util/container.h" -#include "util/numeric.h" -#include "porting.h" -#include "networkprotocol.h" -#include -#include -#include +#include "networkprotocol.h" // session_t class NetworkPacket; +class PeerHandler; namespace con { -class ConnectionReceiveThread; -class ConnectionSendThread; +enum rtt_stat_type { + MIN_RTT, + MAX_RTT, + AVG_RTT, + MIN_JITTER, + MAX_JITTER, + AVG_JITTER +}; enum rate_stat_type { CUR_DL_RATE, @@ -49,295 +31,43 @@ enum rate_stat_type { AVG_LOSS_RATE, }; -class Peer; - -// FIXME: Peer refcounting should generally be replaced by std::shared_ptr -class PeerHelper -{ +class IPeer { public: - PeerHelper() = default; - inline PeerHelper(Peer *peer) { *this = peer; } - ~PeerHelper(); + // Unique id of the peer + const session_t id; - PeerHelper& operator=(Peer *peer); - inline Peer* operator->() const { return m_peer; } - inline Peer* operator&() const { return m_peer; } - - inline bool operator!() { return !m_peer; } - inline bool operator!=(std::nullptr_t) { return !!m_peer; } - -private: - Peer *m_peer = nullptr; -}; - -/* - Connection -*/ - -enum ConnectionEventType { - CONNEVENT_NONE, - CONNEVENT_DATA_RECEIVED, - CONNEVENT_PEER_ADDED, - CONNEVENT_PEER_REMOVED, - CONNEVENT_BIND_FAILED, -}; - -struct ConnectionEvent; -typedef std::shared_ptr ConnectionEventPtr; - -// This is very similar to ConnectionCommand -struct ConnectionEvent -{ - const ConnectionEventType type; - session_t peer_id = 0; - Buffer data; - bool timeout = false; - Address address; - - // We don't want to copy "data" - DISABLE_CLASS_COPY(ConnectionEvent); - - static ConnectionEventPtr create(ConnectionEventType type); - static ConnectionEventPtr dataReceived(session_t peer_id, const Buffer &data); - static ConnectionEventPtr peerAdded(session_t peer_id, Address address); - static ConnectionEventPtr peerRemoved(session_t peer_id, bool is_timeout, Address address); - static ConnectionEventPtr bindFailed(); - - const char *describe() const; - -private: - ConnectionEvent(ConnectionEventType type_) : - type(type_) {} -}; - -struct ConnectionCommand; -typedef std::shared_ptr ConnectionCommandPtr; - -struct BufferedPacket; -typedef std::shared_ptr BufferedPacketPtr; - -class Connection; -class PeerHandler; - -class Peer { - public: - friend class PeerHelper; - - virtual ~Peer() { - MutexAutoLock usage_lock(m_exclusive_access_mutex); - FATAL_ERROR_IF(m_usage != 0, "Reference counting failure"); - } - - // Unique id of the peer - const session_t id; - - void Drop(); - - virtual void PutReliableSendCommand(ConnectionCommandPtr &c, - unsigned int max_packet_size) {}; - - virtual const Address &getAddress() const = 0; - - bool isPendingDeletion() const { - MutexAutoLock lock(m_exclusive_access_mutex); - return m_pending_deletion; - } - void ResetTimeout() { - MutexAutoLock lock(m_exclusive_access_mutex); - m_timeout_counter = 0; - } - - bool isHalfOpen() const { - MutexAutoLock lock(m_exclusive_access_mutex); - return m_half_open; - } - void SetFullyOpen() { - MutexAutoLock lock(m_exclusive_access_mutex); - m_half_open = false; - } - - virtual bool isTimedOut(float timeout, std::string &reason); - - unsigned int m_increment_packets_remaining = 0; - - virtual u16 getNextSplitSequenceNumber(u8 channel) { return 0; }; - virtual void setNextSplitSequenceNumber(u8 channel, u16 seqnum) {}; - virtual SharedBuffer addSplitPacket(u8 channel, BufferedPacketPtr &toadd, - bool reliable) - { - FATAL_ERROR("unimplemented in abstract class"); - } - - virtual bool Ping(float dtime, SharedBuffer& data) { return false; }; - - virtual float getStat(rtt_stat_type type) const { - switch (type) { - case MIN_RTT: - return m_rtt.min_rtt; - case MAX_RTT: - return m_rtt.max_rtt; - case AVG_RTT: - return m_rtt.avg_rtt; - case MIN_JITTER: - return m_rtt.jitter_min; - case MAX_JITTER: - return m_rtt.jitter_max; - case AVG_JITTER: - return m_rtt.jitter_avg; - } - return -1; - } - - protected: - Peer(session_t id, const Address &address, Connection *connection) : - id(id), - m_connection(connection), - address(address), - m_last_timeout_check(porting::getTimeMs()) - { - } - - virtual void reportRTT(float rtt) {}; - - void RTTStatistics(float rtt, - const std::string &profiler_id = "", - unsigned int num_samples = 1000); - - bool IncUseCount(); - void DecUseCount(); - - mutable std::mutex m_exclusive_access_mutex; - - bool m_pending_deletion = false; - - Connection *m_connection; - - // Address of the peer - Address address; - - // Ping timer - float m_ping_timer = 0.0f; - - private: - struct rttstats { - float jitter_min = FLT_MAX; - float jitter_max = 0.0f; - float jitter_avg = -1.0f; - float min_rtt = FLT_MAX; - float max_rtt = 0.0f; - float avg_rtt = -1.0f; - }; - - rttstats m_rtt; - float m_last_rtt = -1.0f; - - /* - Until the peer has communicated with us using their assigned peer id - the connection is considered half-open. - During this time we inhibit re-sending any reliables or pings. This - is to avoid spending too many resources on a potential DoS attack - and to make sure Minetest servers are not useful for UDP amplificiation. - */ - bool m_half_open = true; - - // current usage count - unsigned int m_usage = 0; - - // Seconds from last receive - float m_timeout_counter = 0.0f; - - u64 m_last_timeout_check; -}; - -class UDPPeer; - -class Connection -{ -public: - friend class ConnectionSendThread; - friend class ConnectionReceiveThread; - - Connection(u32 protocol_id, u32 max_packet_size, float timeout, bool ipv6, - PeerHandler *peerhandler); - ~Connection(); - - /* Interface */ - ConnectionEventPtr waitEvent(u32 timeout_ms); - - void putCommand(ConnectionCommandPtr c); - - void SetTimeoutMs(u32 timeout) { m_bc_receive_timeout = timeout; } - void Serve(Address bind_addr); - void Connect(Address address); - bool Connected(); - void Disconnect(); - bool ReceiveTimeoutMs(NetworkPacket *pkt, u32 timeout_ms); - void Receive(NetworkPacket *pkt); - bool TryReceive(NetworkPacket *pkt); - void Send(session_t peer_id, u8 channelnum, NetworkPacket *pkt, bool reliable); - session_t GetPeerID() const { return m_peer_id; } - Address GetPeerAddress(session_t peer_id); - float getPeerStat(session_t peer_id, rtt_stat_type type); - float getLocalStat(rate_stat_type type); - u32 GetProtocolID() const { return m_protocol_id; }; - const std::string getDesc(); - void DisconnectPeer(session_t peer_id); + virtual const Address &getAddress() const = 0; protected: - PeerHelper getPeerNoEx(session_t peer_id); - session_t lookupPeer(const Address& sender); - - session_t createPeer(const Address& sender, int fd); - UDPPeer* createServerPeer(const Address& sender); - bool deletePeer(session_t peer_id, bool timeout); - - void SetPeerID(session_t id) { m_peer_id = id; } - - void doResendOne(session_t peer_id); - - void sendAck(session_t peer_id, u8 channelnum, u16 seqnum); - - std::vector getPeerIDs() - { - MutexAutoLock peerlock(m_peers_mutex); - return m_peer_ids; - } - - u32 getActiveCount(); - - UDPSocket m_udpSocket; - // Command queue: user -> SendThread - MutexedQueue m_command_queue; - - void putEvent(ConnectionEventPtr e); - - void TriggerSend(); - - bool ConnectedToServer() - { - return getPeerNoEx(PEER_ID_SERVER) != nullptr; - } -private: - // Event queue: ReceiveThread -> user - MutexedQueue m_event_queue; - - session_t m_peer_id = 0; - u32 m_protocol_id; - - std::map m_peers; - std::vector m_peer_ids; - std::mutex m_peers_mutex; - - std::unique_ptr m_sendThread; - std::unique_ptr m_receiveThread; - - mutable std::mutex m_info_mutex; - - // Backwards compatibility - PeerHandler *m_bc_peerhandler; - u32 m_bc_receive_timeout = 0; - - bool m_shutting_down = false; + IPeer(session_t id) : id(id) {} + ~IPeer() {} }; +class IConnection +{ +public: + virtual ~IConnection() = default; + + virtual void Serve(Address bind_addr) = 0; + virtual void Connect(Address address) = 0; + virtual bool Connected() = 0; + virtual void Disconnect() = 0; + virtual void DisconnectPeer(session_t peer_id) = 0; + + virtual bool ReceiveTimeoutMs(NetworkPacket *pkt, u32 timeout_ms) = 0; + bool TryReceive(NetworkPacket *pkt) { + return ReceiveTimeoutMs(pkt, 0); + } + + virtual void Send(session_t peer_id, u8 channelnum, NetworkPacket *pkt, bool reliable) = 0; + + virtual session_t GetPeerID() const = 0; + virtual Address GetPeerAddress(session_t peer_id) = 0; + virtual float getPeerStat(session_t peer_id, rtt_stat_type type) = 0; + virtual float getLocalStat(rate_stat_type type) = 0; +}; + +// MTP = Minetest Protocol +IConnection *createMTP(float timeout, bool ipv6, PeerHandler *handler); + } // namespace diff --git a/src/network/mtp/impl.cpp b/src/network/mtp/impl.cpp new file mode 100644 index 000000000..1ef5eb853 --- /dev/null +++ b/src/network/mtp/impl.cpp @@ -0,0 +1,1674 @@ +/* +Minetest +Copyright (C) 2013 celeron55, Perttu Ahola + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include +#include +#include +#include +#include "network/mtp/internal.h" +#include "serialization.h" +#include "log.h" +#include "porting.h" +#include "network/mtp/threads.h" +#include "network/peerhandler.h" +#include "network/networkpacket.h" +#include "util/serialize.h" +#include "util/numeric.h" +#include "util/string.h" +#include "settings.h" +#include "profiler.h" + +namespace con +{ + +/******************************************************************************/ +/* defines used for debugging and profiling */ +/******************************************************************************/ +#ifdef NDEBUG + #define PROFILE(a) +#else + #define PROFILE(a) a +#endif + +// TODO: Clean this up. +#define LOG(a) a + +#define PING_TIMEOUT 5.0f + +// exponent base +#define RESEND_SCALE_BASE 1.5f + +// since spacing is exponential the numbers here shouldn't be too high +// (it's okay to start out quick) +#define RESEND_TIMEOUT_MIN 0.1f +#define RESEND_TIMEOUT_MAX 2.0f +#define RESEND_TIMEOUT_FACTOR 2 + +u16 BufferedPacket::getSeqnum() const +{ + if (size() < BASE_HEADER_SIZE + 3) + return 0; // should never happen + + return readU16(&data[BASE_HEADER_SIZE + 1]); +} + +BufferedPacketPtr makePacket(const Address &address, const SharedBuffer &data, + u32 protocol_id, session_t sender_peer_id, u8 channel) +{ + u32 packet_size = data.getSize() + BASE_HEADER_SIZE; + + auto p = std::make_shared(packet_size); + p->address = address; + + writeU32(&p->data[0], protocol_id); + writeU16(&p->data[4], sender_peer_id); + writeU8(&p->data[6], channel); + + memcpy(&p->data[BASE_HEADER_SIZE], *data, data.getSize()); + + return p; +} + +SharedBuffer makeOriginalPacket(const SharedBuffer &data) +{ + u32 header_size = 1; + u32 packet_size = data.getSize() + header_size; + SharedBuffer b(packet_size); + + writeU8(&(b[0]), PACKET_TYPE_ORIGINAL); + if (data.getSize() > 0) { + memcpy(&(b[header_size]), *data, data.getSize()); + } + return b; +} + +// Split data in chunks and add TYPE_SPLIT headers to them +void makeSplitPacket(const SharedBuffer &data, u32 chunksize_max, u16 seqnum, + std::list> *chunks) +{ + // Chunk packets, containing the TYPE_SPLIT header + const u32 chunk_header_size = 7; + const u32 maximum_data_size = chunksize_max - chunk_header_size; + u32 start = 0, end = 0; + u16 chunk_num = 0; + do { + end = start + maximum_data_size - 1; + if (end > data.getSize() - 1) + end = data.getSize() - 1; + + u32 payload_size = end - start + 1; + u32 packet_size = chunk_header_size + payload_size; + + SharedBuffer chunk(packet_size); + + writeU8(&chunk[0], PACKET_TYPE_SPLIT); + writeU16(&chunk[1], seqnum); + // [3] u16 chunk_count is written at next stage + writeU16(&chunk[5], chunk_num); + memcpy(&chunk[chunk_header_size], &data[start], payload_size); + + chunks->push_back(chunk); + + start = end + 1; + sanity_check(chunk_num < 0xFFFF); // overflow + chunk_num++; + } + while (end != data.getSize() - 1); + + for (auto &chunk : *chunks) { + // Write chunk_count + writeU16(&chunk[3], chunk_num); + } +} + +void makeAutoSplitPacket(const SharedBuffer &data, u32 chunksize_max, + u16 &split_seqnum, std::list> *list) +{ + u32 original_header_size = 1; + + if (data.getSize() + original_header_size > chunksize_max) { + makeSplitPacket(data, chunksize_max, split_seqnum, list); + split_seqnum++; + return; + } + + list->push_back(makeOriginalPacket(data)); +} + +SharedBuffer makeReliablePacket(const SharedBuffer &data, u16 seqnum) +{ + u32 header_size = 3; + u32 packet_size = data.getSize() + header_size; + SharedBuffer b(packet_size); + + writeU8(&b[0], PACKET_TYPE_RELIABLE); + writeU16(&b[1], seqnum); + + memcpy(&b[header_size], *data, data.getSize()); + + return b; +} + +/* + ReliablePacketBuffer +*/ + +void ReliablePacketBuffer::print() +{ + MutexAutoLock listlock(m_list_mutex); + LOG(dout_con<<"Dump of ReliablePacketBuffer:" << std::endl); + unsigned int index = 0; + for (BufferedPacketPtr &packet : m_list) { + LOG(dout_con<getSeqnum() << std::endl); + index++; + } +} + +bool ReliablePacketBuffer::empty() +{ + MutexAutoLock listlock(m_list_mutex); + return m_list.empty(); +} + +u32 ReliablePacketBuffer::size() +{ + MutexAutoLock listlock(m_list_mutex); + return m_list.size(); +} + +ReliablePacketBuffer::FindResult ReliablePacketBuffer::findPacketNoLock(u16 seqnum) +{ + for (auto it = m_list.begin(); it != m_list.end(); ++it) { + if ((*it)->getSeqnum() == seqnum) + return it; + } + return m_list.end(); +} + +bool ReliablePacketBuffer::getFirstSeqnum(u16& result) +{ + MutexAutoLock listlock(m_list_mutex); + if (m_list.empty()) + return false; + result = m_list.front()->getSeqnum(); + return true; +} + +BufferedPacketPtr ReliablePacketBuffer::popFirst() +{ + MutexAutoLock listlock(m_list_mutex); + if (m_list.empty()) + throw NotFoundException("Buffer is empty"); + + BufferedPacketPtr p(m_list.front()); + m_list.pop_front(); + + if (m_list.empty()) { + m_oldest_non_answered_ack = 0; + } else { + m_oldest_non_answered_ack = m_list.front()->getSeqnum(); + } + return p; +} + +BufferedPacketPtr ReliablePacketBuffer::popSeqnum(u16 seqnum) +{ + MutexAutoLock listlock(m_list_mutex); + auto r = findPacketNoLock(seqnum); + if (r == m_list.end()) { + LOG(dout_con<<"Sequence number: " << seqnum + << " not found in reliable buffer"<getSeqnum(); + } + return p; +} + +void ReliablePacketBuffer::insert(BufferedPacketPtr &p_ptr, u16 next_expected) +{ + MutexAutoLock listlock(m_list_mutex); + const BufferedPacket &p = *p_ptr; + + if (p.size() < BASE_HEADER_SIZE + 3) { + errorstream << "ReliablePacketBuffer::insert(): Invalid data size for " + "reliable packet" << std::endl; + return; + } + u8 type = readU8(&p.data[BASE_HEADER_SIZE + 0]); + if (type != PACKET_TYPE_RELIABLE) { + errorstream << "ReliablePacketBuffer::insert(): type is not reliable" + << std::endl; + return; + } + const u16 seqnum = p.getSeqnum(); + + if (!seqnum_in_window(seqnum, next_expected, MAX_RELIABLE_WINDOW_SIZE)) { + errorstream << "ReliablePacketBuffer::insert(): seqnum is outside of " + "expected window " << std::endl; + return; + } + if (seqnum == next_expected) { + errorstream << "ReliablePacketBuffer::insert(): seqnum is next expected" + << std::endl; + return; + } + + sanity_check(m_list.size() <= SEQNUM_MAX); // FIXME: Handle the error? + + // Find the right place for the packet and insert it there + // If list is empty, just add it + if (m_list.empty()) { + m_list.push_back(p_ptr); + m_oldest_non_answered_ack = seqnum; + // Done. + return; + } + + // Otherwise find the right place + auto it = m_list.begin(); + // Find the first packet in the list which has a higher seqnum + u16 s = (*it)->getSeqnum(); + + /* case seqnum is smaller then next_expected seqnum */ + /* this is true e.g. on wrap around */ + if (seqnum < next_expected) { + while(((s < seqnum) || (s >= next_expected)) && (it != m_list.end())) { + ++it; + if (it != m_list.end()) + s = (*it)->getSeqnum(); + } + } + /* non wrap around case (at least for incoming and next_expected */ + else + { + while(((s < seqnum) && (s >= next_expected)) && (it != m_list.end())) { + ++it; + if (it != m_list.end()) + s = (*it)->getSeqnum(); + } + } + + if (s == seqnum) { + /* nothing to do this seems to be a resent packet */ + /* for paranoia reason data should be compared */ + auto &i = *it; + if ( + (i->getSeqnum() != seqnum) || + (i->size() != p.size()) || + (i->address != p.address) + ) + { + /* if this happens your maximum transfer window may be to big */ + char buf[200]; + snprintf(buf, sizeof(buf), + "Duplicated seqnum %d non matching packet detected:\n", + seqnum); + warningstream << buf; + snprintf(buf, sizeof(buf), + "Old: seqnum: %05d size: %04zu, address: %s\n", + i->getSeqnum(), i->size(), + i->address.serializeString().c_str()); + warningstream << buf; + snprintf(buf, sizeof(buf), + "New: seqnum: %05d size: %04zu, address: %s\n", + p.getSeqnum(), p.size(), + p.address.serializeString().c_str()); + warningstream << buf << std::flush; + throw IncomingDataCorruption("duplicated packet isn't same as original one"); + } + } + /* insert or push back */ + else if (it != m_list.end()) { + m_list.insert(it, p_ptr); + } else { + m_list.push_back(p_ptr); + } + + /* update last packet number */ + m_oldest_non_answered_ack = m_list.front()->getSeqnum(); +} + +void ReliablePacketBuffer::incrementTimeouts(float dtime) +{ + MutexAutoLock listlock(m_list_mutex); + for (auto &packet : m_list) { + packet->time += dtime; + packet->totaltime += dtime; + } +} + +u32 ReliablePacketBuffer::getTimedOuts(float timeout) +{ + MutexAutoLock listlock(m_list_mutex); + u32 count = 0; + for (auto &packet : m_list) { + if (packet->totaltime >= timeout) + count++; + } + return count; +} + +std::vector> + ReliablePacketBuffer::getResend(float timeout, u32 max_packets) +{ + MutexAutoLock listlock(m_list_mutex); + std::vector> timed_outs; + for (auto &packet : m_list) { + // resend time scales exponentially with each cycle + const float pkt_timeout = timeout * powf(RESEND_SCALE_BASE, packet->resend_count); + + if (packet->time < pkt_timeout) + continue; + + // caller will resend packet so reset time and increase counter + packet->time = 0.0f; + packet->resend_count++; + + timed_outs.emplace_back(packet); + + if (timed_outs.size() >= max_packets) + break; + } + return timed_outs; +} + +/* + IncomingSplitPacket +*/ + +bool IncomingSplitPacket::insert(u32 chunk_num, SharedBuffer &chunkdata) +{ + sanity_check(chunk_num < chunk_count); + + // If chunk already exists, ignore it. + // Sometimes two identical packets may arrive when there is network + // lag and the server re-sends stuff. + if (chunks.find(chunk_num) != chunks.end()) + return false; + + // Set chunk data in buffer + chunks[chunk_num] = chunkdata; + + return true; +} + +SharedBuffer IncomingSplitPacket::reassemble() +{ + sanity_check(allReceived()); + + // Calculate total size + u32 totalsize = 0; + for (const auto &chunk : chunks) + totalsize += chunk.second.getSize(); + + SharedBuffer fulldata(totalsize); + + // Copy chunks to data buffer + u32 start = 0; + for (u32 chunk_i = 0; chunk_i < chunk_count; chunk_i++) { + const SharedBuffer &buf = chunks[chunk_i]; + memcpy(&fulldata[start], *buf, buf.getSize()); + start += buf.getSize(); + } + + return fulldata; +} + +/* + IncomingSplitBuffer +*/ + +IncomingSplitBuffer::~IncomingSplitBuffer() +{ + MutexAutoLock listlock(m_map_mutex); + for (auto &i : m_buf) { + delete i.second; + } +} + +SharedBuffer IncomingSplitBuffer::insert(BufferedPacketPtr &p_ptr, bool reliable) +{ + MutexAutoLock listlock(m_map_mutex); + const BufferedPacket &p = *p_ptr; + + u32 headersize = BASE_HEADER_SIZE + 7; + if (p.size() < headersize) { + errorstream << "Invalid data size for split packet" << std::endl; + return SharedBuffer(); + } + u8 type = readU8(&p.data[BASE_HEADER_SIZE+0]); + u16 seqnum = readU16(&p.data[BASE_HEADER_SIZE+1]); + u16 chunk_count = readU16(&p.data[BASE_HEADER_SIZE+3]); + u16 chunk_num = readU16(&p.data[BASE_HEADER_SIZE+5]); + + if (type != PACKET_TYPE_SPLIT) { + errorstream << "IncomingSplitBuffer::insert(): type is not split" + << std::endl; + return SharedBuffer(); + } + if (chunk_num >= chunk_count) { + errorstream << "IncomingSplitBuffer::insert(): chunk_num=" << chunk_num + << " >= chunk_count=" << chunk_count << std::endl; + return SharedBuffer(); + } + + // Add if doesn't exist + IncomingSplitPacket *sp; + if (m_buf.find(seqnum) == m_buf.end()) { + sp = new IncomingSplitPacket(chunk_count, reliable); + m_buf[seqnum] = sp; + } else { + sp = m_buf[seqnum]; + } + + if (chunk_count != sp->chunk_count) { + errorstream << "IncomingSplitBuffer::insert(): chunk_count=" + << chunk_count << " != sp->chunk_count=" << sp->chunk_count + << std::endl; + return SharedBuffer(); + } + if (reliable != sp->reliable) + LOG(derr_con<<"Connection: WARNING: reliable="<reliable="<reliable + < chunkdata(chunkdatasize); + memcpy(*chunkdata, &(p.data[headersize]), chunkdatasize); + + if (!sp->insert(chunk_num, chunkdata)) + return SharedBuffer(); + + // If not all chunks are received, return empty buffer + if (!sp->allReceived()) + return SharedBuffer(); + + SharedBuffer fulldata = sp->reassemble(); + + // Remove sp from buffer + m_buf.erase(seqnum); + delete sp; + + return fulldata; +} + +void IncomingSplitBuffer::removeUnreliableTimedOuts(float dtime, float timeout) +{ + MutexAutoLock listlock(m_map_mutex); + std::vector remove_queue; + { + for (const auto &i : m_buf) { + IncomingSplitPacket *p = i.second; + // Reliable ones are not removed by timeout + if (p->reliable) + continue; + p->time += dtime; + if (p->time >= timeout) + remove_queue.push_back(i.first); + } + } + for (u16 j : remove_queue) { + LOG(dout_con<<"NOTE: Removing timed out unreliable split packet"<second; + m_buf.erase(it); + } +} + +/* + ConnectionCommand + */ + +ConnectionCommandPtr ConnectionCommand::create(ConnectionCommandType type) +{ + return ConnectionCommandPtr(new ConnectionCommand(type)); +} + +ConnectionCommandPtr ConnectionCommand::serve(Address address) +{ + auto c = create(CONNCMD_SERVE); + c->address = address; + return c; +} + +ConnectionCommandPtr ConnectionCommand::connect(Address address) +{ + auto c = create(CONNCMD_CONNECT); + c->address = address; + return c; +} + +ConnectionCommandPtr ConnectionCommand::disconnect() +{ + return create(CONNCMD_DISCONNECT); +} + +ConnectionCommandPtr ConnectionCommand::disconnect_peer(session_t peer_id) +{ + auto c = create(CONNCMD_DISCONNECT_PEER); + c->peer_id = peer_id; + return c; +} + +ConnectionCommandPtr ConnectionCommand::resend_one(session_t peer_id) +{ + auto c = create(CONNCMD_RESEND_ONE); + c->peer_id = peer_id; + c->channelnum = 0; // must be same as createPeer + c->reliable = true; + return c; +} + +ConnectionCommandPtr ConnectionCommand::send(session_t peer_id, u8 channelnum, + NetworkPacket *pkt, bool reliable) +{ + auto c = create(CONNCMD_SEND); + c->peer_id = peer_id; + c->channelnum = channelnum; + c->reliable = reliable; + c->data = pkt->oldForgePacket(); + return c; +} + +ConnectionCommandPtr ConnectionCommand::ack(session_t peer_id, u8 channelnum, const Buffer &data) +{ + auto c = create(CONCMD_ACK); + c->peer_id = peer_id; + c->channelnum = channelnum; + c->reliable = false; + data.copyTo(c->data); + return c; +} + +ConnectionCommandPtr ConnectionCommand::createPeer(session_t peer_id, const Buffer &data) +{ + auto c = create(CONCMD_CREATE_PEER); + c->peer_id = peer_id; + c->channelnum = 0; + c->reliable = true; + c->raw = true; + data.copyTo(c->data); + return c; +} + +/* + Channel +*/ + +u16 Channel::readNextIncomingSeqNum() +{ + MutexAutoLock internal(m_internal_mutex); + return next_incoming_seqnum; +} + +u16 Channel::incNextIncomingSeqNum() +{ + MutexAutoLock internal(m_internal_mutex); + u16 retval = next_incoming_seqnum; + next_incoming_seqnum++; + return retval; +} + +u16 Channel::readNextSplitSeqNum() +{ + MutexAutoLock internal(m_internal_mutex); + return next_outgoing_split_seqnum; +} +void Channel::setNextSplitSeqNum(u16 seqnum) +{ + MutexAutoLock internal(m_internal_mutex); + next_outgoing_split_seqnum = seqnum; +} + +u16 Channel::getOutgoingSequenceNumber(bool& successful) +{ + MutexAutoLock internal(m_internal_mutex); + + u16 retval = next_outgoing_seqnum; + successful = false; + + /* shortcut if there ain't any packet in outgoing list */ + if (outgoing_reliables_sent.empty()) { + successful = true; + next_outgoing_seqnum++; + return retval; + } + + u16 lowest_unacked_seqnumber; + if (outgoing_reliables_sent.getFirstSeqnum(lowest_unacked_seqnumber)) { + if (lowest_unacked_seqnumber < next_outgoing_seqnum) { + // ugly cast but this one is required in order to tell compiler we + // know about difference of two unsigned may be negative in general + // but we already made sure it won't happen in this case + if (((u16)(next_outgoing_seqnum - lowest_unacked_seqnumber)) > m_window_size) { + return 0; + } + } else { + // ugly cast but this one is required in order to tell compiler we + // know about difference of two unsigned may be negative in general + // but we already made sure it won't happen in this case + if ((next_outgoing_seqnum + (u16)(SEQNUM_MAX - lowest_unacked_seqnumber)) > + m_window_size) { + return 0; + } + } + } + + successful = true; + next_outgoing_seqnum++; + return retval; +} + +u16 Channel::readOutgoingSequenceNumber() +{ + MutexAutoLock internal(m_internal_mutex); + return next_outgoing_seqnum; +} + +bool Channel::putBackSequenceNumber(u16 seqnum) +{ + if (((seqnum + 1) % (SEQNUM_MAX+1)) == next_outgoing_seqnum) { + + next_outgoing_seqnum = seqnum; + return true; + } + return false; +} + +void Channel::UpdateBytesSent(unsigned int bytes, unsigned int packets) +{ + MutexAutoLock internal(m_internal_mutex); + current_bytes_transfered += bytes; + current_packet_successful += packets; +} + +void Channel::UpdateBytesReceived(unsigned int bytes) { + MutexAutoLock internal(m_internal_mutex); + current_bytes_received += bytes; +} + +void Channel::UpdateBytesLost(unsigned int bytes) +{ + MutexAutoLock internal(m_internal_mutex); + current_bytes_lost += bytes; +} + + +void Channel::UpdatePacketLossCounter(unsigned int count) +{ + MutexAutoLock internal(m_internal_mutex); + current_packet_loss += count; +} + +void Channel::UpdatePacketTooLateCounter() +{ + MutexAutoLock internal(m_internal_mutex); + current_packet_too_late++; +} + +void Channel::UpdateTimers(float dtime) +{ + bpm_counter += dtime; + packet_loss_counter += dtime; + + if (packet_loss_counter > 1.0f) { + packet_loss_counter -= 1.0f; + + unsigned int packet_loss; + unsigned int packets_successful; + unsigned int packet_too_late; + + bool reasonable_amount_of_data_transmitted = false; + + { + MutexAutoLock internal(m_internal_mutex); + packet_loss = current_packet_loss; + packet_too_late = current_packet_too_late; + packets_successful = current_packet_successful; + + // has half the window even been used? + if (current_bytes_transfered > (unsigned int) (m_window_size*512/2)) { + reasonable_amount_of_data_transmitted = true; + } + current_packet_loss = 0; + current_packet_too_late = 0; + current_packet_successful = 0; + } + + // Packets too late means either packet duplication along the way + // or we were too fast in resending it (which should be self-regulating). + // Count this a signal of congestion, like packet loss. + packet_loss = std::min(packet_loss + packet_too_late, packets_successful); + + /* dynamic window size */ + float successful_to_lost_ratio = 0.0f; + bool done = false; + + if (packets_successful > 0) { + successful_to_lost_ratio = packet_loss/packets_successful; + } else if (packet_loss > 0) { + setWindowSize(m_window_size - 10); + done = true; + } + + if (!done) { + if (successful_to_lost_ratio < 0.01f) { + /* don't even think about increasing if we didn't even + * use major parts of our window */ + if (reasonable_amount_of_data_transmitted) + setWindowSize(m_window_size + 100); + } else if (successful_to_lost_ratio < 0.05f) { + /* don't even think about increasing if we didn't even + * use major parts of our window */ + if (reasonable_amount_of_data_transmitted) + setWindowSize(m_window_size + 50); + } else if (successful_to_lost_ratio > 0.15f) { + setWindowSize(m_window_size - 100); + } else if (successful_to_lost_ratio > 0.1f) { + setWindowSize(m_window_size - 50); + } + } + } + + if (bpm_counter > 10.0f) { + { + MutexAutoLock internal(m_internal_mutex); + cur_kbps = + (((float) current_bytes_transfered)/bpm_counter)/1024.0f; + current_bytes_transfered = 0; + cur_kbps_lost = + (((float) current_bytes_lost)/bpm_counter)/1024.0f; + current_bytes_lost = 0; + cur_incoming_kbps = + (((float) current_bytes_received)/bpm_counter)/1024.0f; + current_bytes_received = 0; + bpm_counter = 0.0f; + } + + if (cur_kbps > max_kbps) { + max_kbps = cur_kbps; + } + + if (cur_kbps_lost > max_kbps_lost) { + max_kbps_lost = cur_kbps_lost; + } + + if (cur_incoming_kbps > max_incoming_kbps) { + max_incoming_kbps = cur_incoming_kbps; + } + + rate_samples = MYMIN(rate_samples+1,10); + float old_fraction = ((float) (rate_samples-1) )/( (float) rate_samples); + avg_kbps = avg_kbps * old_fraction + + cur_kbps * (1.0 - old_fraction); + avg_kbps_lost = avg_kbps_lost * old_fraction + + cur_kbps_lost * (1.0 - old_fraction); + avg_incoming_kbps = avg_incoming_kbps * old_fraction + + cur_incoming_kbps * (1.0 - old_fraction); + } +} + + +/* + Peer +*/ + +PeerHelper::~PeerHelper() +{ + if (m_peer) + m_peer->DecUseCount(); + + m_peer = nullptr; +} + +PeerHelper& PeerHelper::operator=(Peer* peer) +{ + if (m_peer) + m_peer->DecUseCount(); + m_peer = peer; + if (peer && !peer->IncUseCount()) + m_peer = nullptr; + return *this; +} + +bool Peer::IncUseCount() +{ + MutexAutoLock lock(m_exclusive_access_mutex); + + if (!m_pending_deletion) { + this->m_usage++; + return true; + } + + return false; +} + +void Peer::DecUseCount() +{ + { + MutexAutoLock lock(m_exclusive_access_mutex); + sanity_check(m_usage > 0); + m_usage--; + + if (!((m_pending_deletion) && (m_usage == 0))) + return; + } + delete this; +} + +void Peer::RTTStatistics(float rtt, const std::string &profiler_id, + unsigned int num_samples) { + + if (m_last_rtt > 0) { + /* set min max values */ + if (rtt < m_rtt.min_rtt) + m_rtt.min_rtt = rtt; + if (rtt >= m_rtt.max_rtt) + m_rtt.max_rtt = rtt; + + /* do average calculation */ + if (m_rtt.avg_rtt < 0.0) + m_rtt.avg_rtt = rtt; + else + m_rtt.avg_rtt = m_rtt.avg_rtt * (num_samples/(num_samples-1)) + + rtt * (1/num_samples); + + /* do jitter calculation */ + + //just use some neutral value at beginning + float jitter = m_rtt.jitter_min; + + if (rtt > m_last_rtt) + jitter = rtt-m_last_rtt; + + if (rtt <= m_last_rtt) + jitter = m_last_rtt - rtt; + + if (jitter < m_rtt.jitter_min) + m_rtt.jitter_min = jitter; + if (jitter >= m_rtt.jitter_max) + m_rtt.jitter_max = jitter; + + if (m_rtt.jitter_avg < 0.0) + m_rtt.jitter_avg = jitter; + else + m_rtt.jitter_avg = m_rtt.jitter_avg * (num_samples/(num_samples-1)) + + jitter * (1/num_samples); + + if (!profiler_id.empty()) { + g_profiler->graphAdd(profiler_id + " RTT [ms]", rtt * 1000.f); + g_profiler->graphAdd(profiler_id + " jitter [ms]", jitter * 1000.f); + } + } + /* save values required for next loop */ + m_last_rtt = rtt; +} + +bool Peer::isTimedOut(float timeout, std::string &reason) +{ + MutexAutoLock lock(m_exclusive_access_mutex); + + { + u64 current_time = porting::getTimeMs(); + float dtime = CALC_DTIME(m_last_timeout_check, current_time); + m_last_timeout_check = current_time; + m_timeout_counter += dtime; + } + if (m_timeout_counter > timeout) { + reason = "timeout counter"; + return true; + } + + return false; +} + +void Peer::Drop() +{ + { + MutexAutoLock usage_lock(m_exclusive_access_mutex); + m_pending_deletion = true; + if (m_usage != 0) + return; + } + + PROFILE(std::stringstream peerIdentifier1); + PROFILE(peerIdentifier1 << "runTimeouts[" << m_connection->getDesc() + << ";" << id << ";RELIABLE]"); + PROFILE(g_profiler->remove(peerIdentifier1.str())); + PROFILE(std::stringstream peerIdentifier2); + PROFILE(peerIdentifier2 << "sendPackets[" << m_connection->getDesc() + << ";" << id << ";RELIABLE]"); + PROFILE(ScopeProfiler peerprofiler(g_profiler, peerIdentifier2.str(), SPT_AVG)); + + delete this; +} + +UDPPeer::UDPPeer(session_t id, const Address &address, Connection *connection) : + Peer(id, address, connection) +{ + for (Channel &channel : channels) + channel.setWindowSize(START_RELIABLE_WINDOW_SIZE); +} + +bool UDPPeer::isTimedOut(float timeout, std::string &reason) +{ + if (Peer::isTimedOut(timeout, reason)) + return true; + + MutexAutoLock lock(m_exclusive_access_mutex); + + for (int i = 0; i < CHANNEL_COUNT; i++) { + Channel &channel = channels[i]; + if (channel.outgoing_reliables_sent.getTimedOuts(timeout) > 0) { + reason = "outgoing reliables channel=" + itos(i); + return true; + } + } + + return false; +} + +void UDPPeer::reportRTT(float rtt) +{ + if (rtt < 0) + return; + RTTStatistics(rtt,"rudp",MAX_RELIABLE_WINDOW_SIZE*10); + + // use this value to decide the resend timeout + const float rtt_stat = getStat(AVG_RTT); + if (rtt_stat < 0) + return; + float timeout = rtt_stat * RESEND_TIMEOUT_FACTOR; + if (timeout < RESEND_TIMEOUT_MIN) + timeout = RESEND_TIMEOUT_MIN; + if (timeout > RESEND_TIMEOUT_MAX) + timeout = RESEND_TIMEOUT_MAX; + + float timeout_old = getResendTimeout(); + setResendTimeout(timeout); + + if (std::abs(timeout - timeout_old) >= 0.001f) { + dout_con << m_connection->getDesc() << " set resend timeout " << timeout + << " (rtt=" << rtt_stat << ") for peer id: " << id << std::endl; + } +} + +bool UDPPeer::Ping(float dtime,SharedBuffer& data) +{ + m_ping_timer += dtime; + if (!isHalfOpen() && m_ping_timer >= PING_TIMEOUT) + { + // Create and send PING packet + writeU8(&data[0], PACKET_TYPE_CONTROL); + writeU8(&data[1], CONTROLTYPE_PING); + m_ping_timer = 0.0f; + return true; + } + return false; +} + +void UDPPeer::PutReliableSendCommand(ConnectionCommandPtr &c, + unsigned int max_packet_size) +{ + if (m_pending_disconnect) + return; + + Channel &chan = channels[c->channelnum]; + + if (chan.queued_commands.empty() && + /* don't queue more packets then window size */ + (chan.queued_reliables.size() + 1 < chan.getWindowSize() / 2)) { + LOG(dout_con<getDesc() + <<" processing reliable command for peer id: " << c->peer_id + <<" data size: " << c->data.getSize() << std::endl); + if (processReliableSendCommand(c, max_packet_size)) + return; + } else { + LOG(dout_con<getDesc() + <<" Queueing reliable command for peer id: " << c->peer_id + <<" data size: " << c->data.getSize() <= chan.getWindowSize() / 2) { + LOG(derr_con << m_connection->getDesc() + << "Possible packet stall to peer id: " << c->peer_id + << " queued_commands=" << chan.queued_commands.size() + << std::endl); + } + } + chan.queued_commands.push_back(c); +} + +bool UDPPeer::processReliableSendCommand( + ConnectionCommandPtr &c_ptr, + unsigned int max_packet_size) +{ + if (m_pending_disconnect) + return true; + + const auto &c = *c_ptr; + Channel &chan = channels[c.channelnum]; + + const u32 chunksize_max = max_packet_size + - BASE_HEADER_SIZE + - RELIABLE_HEADER_SIZE; + + std::list> originals; + + if (c.raw) { + originals.emplace_back(c.data); + } else { + u16 split_seqnum = chan.readNextSplitSeqNum(); + makeAutoSplitPacket(c.data, chunksize_max, split_seqnum, &originals); + chan.setNextSplitSeqNum(split_seqnum); + } + + sanity_check(originals.size() < MAX_RELIABLE_WINDOW_SIZE); + + bool have_sequence_number = false; + bool have_initial_sequence_number = false; + std::queue toadd; + u16 initial_sequence_number = 0; + + for (SharedBuffer &original : originals) { + u16 seqnum = chan.getOutgoingSequenceNumber(have_sequence_number); + + /* oops, we don't have enough sequence numbers to send this packet */ + if (!have_sequence_number) + break; + + if (!have_initial_sequence_number) + { + initial_sequence_number = seqnum; + have_initial_sequence_number = true; + } + + SharedBuffer reliable = makeReliablePacket(original, seqnum); + + // Add base headers and make a packet + BufferedPacketPtr p = con::makePacket(address, reliable, + m_connection->GetProtocolID(), m_connection->GetPeerID(), + c.channelnum); + + toadd.push(p); + } + + if (have_sequence_number) { + while (!toadd.empty()) { + BufferedPacketPtr p = toadd.front(); + toadd.pop(); +// LOG(dout_con<getDesc() +// << " queuing reliable packet for peer_id: " << c.peer_id +// << " channel: " << (c.channelnum&0xFF) +// << " seqnum: " << readU16(&p.data[BASE_HEADER_SIZE+1]) +// << std::endl) + chan.queued_reliables.push(p); + } + sanity_check(chan.queued_reliables.size() < 0xFFFF); + return true; + } + + u16 packets_available = toadd.size(); + /* we didn't get a single sequence number no need to fill queue */ + if (!have_initial_sequence_number) { + dout_con << m_connection->getDesc() << " No sequence numbers available!" << std::endl; + return false; + } + + while (!toadd.empty()) { + /* remove packet */ + toadd.pop(); + + bool successfully_put_back_sequence_number + = chan.putBackSequenceNumber( + (initial_sequence_number+toadd.size() % (SEQNUM_MAX+1))); + + FATAL_ERROR_IF(!successfully_put_back_sequence_number, "error"); + } + + u32 n_queued = chan.outgoing_reliables_sent.size(); + + LOG(dout_con<getDesc() + << " Windowsize exceeded on reliable sending " + << c.data.getSize() << " bytes" + << std::endl << "\t\tinitial_sequence_number: " + << initial_sequence_number + << std::endl << "\t\tgot at most : " + << packets_available << " packets" + << std::endl << "\t\tpackets queued : " + << n_queued + << std::endl); + + return false; +} + +void UDPPeer::RunCommandQueues( + unsigned int max_packet_size, + unsigned int maxtransfer) +{ + + for (Channel &channel : channels) { + + if ((!channel.queued_commands.empty()) && + (channel.queued_reliables.size() < maxtransfer)) { + try { + ConnectionCommandPtr c = channel.queued_commands.front(); + + LOG(dout_con << m_connection->getDesc() + << " processing queued reliable command " << std::endl); + + // Packet is processed, remove it from queue + if (processReliableSendCommand(c, max_packet_size)) { + channel.queued_commands.pop_front(); + } else { + LOG(dout_con << m_connection->getDesc() + << " Failed to queue packets for peer_id: " << c->peer_id + << ", delaying sending of " << c->data.getSize() + << " bytes" << std::endl); + } + } + catch (ItemNotFoundException &e) { + // intentionally empty + } + } + } +} + +u16 UDPPeer::getNextSplitSequenceNumber(u8 channel) +{ + assert(channel < CHANNEL_COUNT); // Pre-condition + return channels[channel].readNextSplitSeqNum(); +} + +void UDPPeer::setNextSplitSequenceNumber(u8 channel, u16 seqnum) +{ + assert(channel < CHANNEL_COUNT); // Pre-condition + channels[channel].setNextSplitSeqNum(seqnum); +} + +SharedBuffer UDPPeer::addSplitPacket(u8 channel, BufferedPacketPtr &toadd, + bool reliable) +{ + assert(channel < CHANNEL_COUNT); // Pre-condition + return channels[channel].incoming_splits.insert(toadd, reliable); +} + +/* + ConnectionEvent +*/ + +const char *ConnectionEvent::describe() const +{ + switch(type) { + case CONNEVENT_NONE: + return "CONNEVENT_NONE"; + case CONNEVENT_DATA_RECEIVED: + return "CONNEVENT_DATA_RECEIVED"; + case CONNEVENT_PEER_ADDED: + return "CONNEVENT_PEER_ADDED"; + case CONNEVENT_PEER_REMOVED: + return "CONNEVENT_PEER_REMOVED"; + case CONNEVENT_BIND_FAILED: + return "CONNEVENT_BIND_FAILED"; + } + return "Invalid ConnectionEvent"; +} + + +ConnectionEventPtr ConnectionEvent::create(ConnectionEventType type) +{ + return std::shared_ptr(new ConnectionEvent(type)); +} + +ConnectionEventPtr ConnectionEvent::dataReceived(session_t peer_id, const Buffer &data) +{ + auto e = create(CONNEVENT_DATA_RECEIVED); + e->peer_id = peer_id; + data.copyTo(e->data); + return e; +} + +ConnectionEventPtr ConnectionEvent::peerAdded(session_t peer_id, Address address) +{ + auto e = create(CONNEVENT_PEER_ADDED); + e->peer_id = peer_id; + e->address = address; + return e; +} + +ConnectionEventPtr ConnectionEvent::peerRemoved(session_t peer_id, bool is_timeout, Address address) +{ + auto e = create(CONNEVENT_PEER_REMOVED); + e->peer_id = peer_id; + e->timeout = is_timeout; + e->address = address; + return e; +} + +ConnectionEventPtr ConnectionEvent::bindFailed() +{ + return create(CONNEVENT_BIND_FAILED); +} + +/* + Connection +*/ + +Connection::Connection(u32 max_packet_size, float timeout, + bool ipv6, PeerHandler *peerhandler) : + m_udpSocket(ipv6), + m_protocol_id(PROTOCOL_ID), + m_sendThread(new ConnectionSendThread(max_packet_size, timeout)), + m_receiveThread(new ConnectionReceiveThread()), + m_bc_peerhandler(peerhandler) + +{ + /* Amount of time Receive() will wait for data, this is entirely different + * from the connection timeout */ + m_udpSocket.setTimeoutMs(500); + + m_sendThread->setParent(this); + m_receiveThread->setParent(this); + + m_sendThread->start(); + m_receiveThread->start(); +} + + +Connection::~Connection() +{ + m_shutting_down = true; + // request threads to stop + m_sendThread->stop(); + m_receiveThread->stop(); + + //TODO for some unkonwn reason send/receive threads do not exit as they're + // supposed to be but wait on peer timeout. To speed up shutdown we reduce + // timeout to half a second. + m_sendThread->setPeerTimeout(0.5); + + // wait for threads to finish + m_sendThread->wait(); + m_receiveThread->wait(); + + // Delete peers + for (auto &peer : m_peers) { + delete peer.second; + } +} + +/* Internal stuff */ + +void Connection::putEvent(ConnectionEventPtr e) +{ + assert(e->type != CONNEVENT_NONE); // Pre-condition + m_event_queue.push_back(e); +} + +void Connection::TriggerSend() +{ + m_sendThread->Trigger(); +} + +PeerHelper Connection::getPeerNoEx(session_t peer_id) +{ + MutexAutoLock peerlock(m_peers_mutex); + std::map::iterator node = m_peers.find(peer_id); + + if (node == m_peers.end()) { + return PeerHelper(NULL); + } + + // Error checking + FATAL_ERROR_IF(node->second->id != peer_id, "Invalid peer id"); + + return PeerHelper(node->second); +} + +/* find peer_id for address */ +session_t Connection::lookupPeer(const Address& sender) +{ + MutexAutoLock peerlock(m_peers_mutex); + for (auto &it: m_peers) { + Peer *peer = it.second; + if (peer->isPendingDeletion()) + continue; + + if (peer->getAddress() == sender) + return peer->id; + } + + return PEER_ID_INEXISTENT; +} + +u32 Connection::getActiveCount() +{ + MutexAutoLock peerlock(m_peers_mutex); + u32 count = 0; + for (auto &it : m_peers) { + Peer *peer = it.second; + if (peer->isPendingDeletion()) + continue; + if (peer->isHalfOpen()) + continue; + count++; + } + return count; +} + +bool Connection::deletePeer(session_t peer_id, bool timeout) +{ + Peer *peer = 0; + + /* lock list as short as possible */ + { + MutexAutoLock peerlock(m_peers_mutex); + if (m_peers.find(peer_id) == m_peers.end()) + return false; + peer = m_peers[peer_id]; + m_peers.erase(peer_id); + auto it = std::find(m_peer_ids.begin(), m_peer_ids.end(), peer_id); + m_peer_ids.erase(it); + } + + // Create event + putEvent(ConnectionEvent::peerRemoved(peer_id, timeout, peer->getAddress())); + + peer->Drop(); + return true; +} + +/* Interface */ + +ConnectionEventPtr Connection::waitEvent(u32 timeout_ms) +{ + try { + return m_event_queue.pop_front(timeout_ms); + } catch(ItemNotFoundException &ex) { + return ConnectionEvent::create(CONNEVENT_NONE); + } +} + +void Connection::putCommand(ConnectionCommandPtr c) +{ + if (!m_shutting_down) { + m_command_queue.push_back(c); + m_sendThread->Trigger(); + } +} + +void Connection::Serve(Address bind_addr) +{ + putCommand(ConnectionCommand::serve(bind_addr)); +} + +void Connection::Connect(Address address) +{ + putCommand(ConnectionCommand::connect(address)); +} + +bool Connection::Connected() +{ + MutexAutoLock peerlock(m_peers_mutex); + + if (m_peers.size() != 1) + return false; + + std::map::iterator node = m_peers.find(PEER_ID_SERVER); + if (node == m_peers.end()) + return false; + + if (m_peer_id == PEER_ID_INEXISTENT) + return false; + + return true; +} + +void Connection::Disconnect() +{ + putCommand(ConnectionCommand::disconnect()); +} + +bool Connection::ReceiveTimeoutMs(NetworkPacket *pkt, u32 timeout_ms) +{ + /* + Note that this function can potentially wait infinitely if non-data + events keep happening before the timeout expires. + This is not considered to be a problem (is it?) + */ + for(;;) { + ConnectionEventPtr e_ptr = waitEvent(timeout_ms); + const ConnectionEvent &e = *e_ptr; + + if (e.type != CONNEVENT_NONE) { + LOG(dout_con << getDesc() << ": Receive: got event: " + << e.describe() << std::endl); + } + + switch (e.type) { + case CONNEVENT_NONE: + return false; + case CONNEVENT_DATA_RECEIVED: + // Data size is lesser than command size, ignoring packet + if (e.data.getSize() < 2) { + continue; + } + + pkt->putRawPacket(*e.data, e.data.getSize(), e.peer_id); + return true; + case CONNEVENT_PEER_ADDED: { + UDPPeer tmp(e.peer_id, e.address, this); + if (m_bc_peerhandler) + m_bc_peerhandler->peerAdded(&tmp); + continue; + } + case CONNEVENT_PEER_REMOVED: { + UDPPeer tmp(e.peer_id, e.address, this); + if (m_bc_peerhandler) + m_bc_peerhandler->deletingPeer(&tmp, e.timeout); + continue; + } + case CONNEVENT_BIND_FAILED: + throw ConnectionBindFailed("Failed to bind socket " + "(port already in use?)"); + } + } + return false; +} + +void Connection::Send(session_t peer_id, u8 channelnum, + NetworkPacket *pkt, bool reliable) +{ + assert(channelnum < CHANNEL_COUNT); // Pre-condition + + // approximate check similar to UDPPeer::processReliableSendCommand() + // to get nicer errors / backtraces if this happens. + if (reliable && pkt->getSize() > MAX_RELIABLE_WINDOW_SIZE*512) { + std::ostringstream oss; + oss << "Packet too big for window, peer_id=" << peer_id + << " command=" << pkt->getCommand() << " size=" << pkt->getSize(); + FATAL_ERROR(oss.str().c_str()); + } + + putCommand(ConnectionCommand::send(peer_id, channelnum, pkt, reliable)); +} + +Address Connection::GetPeerAddress(session_t peer_id) +{ + PeerHelper peer = getPeerNoEx(peer_id); + + if (!peer) + throw PeerNotFoundException("No address for peer found!"); + return peer->getAddress(); +} + +float Connection::getPeerStat(session_t peer_id, rtt_stat_type type) +{ + PeerHelper peer = getPeerNoEx(peer_id); + if (!peer) + return -1; + return peer->getStat(type); +} + +float Connection::getLocalStat(rate_stat_type type) +{ + PeerHelper peer = getPeerNoEx(PEER_ID_SERVER); + + FATAL_ERROR_IF(!peer, "Connection::getLocalStat we couldn't get our own peer? are you serious???"); + + float retval = 0; + + for (Channel &channel : dynamic_cast(&peer)->channels) { + switch(type) { + case CUR_DL_RATE: + retval += channel.getCurrentDownloadRateKB(); + break; + case AVG_DL_RATE: + retval += channel.getAvgDownloadRateKB(); + break; + case CUR_INC_RATE: + retval += channel.getCurrentIncomingRateKB(); + break; + case AVG_INC_RATE: + retval += channel.getAvgIncomingRateKB(); + break; + case AVG_LOSS_RATE: + retval += channel.getAvgLossRateKB(); + break; + case CUR_LOSS_RATE: + retval += channel.getCurrentLossRateKB(); + break; + default: + FATAL_ERROR("Connection::getLocalStat Invalid stat type"); + } + } + return retval; +} + +session_t Connection::createPeer(const Address &sender, int fd) +{ + // Somebody wants to make a new connection + + // Get a unique peer id + const session_t minimum = 2; + const session_t overflow = MAX_UDP_PEERS; + + /* + Find an unused peer id + */ + + MutexAutoLock lock(m_peers_mutex); + session_t peer_id_new; + for (int tries = 0; tries < 100; tries++) { + peer_id_new = myrand_range(minimum, overflow - 1); + if (m_peers.find(peer_id_new) == m_peers.end()) + break; + } + if (m_peers.find(peer_id_new) != m_peers.end()) { + errorstream << getDesc() << " ran out of peer ids" << std::endl; + return PEER_ID_INEXISTENT; + } + + // Create a peer + Peer *peer = 0; + peer = new UDPPeer(peer_id_new, sender, this); + + m_peers[peer->id] = peer; + m_peer_ids.push_back(peer->id); + + LOG(dout_con << getDesc() + << "createPeer(): giving peer_id=" << peer_id_new << std::endl); + + { + Buffer reply(4); + writeU8(&reply[0], PACKET_TYPE_CONTROL); + writeU8(&reply[1], CONTROLTYPE_SET_PEER_ID); + writeU16(&reply[2], peer_id_new); + putCommand(ConnectionCommand::createPeer(peer_id_new, reply)); + } + + // Create peer addition event + putEvent(ConnectionEvent::peerAdded(peer_id_new, sender)); + + // We're now talking to a valid peer_id + return peer_id_new; +} + +const std::string Connection::getDesc() +{ + MutexAutoLock _(m_info_mutex); + return std::string("con(")+ + itos(m_udpSocket.GetHandle())+"/"+itos(m_peer_id)+")"; +} + +void Connection::DisconnectPeer(session_t peer_id) +{ + putCommand(ConnectionCommand::disconnect_peer(peer_id)); +} + +void Connection::doResendOne(session_t peer_id) +{ + assert(peer_id != PEER_ID_INEXISTENT); + putCommand(ConnectionCommand::resend_one(peer_id)); +} + +void Connection::sendAck(session_t peer_id, u8 channelnum, u16 seqnum) +{ + assert(channelnum < CHANNEL_COUNT); // Pre-condition + + LOG(dout_con< ack(4); + writeU8(&ack[0], PACKET_TYPE_CONTROL); + writeU8(&ack[1], CONTROLTYPE_ACK); + writeU16(&ack[2], seqnum); + + putCommand(ConnectionCommand::ack(peer_id, channelnum, ack)); + m_sendThread->Trigger(); +} + +UDPPeer* Connection::createServerPeer(const Address &address) +{ + if (ConnectedToServer()) + throw ConnectionException("Already connected to a server"); + + UDPPeer *peer = new UDPPeer(PEER_ID_SERVER, address, this); + peer->SetFullyOpen(); + + { + MutexAutoLock lock(m_peers_mutex); + m_peers[peer->id] = peer; + m_peer_ids.push_back(peer->id); + } + + return peer; +} + +} // namespace diff --git a/src/network/mtp/impl.h b/src/network/mtp/impl.h new file mode 100644 index 000000000..7105bac6d --- /dev/null +++ b/src/network/mtp/impl.h @@ -0,0 +1,322 @@ +/* +Minetest +Copyright (C) 2013 celeron55, Perttu Ahola + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include "network/connection.h" +#include "network/socket.h" +#include "constants.h" +#include "util/pointer.h" +#include "util/container.h" +#include "util/numeric.h" +#include "porting.h" +#include "network/networkprotocol.h" +#include +#include +#include + +namespace con +{ + +class ConnectionReceiveThread; +class ConnectionSendThread; + +class Peer; + +// FIXME: Peer refcounting should generally be replaced by std::shared_ptr +class PeerHelper +{ +public: + PeerHelper() = default; + inline PeerHelper(Peer *peer) { *this = peer; } + ~PeerHelper(); + + PeerHelper& operator=(Peer *peer); + inline Peer* operator->() const { return m_peer; } + inline Peer* operator&() const { return m_peer; } + + inline bool operator!() { return !m_peer; } + inline bool operator!=(std::nullptr_t) { return !!m_peer; } + +private: + Peer *m_peer = nullptr; +}; + +/* + Connection +*/ + +enum ConnectionEventType { + CONNEVENT_NONE, + CONNEVENT_DATA_RECEIVED, + CONNEVENT_PEER_ADDED, + CONNEVENT_PEER_REMOVED, + CONNEVENT_BIND_FAILED, +}; + +struct ConnectionEvent; +typedef std::shared_ptr ConnectionEventPtr; + +// This is very similar to ConnectionCommand +struct ConnectionEvent +{ + const ConnectionEventType type; + session_t peer_id = 0; + Buffer data; + bool timeout = false; + Address address; + + // We don't want to copy "data" + DISABLE_CLASS_COPY(ConnectionEvent); + + static ConnectionEventPtr create(ConnectionEventType type); + static ConnectionEventPtr dataReceived(session_t peer_id, const Buffer &data); + static ConnectionEventPtr peerAdded(session_t peer_id, Address address); + static ConnectionEventPtr peerRemoved(session_t peer_id, bool is_timeout, Address address); + static ConnectionEventPtr bindFailed(); + + const char *describe() const; + +private: + ConnectionEvent(ConnectionEventType type_) : + type(type_) {} +}; + +struct ConnectionCommand; +typedef std::shared_ptr ConnectionCommandPtr; + +struct BufferedPacket; +typedef std::shared_ptr BufferedPacketPtr; + +class Connection; +class PeerHandler; + +class Peer : public IPeer { + public: + friend class PeerHelper; + + virtual ~Peer() { + MutexAutoLock usage_lock(m_exclusive_access_mutex); + FATAL_ERROR_IF(m_usage != 0, "Reference counting failure"); + } + + void Drop(); + + virtual void PutReliableSendCommand(ConnectionCommandPtr &c, + unsigned int max_packet_size) {}; + + bool isPendingDeletion() const { + MutexAutoLock lock(m_exclusive_access_mutex); + return m_pending_deletion; + } + void ResetTimeout() { + MutexAutoLock lock(m_exclusive_access_mutex); + m_timeout_counter = 0; + } + + bool isHalfOpen() const { + MutexAutoLock lock(m_exclusive_access_mutex); + return m_half_open; + } + void SetFullyOpen() { + MutexAutoLock lock(m_exclusive_access_mutex); + m_half_open = false; + } + + virtual bool isTimedOut(float timeout, std::string &reason); + + unsigned int m_increment_packets_remaining = 0; + + virtual u16 getNextSplitSequenceNumber(u8 channel) { return 0; }; + virtual void setNextSplitSequenceNumber(u8 channel, u16 seqnum) {}; + virtual SharedBuffer addSplitPacket(u8 channel, BufferedPacketPtr &toadd, + bool reliable) + { + FATAL_ERROR("unimplemented in abstract class"); + } + + virtual bool Ping(float dtime, SharedBuffer& data) { return false; }; + + virtual float getStat(rtt_stat_type type) const { + switch (type) { + case MIN_RTT: + return m_rtt.min_rtt; + case MAX_RTT: + return m_rtt.max_rtt; + case AVG_RTT: + return m_rtt.avg_rtt; + case MIN_JITTER: + return m_rtt.jitter_min; + case MAX_JITTER: + return m_rtt.jitter_max; + case AVG_JITTER: + return m_rtt.jitter_avg; + } + return -1; + } + + protected: + Peer(session_t id, const Address &address, Connection *connection) : + IPeer(id), + m_connection(connection), + address(address), + m_last_timeout_check(porting::getTimeMs()) + { + } + + virtual void reportRTT(float rtt) {}; + + void RTTStatistics(float rtt, + const std::string &profiler_id = "", + unsigned int num_samples = 1000); + + bool IncUseCount(); + void DecUseCount(); + + mutable std::mutex m_exclusive_access_mutex; + + bool m_pending_deletion = false; + + Connection *m_connection; + + // Address of the peer + Address address; + + // Ping timer + float m_ping_timer = 0.0f; + + private: + struct rttstats { + float jitter_min = FLT_MAX; + float jitter_max = 0.0f; + float jitter_avg = -1.0f; + float min_rtt = FLT_MAX; + float max_rtt = 0.0f; + float avg_rtt = -1.0f; + }; + + rttstats m_rtt; + float m_last_rtt = -1.0f; + + /* + Until the peer has communicated with us using their assigned peer id + the connection is considered half-open. + During this time we inhibit re-sending any reliables or pings. This + is to avoid spending too many resources on a potential DoS attack + and to make sure Minetest servers are not useful for UDP amplificiation. + */ + bool m_half_open = true; + + // current usage count + unsigned int m_usage = 0; + + // Seconds from last receive + float m_timeout_counter = 0.0f; + + u64 m_last_timeout_check; +}; + +class UDPPeer; + +class Connection final : public IConnection +{ +public: + friend class ConnectionSendThread; + friend class ConnectionReceiveThread; + + Connection(u32 max_packet_size, float timeout, bool ipv6, + PeerHandler *peerhandler); + ~Connection(); + + /* Interface */ + ConnectionEventPtr waitEvent(u32 timeout_ms); + + void putCommand(ConnectionCommandPtr c); + + void Serve(Address bind_addr); + void Connect(Address address); + bool Connected(); + void Disconnect(); + bool ReceiveTimeoutMs(NetworkPacket *pkt, u32 timeout_ms); + void Send(session_t peer_id, u8 channelnum, NetworkPacket *pkt, bool reliable); + session_t GetPeerID() const { return m_peer_id; } + Address GetPeerAddress(session_t peer_id); + float getPeerStat(session_t peer_id, rtt_stat_type type); + float getLocalStat(rate_stat_type type); + u32 GetProtocolID() const { return m_protocol_id; }; + const std::string getDesc(); + void DisconnectPeer(session_t peer_id); + +protected: + PeerHelper getPeerNoEx(session_t peer_id); + session_t lookupPeer(const Address& sender); + + session_t createPeer(const Address& sender, int fd); + UDPPeer* createServerPeer(const Address& sender); + bool deletePeer(session_t peer_id, bool timeout); + + void SetPeerID(session_t id) { m_peer_id = id; } + + void doResendOne(session_t peer_id); + + void sendAck(session_t peer_id, u8 channelnum, u16 seqnum); + + std::vector getPeerIDs() + { + MutexAutoLock peerlock(m_peers_mutex); + return m_peer_ids; + } + + u32 getActiveCount(); + + UDPSocket m_udpSocket; + // Command queue: user -> SendThread + MutexedQueue m_command_queue; + + void putEvent(ConnectionEventPtr e); + + void TriggerSend(); + + bool ConnectedToServer() + { + return getPeerNoEx(PEER_ID_SERVER) != nullptr; + } +private: + // Event queue: ReceiveThread -> user + MutexedQueue m_event_queue; + + session_t m_peer_id = 0; + u32 m_protocol_id; + + std::map m_peers; + std::vector m_peer_ids; + std::mutex m_peers_mutex; + + std::unique_ptr m_sendThread; + std::unique_ptr m_receiveThread; + + mutable std::mutex m_info_mutex; + + // Backwards compatibility + PeerHandler *m_bc_peerhandler; + + bool m_shutting_down = false; +}; + +} // namespace diff --git a/src/network/connection_internal.h b/src/network/mtp/internal.h similarity index 91% rename from src/network/connection_internal.h rename to src/network/mtp/internal.h index a528f3fef..4cf6cb57a 100644 --- a/src/network/connection_internal.h +++ b/src/network/mtp/internal.h @@ -19,11 +19,10 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once -/********************************************/ -/* may only be included from in src/network */ -/********************************************/ +#include "network/mtp/impl.h" -#include "connection.h" +// Constant that differentiates the protocol from random data and other protocols +#define PROTOCOL_ID 0x4f457403 #define MAX_UDP_PEERS 65535 @@ -161,6 +160,32 @@ inline float CALC_DTIME(u64 lasttime, u64 curtime) return MYMAX(MYMIN(value, 0.1f), 0.0f); } +/* Exceptions */ + +class NotFoundException : public BaseException +{ +public: + NotFoundException(const char *s) : BaseException(s) {} +}; + +class ProcessedSilentlyException : public BaseException +{ +public: + ProcessedSilentlyException(const char *s) : BaseException(s) {} +}; + +class ProcessedQueued : public BaseException +{ +public: + ProcessedQueued(const char *s) : BaseException(s) {} +}; + +class IncomingDataCorruption : public BaseException +{ +public: + IncomingDataCorruption(const char *s) : BaseException(s) {} +}; + /* Struct for all kinds of packets. Includes following data: @@ -329,15 +354,24 @@ private: static ConnectionCommandPtr create(ConnectionCommandType type); }; -/* maximum window size to use, 0xFFFF is theoretical maximum. don't think about +/* + * Window sizes to use, in packets (not bytes!). + * 0xFFFF is theoretical maximum. don't think about * touching it, the less you're away from it the more likely data corruption * will occur + * + * Note: window sizes directly translate to maximum possible throughput, e.g. + * (2048 * 512 bytes) / 33ms = 15 MiB/s */ + +// Due to backwards compatibility we have different window sizes for what we'll +// accept from peers vs. what we use for sending. #define MAX_RELIABLE_WINDOW_SIZE 0x8000 +#define MAX_RELIABLE_WINDOW_SIZE_SEND 2048 /* starting value for window size */ -#define START_RELIABLE_WINDOW_SIZE 0x400 +#define START_RELIABLE_WINDOW_SIZE 64 /* minimum value for window size */ -#define MIN_RELIABLE_WINDOW_SIZE 0x40 +#define MIN_RELIABLE_WINDOW_SIZE 32 class Channel { @@ -405,7 +439,7 @@ public: void setWindowSize(long size) { - m_window_size = (u16)rangelim(size, MIN_RELIABLE_WINDOW_SIZE, MAX_RELIABLE_WINDOW_SIZE); + m_window_size = (u16)rangelim(size, MIN_RELIABLE_WINDOW_SIZE, MAX_RELIABLE_WINDOW_SIZE_SEND); } private: diff --git a/src/network/connectionthreads.cpp b/src/network/mtp/threads.cpp similarity index 96% rename from src/network/connectionthreads.cpp rename to src/network/mtp/threads.cpp index d5c9a39ed..50b7fa1e3 100644 --- a/src/network/connectionthreads.cpp +++ b/src/network/mtp/threads.cpp @@ -18,7 +18,7 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "connectionthreads.h" +#include "network/mtp/threads.h" #include "log.h" #include "profiler.h" #include "settings.h" @@ -35,7 +35,6 @@ namespace con #define PROFILE(a) #undef DEBUG_CONNECTION_KBPS #else -/* this mutex is used to achieve log message consistency */ #define PROFILE(a) a //#define DEBUG_CONNECTION_KBPS #undef DEBUG_CONNECTION_KBPS @@ -59,14 +58,24 @@ static inline u8 readChannel(const u8 *packetdata) /* Connection Threads */ /******************************************************************************/ +#define MPPI_SETTING "max_packets_per_iteration" + ConnectionSendThread::ConnectionSendThread(unsigned int max_packet_size, float timeout) : Thread("ConnectionSend"), m_max_packet_size(max_packet_size), m_timeout(timeout), - m_max_data_packets_per_iteration(g_settings->getU16("max_packets_per_iteration")) + m_max_data_packets_per_iteration(g_settings->getU16(MPPI_SETTING)) { - SANITY_CHECK(m_max_data_packets_per_iteration > 1); + auto &mppi = m_max_data_packets_per_iteration; + mppi = MYMAX(mppi, 1); + + const auto mppi_default = Settings::getLayer(SL_DEFAULTS)->getU16(MPPI_SETTING); + if (mppi < mppi_default) { + warningstream << "You are running the network code with a non-default " + "configuration (" MPPI_SETTING "=" << mppi << "). " + "This is not recommended in production." << std::endl; + } } void *ConnectionSendThread::run() @@ -211,7 +220,8 @@ void ConnectionSendThread::runTimeouts(float dtime, u32 peer_packet_quota) } float resend_timeout = udpPeer->getResendTimeout(); - for (Channel &channel : udpPeer->channels) { + for (int ch = 0; ch < CHANNEL_COUNT; ch++) { + auto &channel = udpPeer->channels[ch]; // Remove timed out incomplete unreliable split packets channel.incoming_splits.removeUnreliableTimedOuts(dtime, peer_timeout); @@ -232,8 +242,8 @@ void ConnectionSendThread::runTimeouts(float dtime, u32 peer_packet_quota) if (!timed_outs.empty()) { dout_con << m_connection->getDesc() << "Skipping re-send of " << timed_outs.size() << - " timed-out reliables to peer_id " << udpPeer->id - << " (half-open)." << std::endl; + " timed-out reliables to peer_id=" << udpPeer->id + << " channel=" << ch << " (half-open)." << std::endl; } continue; } @@ -246,7 +256,14 @@ void ConnectionSendThread::runTimeouts(float dtime, u32 peer_packet_quota) for (const auto &k : timed_outs) resendReliable(channel, k.get(), resend_timeout); + auto ws_old = channel.getWindowSize(); channel.UpdateTimers(dtime); + auto ws_new = channel.getWindowSize(); + if (ws_old != ws_new) { + dout_con << m_connection->getDesc() << + "Window size adjusted to " << ws_new << " for peer_id=" + << udpPeer->id << " channel=" << ch << std::endl; + } } /* send ping if necessary */ @@ -299,12 +316,12 @@ void ConnectionSendThread::rawSend(const BufferedPacket *p) assert(p); try { m_connection->m_udpSocket.Send(p->address, p->data, p->size()); - LOG(dout_con << m_connection->getDesc() - << " rawSend: " << p->size() - << " bytes sent" << std::endl); + //LOG(dout_con << m_connection->getDesc() + // << " rawSend: " << p->size() + // << " bytes sent" << std::endl); } catch (SendFailedException &e) { LOG(derr_con << m_connection->getDesc() - << "Connection::rawSend(): SendFailedException: " + << "SendFailedException: " << e.what() << " to " << p->address.serializeString() << std::endl); } } @@ -317,6 +334,7 @@ void ConnectionSendThread::sendAsPacketReliable(BufferedPacketPtr &p, Channel *c channel->outgoing_reliables_sent.insert(p, (channel->readOutgoingSequenceNumber() - MAX_RELIABLE_WINDOW_SIZE) % (MAX_RELIABLE_WINDOW_SIZE + 1)); + // wtf is this calculation?? ^ } catch (AlreadyExistsException &e) { LOG(derr_con << m_connection->getDesc() @@ -675,9 +693,9 @@ void ConnectionSendThread::sendPackets(float dtime, u32 peer_packet_quota) PROFILE(ScopeProfiler peerprofiler(g_profiler, peerIdentifier.str(), SPT_AVG)); - LOG(dout_con << m_connection->getDesc() - << " Handle per peer queues: peer_id=" << peerId - << " packet quota: " << peer->m_increment_packets_remaining << std::endl); + //LOG(dout_con << m_connection->getDesc() + // << " Handle per peer queues: peer_id=" << peerId + // << " packet quota: " << peer->m_increment_packets_remaining << std::endl); // first send queued reliable packets for all peers (if possible) for (unsigned int i = 0; i < CHANNEL_COUNT; i++) { @@ -769,7 +787,7 @@ void ConnectionSendThread::sendPackets(float dtime, u32 peer_packet_quota) } } - if (peer_packet_quota > 0) { + if (peer_packet_quota > 0 && !stopRequested()) { for (session_t peerId : peerIds) { PeerHelper peer = m_connection->getPeerNoEx(peerId); if (!peer) @@ -1180,7 +1198,7 @@ SharedBuffer ConnectionReceiveThread::handlePacketType_Control(Channel *chan // an overflow is quite unlikely but as it'd result in major // rtt miscalculation we handle it here if (current_time > p->absolute_send_time) { - float rtt = (current_time - p->absolute_send_time) / 1000.0; + float rtt = (current_time - p->absolute_send_time) / 1000.0f; // Let peer calculate stuff according to it // (avg_rtt and resend_timeout) @@ -1325,12 +1343,6 @@ SharedBuffer ConnectionReceiveThread::handlePacketType_Reliable(Channel *cha << ", seqnum: " << seqnum << std::endl;) m_connection->sendAck(peer->id, channelnum, seqnum); - // we already have this packet so this one was on wire at least - // the current timeout - // we don't know how long this packet was on wire don't do silly guessing - // dynamic_cast(&peer)-> - // reportRTT(dynamic_cast(&peer)->getResendTimeout()); - throw ProcessedSilentlyException("Retransmitting ack for old packet"); } } diff --git a/src/network/connectionthreads.h b/src/network/mtp/threads.h similarity index 99% rename from src/network/connectionthreads.h rename to src/network/mtp/threads.h index fff71f657..b41307fa6 100644 --- a/src/network/connectionthreads.h +++ b/src/network/mtp/threads.h @@ -26,7 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include "threading/thread.h" -#include "connection_internal.h" +#include "network/mtp/internal.h" namespace con { diff --git a/src/network/networkexceptions.h b/src/network/networkexceptions.h index 58a3bb490..1810106e5 100644 --- a/src/network/networkexceptions.h +++ b/src/network/networkexceptions.h @@ -26,11 +26,6 @@ namespace con /* Exceptions */ -class NotFoundException : public BaseException -{ -public: - NotFoundException(const char *s) : BaseException(s) {} -}; class PeerNotFoundException : public BaseException { @@ -56,29 +51,6 @@ public: InvalidIncomingDataException(const char *s) : BaseException(s) {} }; -class NoIncomingDataException : public BaseException -{ -public: - NoIncomingDataException(const char *s) : BaseException(s) {} -}; - -class ProcessedSilentlyException : public BaseException -{ -public: - ProcessedSilentlyException(const char *s) : BaseException(s) {} -}; - -class ProcessedQueued : public BaseException -{ -public: - ProcessedQueued(const char *s) : BaseException(s) {} -}; - -class IncomingDataCorruption : public BaseException -{ -public: - IncomingDataCorruption(const char *s) : BaseException(s) {} -}; } class SocketException : public BaseException diff --git a/src/network/networkpacket.h b/src/network/networkpacket.h index ee85b2951..0260f8072 100644 --- a/src/network/networkpacket.h +++ b/src/network/networkpacket.h @@ -19,7 +19,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once -#include "util/pointer.h" +#include "util/pointer.h" // Buffer +#include "irrlichttypes_bloated.h" #include "networkprotocol.h" #include diff --git a/src/network/networkprotocol.cpp b/src/network/networkprotocol.cpp new file mode 100644 index 000000000..40f8acef1 --- /dev/null +++ b/src/network/networkprotocol.cpp @@ -0,0 +1,67 @@ +// Minetest +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "networkprotocol.h" + + +/* + PROTOCOL VERSION < 37: + Until (and including) version 0.4.17.1 + PROTOCOL VERSION 37: + Redo detached inventory sending + Add TOCLIENT_NODEMETA_CHANGED + New network float format + ContentFeatures version 13 + Add full Euler rotations instead of just yaw + Add TOCLIENT_PLAYER_SPEED + [bump for 5.0.0] + PROTOCOL VERSION 38: + Incremental inventory sending mode + Unknown inventory serialization fields no longer throw an error + Mod-specific formspec version + Player FOV override API + "ephemeral" added to TOCLIENT_PLAY_SOUND + PROTOCOL VERSION 39: + Updated set_sky packet + Adds new sun, moon and stars packets + Minimap modes + PROTOCOL VERSION 40: + TOCLIENT_MEDIA_PUSH changed, TOSERVER_HAVE_MEDIA added + PROTOCOL VERSION 41: + Added new particlespawner parameters + [scheduled bump for 5.6.0] + PROTOCOL VERSION 42: + TOSERVER_UPDATE_CLIENT_INFO added + new fields for TOCLIENT_SET_LIGHTING and TOCLIENT_SET_SKY + Send forgotten TweenedParameter properties + [scheduled bump for 5.7.0] + PROTOCOL VERSION 43: + "start_time" added to TOCLIENT_PLAY_SOUND + place_param2 type change u8 -> optional + [scheduled bump for 5.8.0] + PROTOCOL VERSION 44: + AO_CMD_SET_BONE_POSITION extended + Add TOCLIENT_MOVE_PLAYER_REL + Move default minimap from client-side C++ to server-side builtin Lua + [scheduled bump for 5.9.0] + PROTOCOL VERSION 45: + Minimap HUD element supports negative size values as percentages + [bump for 5.9.1] + PROTOCOL VERSION 46: + Move default hotbar from client-side C++ to server-side builtin Lua + Add shadow tint to Lighting packets + Add shadow color to CloudParam packets + Move death screen to server and make it a regular formspec + The server no longer triggers the hardcoded client-side death + formspec, but the client still supports it for compatibility with + old servers. + Rename TOCLIENT_DEATHSCREEN to TOCLIENT_DEATHSCREEN_LEGACY + Rename TOSERVER_RESPAWN to TOSERVER_RESPAWN_LEGACY + Support float animation frame numbers in TOCLIENT_LOCAL_PLAYER_ANIMATIONS + [scheduled bump for 5.10.0] +*/ + +const u16 LATEST_PROTOCOL_VERSION = 46; + +// See also formspec [Version History] in doc/lua_api.md +const u16 FORMSPEC_API_VERSION = 8; diff --git a/src/network/networkprotocol.h b/src/network/networkprotocol.h index e3618042f..4ee02209a 100644 --- a/src/network/networkprotocol.h +++ b/src/network/networkprotocol.h @@ -19,232 +19,18 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once -#include "util/string.h" +#include "irrTypes.h" +using namespace irr; -/* - changes by PROTOCOL_VERSION: - - PROTOCOL_VERSION 3: - Base for writing changes here - PROTOCOL_VERSION 4: - Add TOCLIENT_MEDIA - Add TOCLIENT_TOOLDEF - Add TOCLIENT_NODEDEF - Add TOCLIENT_CRAFTITEMDEF - Add TOSERVER_INTERACT - Obsolete TOSERVER_CLICK_ACTIVEOBJECT - Obsolete TOSERVER_GROUND_ACTION - PROTOCOL_VERSION 5: - Make players to be handled mostly as ActiveObjects - PROTOCOL_VERSION 6: - Only non-cached textures are sent - PROTOCOL_VERSION 7: - Add TOCLIENT_ITEMDEF - Obsolete TOCLIENT_TOOLDEF - Obsolete TOCLIENT_CRAFTITEMDEF - Compress the contents of TOCLIENT_ITEMDEF and TOCLIENT_NODEDEF - PROTOCOL_VERSION 8: - Digging based on item groups - Many things - PROTOCOL_VERSION 9: - ContentFeatures and NodeDefManager use a different serialization - format; better for future version cross-compatibility - Many things - Obsolete TOCLIENT_PLAYERITEM - PROTOCOL_VERSION 10: - TOCLIENT_PRIVILEGES - Version raised to force 'fly' and 'fast' privileges into effect. - Node metadata change (came in later; somewhat incompatible) - PROTOCOL_VERSION 11: - TileDef in ContentFeatures - Nodebox drawtype - (some dev snapshot) - TOCLIENT_INVENTORY_FORMSPEC - (0.4.0, 0.4.1) - PROTOCOL_VERSION 12: - TOSERVER_INVENTORY_FIELDS - 16-bit node ids - TOCLIENT_DETACHED_INVENTORY - PROTOCOL_VERSION 13: - InventoryList field "Width" (deserialization fails with old versions) - PROTOCOL_VERSION 14: - Added transfer of player pressed keys to the server - Added new messages for mesh and bone animation, as well as attachments - AO_CMD_SET_ANIMATION - AO_CMD_SET_BONE_POSITION - GENERIC_CMD_SET_ATTACHMENT - PROTOCOL_VERSION 15: - Serialization format changes - PROTOCOL_VERSION 16: - TOCLIENT_SHOW_FORMSPEC - PROTOCOL_VERSION 17: - Serialization format change: include backface_culling flag in TileDef - Added rightclickable field in nodedef - TOCLIENT_SPAWN_PARTICLE - TOCLIENT_ADD_PARTICLESPAWNER - TOCLIENT_DELETE_PARTICLESPAWNER - PROTOCOL_VERSION 18: - damageGroups added to ToolCapabilities - sound_place added to ItemDefinition - PROTOCOL_VERSION 19: - AO_CMD_SET_PHYSICS_OVERRIDE - PROTOCOL_VERSION 20: - TOCLIENT_HUDADD - TOCLIENT_HUDRM - TOCLIENT_HUDCHANGE - TOCLIENT_HUD_SET_FLAGS - PROTOCOL_VERSION 21: - TOCLIENT_BREATH - TOSERVER_BREATH - range added to ItemDefinition - drowning, leveled and liquid_range added to ContentFeatures - stepheight and collideWithObjects added to object properties - version, heat and humidity transfer in MapBock - automatic_face_movement_dir and automatic_face_movement_dir_offset - added to object properties - PROTOCOL_VERSION 22: - add swap_node - PROTOCOL_VERSION 23: - Obsolete TOSERVER_RECEIVED_MEDIA - Server: Stop using TOSERVER_CLIENT_READY - PROTOCOL_VERSION 24: - ContentFeatures version 7 - ContentFeatures: change number of special tiles to 6 (CF_SPECIAL_COUNT) - PROTOCOL_VERSION 25: - Rename TOCLIENT_ACCESS_DENIED to TOCLIENT_ACCESS_DENIED_LEGAGY - Rename TOCLIENT_DELETE_PARTICLESPAWNER to - TOCLIENT_DELETE_PARTICLESPAWNER_LEGACY - Rename TOSERVER_PASSWORD to TOSERVER_PASSWORD_LEGACY - Rename TOSERVER_INIT to TOSERVER_INIT_LEGACY - Rename TOCLIENT_INIT to TOCLIENT_INIT_LEGACY - Add TOCLIENT_ACCESS_DENIED new opcode (0x0A), using error codes - for standard error, keeping customisation possible. This - permit translation - Add TOCLIENT_DELETE_PARTICLESPAWNER (0x53), fixing the u16 read and - reading u32 - Add new opcode TOSERVER_INIT for client presentation to server - Add new opcodes TOSERVER_FIRST_SRP, TOSERVER_SRP_BYTES_A, - TOSERVER_SRP_BYTES_M, TOCLIENT_SRP_BYTES_S_B - for the three supported auth mechanisms around srp - Add new opcodes TOCLIENT_ACCEPT_SUDO_MODE and TOCLIENT_DENY_SUDO_MODE - for sudo mode handling (auth mech generic way of changing password). - Add TOCLIENT_HELLO for presenting server to client after client - presentation - Add TOCLIENT_AUTH_ACCEPT to accept connection from client - Rename GENERIC_CMD_SET_ATTACHMENT to AO_CMD_ATTACH_TO - PROTOCOL_VERSION 26: - Add TileDef tileable_horizontal, tileable_vertical flags - PROTOCOL_VERSION 27: - backface_culling: backwards compatibility for playing with - newer client on pre-27 servers. - Add nodedef v3 - connected nodeboxes - PROTOCOL_VERSION 28: - CPT2_MESHOPTIONS - PROTOCOL_VERSION 29: - Server doesn't accept TOSERVER_BREATH anymore - serialization of TileAnimation params changed - TAT_SHEET_2D - Removed client-sided chat perdiction - PROTOCOL VERSION 30: - New ContentFeatures serialization version - Add node and tile color and palette - Fix plantlike visual_scale being applied squared and add compatibility - with pre-30 clients by sending sqrt(visual_scale) - PROTOCOL VERSION 31: - Add tile overlay - Stop sending TOSERVER_CLIENT_READY - PROTOCOL VERSION 32: - Add fading sounds - PROTOCOL VERSION 33: - Add TOCLIENT_UPDATE_PLAYER_LIST and send the player list to the client, - instead of guessing based on the active object list. - PROTOCOL VERSION 34: - Add sound pitch - PROTOCOL VERSION 35: - Rename TOCLIENT_CHAT_MESSAGE to TOCLIENT_CHAT_MESSAGE_OLD (0x30) - Add TOCLIENT_CHAT_MESSAGE (0x2F) - This chat message is a signalisation message containing various - informations: - * timestamp - * sender - * type (RAW, NORMAL, ANNOUNCE, SYSTEM) - * content - Add TOCLIENT_CSM_RESTRICTION_FLAGS to define which CSM features should be - limited - Add settable player collisionbox. Breaks compatibility with older - clients as a 1-node vertical offset has been removed from player's - position - Add settable player stepheight using existing object property. - Breaks compatibility with older clients. - PROTOCOL VERSION 36: - Backwards compatibility drop - Add 'can_zoom' to player object properties - Add glow to object properties - Change TileDef serialization format. - Add world-aligned tiles. - Mod channels - Raise ObjectProperties version to 3 for removing 'can_zoom' and adding - 'zoom_fov'. - Nodebox version 5 - Add disconnected nodeboxes - Add TOCLIENT_FORMSPEC_PREPEND - PROTOCOL VERSION 37: - Redo detached inventory sending - Add TOCLIENT_NODEMETA_CHANGED - New network float format - ContentFeatures version 13 - Add full Euler rotations instead of just yaw - Add TOCLIENT_PLAYER_SPEED - PROTOCOL VERSION 38: - Incremental inventory sending mode - Unknown inventory serialization fields no longer throw an error - Mod-specific formspec version - Player FOV override API - "ephemeral" added to TOCLIENT_PLAY_SOUND - PROTOCOL VERSION 39: - Updated set_sky packet - Adds new sun, moon and stars packets - Minimap modes - PROTOCOL VERSION 40: - TOCLIENT_MEDIA_PUSH changed, TOSERVER_HAVE_MEDIA added - PROTOCOL VERSION 41: - Added new particlespawner parameters - [scheduled bump for 5.6.0] - PROTOCOL VERSION 42: - TOSERVER_UPDATE_CLIENT_INFO added - new fields for TOCLIENT_SET_LIGHTING and TOCLIENT_SET_SKY - Send forgotten TweenedParameter properties - [scheduled bump for 5.7.0] - PROTOCOL VERSION 43: - "start_time" added to TOCLIENT_PLAY_SOUND - place_param2 type change u8 -> optional - [scheduled bump for 5.8.0] - PROTOCOL VERSION 44: - AO_CMD_SET_BONE_POSITION extended - Add TOCLIENT_MOVE_PLAYER_REL - Move default minimap from client-side C++ to server-side builtin Lua - [scheduled bump for 5.9.0] -*/ - -#define LATEST_PROTOCOL_VERSION 44 -#define LATEST_PROTOCOL_VERSION_STRING TOSTRING(LATEST_PROTOCOL_VERSION) +extern const u16 LATEST_PROTOCOL_VERSION; // Server's supported network protocol range -#define SERVER_PROTOCOL_VERSION_MIN 37 -#define SERVER_PROTOCOL_VERSION_MAX LATEST_PROTOCOL_VERSION +constexpr u16 SERVER_PROTOCOL_VERSION_MIN = 37; // Client's supported network protocol range -#define CLIENT_PROTOCOL_VERSION_MIN 37 -#define CLIENT_PROTOCOL_VERSION_MAX LATEST_PROTOCOL_VERSION +constexpr u16 CLIENT_PROTOCOL_VERSION_MIN = 37; -// Constant that differentiates the protocol from random data and other protocols -#define PROTOCOL_ID 0x4f457403 - -#define PASSWORD_SIZE 28 // Maximum password length. Allows for - // base64-encoded SHA-1 (27+\0). - -// See also formspec [Version History] in doc/lua_api.md -#define FORMSPEC_API_VERSION 7 +extern const u16 FORMSPEC_API_VERSION; #define TEXTURENAME_ALLOWED_CHARS "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.-" @@ -257,10 +43,10 @@ enum ToClientCommand : u16 Sent after TOSERVER_INIT. u8 deployed serialization version - u16 deployed network compression mode + u16 unused (network compression, never implemeneted) u16 deployed protocol version u32 supported auth methods - std::string username that should be used for legacy hash (for proper casing) + std::string unused (used to be username) */ TOCLIENT_AUTH_ACCEPT = 0x03, /* @@ -388,10 +174,10 @@ enum ToClientCommand : u16 f32 transition_time */ - TOCLIENT_DEATHSCREEN = 0x37, + TOCLIENT_DEATHSCREEN_LEGACY = 0x37, /* - u8 bool set camera point target - v3f1000 camera point target (to point the death cause or whatever) + u8 bool unused + v3f1000 unused */ TOCLIENT_MEDIA = 0x38, @@ -911,7 +697,7 @@ enum ToServerCommand : u16 Sent first after connected. u8 serialization version (=SER_FMT_VER_HIGHEST_READ) - u16 supported network compression modes + u16 unused (supported network compression modes, never implemeneted) u16 minimum supported network protocol version u16 maximum supported network protocol version std::string player name @@ -954,6 +740,8 @@ enum ToServerCommand : u16 [2+12+12+4+4+4] u8 fov*80 [2+12+12+4+4+4+1] u8 ceil(wanted_range / MAP_BLOCKSIZE) [2+12+12+4+4+4+1+1] u8 camera_inverted (bool) + [2+12+12+4+4+4+1+1+1] f32 movement_speed + [2+12+12+4+4+4+1+1+1+4] f32 movement_direction */ @@ -999,10 +787,7 @@ enum ToServerCommand : u16 [2] u16 item */ - TOSERVER_RESPAWN = 0x38, - /* - u16 TOSERVER_RESPAWN - */ + TOSERVER_RESPAWN_LEGACY = 0x38, TOSERVER_INTERACT = 0x39, /* @@ -1146,26 +931,6 @@ enum AccessDeniedCode : u8 { SERVER_ACCESSDENIED_MAX, }; -enum NetProtoCompressionMode { - NETPROTO_COMPRESSION_NONE = 0, -}; - -constexpr const char *accessDeniedStrings[SERVER_ACCESSDENIED_MAX] = { - "Invalid password", - "Your client sent something the server didn't expect. Try reconnecting or updating your client.", - "The server is running in simple singleplayer mode. You cannot connect.", - "Your client's version is not supported.\nPlease contact the server administrator.", - "Player name contains disallowed characters", - "Player name not allowed", - "Too many users", - "Empty passwords are disallowed. Set a password and try again.", - "Another client is connected with this name. If your client closed unexpectedly, try again in a minute.", - "Internal server error", - "", - "Server shutting down", - "The server has experienced an internal error. You will now be disconnected." -}; - enum PlayerListModifer : u8 { PLAYER_LIST_INIT, diff --git a/src/network/peerhandler.h b/src/network/peerhandler.h index da65483ef..adda995b3 100644 --- a/src/network/peerhandler.h +++ b/src/network/peerhandler.h @@ -19,59 +19,30 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once -#include "networkprotocol.h" - namespace con { -typedef enum -{ - MIN_RTT, - MAX_RTT, - AVG_RTT, - MIN_JITTER, - MAX_JITTER, - AVG_JITTER -} rtt_stat_type; - -class Peer; +class IPeer; class PeerHandler { public: PeerHandler() = default; - virtual ~PeerHandler() = default; + // Note: all functions are called from within a Receive() call on the same thread. + /* This is called after the Peer has been inserted into the Connection's peer container. */ - virtual void peerAdded(Peer *peer) = 0; + virtual void peerAdded(IPeer *peer) = 0; /* This is called before the Peer has been removed from the Connection's peer container. */ - virtual void deletingPeer(Peer *peer, bool timeout) = 0; + virtual void deletingPeer(IPeer *peer, bool timeout) = 0; }; -enum PeerChangeType : u8 -{ - PEER_ADDED, - PEER_REMOVED -}; - -struct PeerChange -{ - PeerChange(PeerChangeType t, session_t _peer_id, bool _timeout) : - type(t), peer_id(_peer_id), timeout(_timeout) - { - } - PeerChange() = delete; - - PeerChangeType type; - session_t peer_id; - bool timeout; -}; } diff --git a/src/network/serveropcodes.cpp b/src/network/serveropcodes.cpp index 1cb413492..4b87c3fbe 100644 --- a/src/network/serveropcodes.cpp +++ b/src/network/serveropcodes.cpp @@ -82,7 +82,7 @@ const ToServerCommandHandler toServerCommandTable[TOSERVER_NUM_MSG_TYPES] = { "TOSERVER_DAMAGE", TOSERVER_STATE_INGAME, &Server::handleCommand_Damage }, // 0x35 null_command_handler, // 0x36 { "TOSERVER_PLAYERITEM", TOSERVER_STATE_INGAME, &Server::handleCommand_PlayerItem }, // 0x37 - { "TOSERVER_RESPAWN", TOSERVER_STATE_INGAME, &Server::handleCommand_Respawn }, // 0x38 + null_command_handler, // 0x38 { "TOSERVER_INTERACT", TOSERVER_STATE_INGAME, &Server::handleCommand_Interact }, // 0x39 { "TOSERVER_REMOVED_SOUNDS", TOSERVER_STATE_INGAME, &Server::handleCommand_RemovedSounds }, // 0x3a { "TOSERVER_NODEMETA_FIELDS", TOSERVER_STATE_INGAME, &Server::handleCommand_NodeMetaFields }, // 0x3b @@ -181,7 +181,7 @@ const ClientCommandFactory clientCommandFactoryTable[TOCLIENT_NUM_MSG_TYPES] = { "TOCLIENT_MOVE_PLAYER", 0, true }, // 0x34 null_command_factory, // 0x35 { "TOCLIENT_FOV", 0, true }, // 0x36 - { "TOCLIENT_DEATHSCREEN", 0, true }, // 0x37 + null_command_factory, // 0x37 { "TOCLIENT_MEDIA", 2, true }, // 0x38 null_command_factory, // 0x39 { "TOCLIENT_NODEDEF", 0, true }, // 0x3A diff --git a/src/network/serveropcodes.h b/src/network/serveropcodes.h index 275270ab9..509d2b4b2 100644 --- a/src/network/serveropcodes.h +++ b/src/network/serveropcodes.h @@ -21,7 +21,6 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once #include "server.h" -#include "networkprotocol.h" class NetworkPacket; // Note: don't forward-declare Server here (#14324) diff --git a/src/network/serverpackethandler.cpp b/src/network/serverpackethandler.cpp index 9de39229b..449e164b6 100644 --- a/src/network/serverpackethandler.cpp +++ b/src/network/serverpackethandler.cpp @@ -101,12 +101,12 @@ void Server::handleCommand_Init(NetworkPacket* pkt) // First byte after command is maximum supported // serialization version u8 client_max; - u16 supp_compr_modes; + u16 unused; u16 min_net_proto_version = 0; u16 max_net_proto_version; std::string playerName; - *pkt >> client_max >> supp_compr_modes >> min_net_proto_version + *pkt >> client_max >> unused >> min_net_proto_version >> max_net_proto_version >> playerName; u8 our_max = SER_FMT_VER_HIGHEST_READ; @@ -135,10 +135,10 @@ void Server::handleCommand_Init(NetworkPacket* pkt) // Figure out a working version if it is possible at all if (max_net_proto_version >= SERVER_PROTOCOL_VERSION_MIN || - min_net_proto_version <= SERVER_PROTOCOL_VERSION_MAX) { + min_net_proto_version <= LATEST_PROTOCOL_VERSION) { // If maximum is larger than our maximum, go with our maximum - if (max_net_proto_version > SERVER_PROTOCOL_VERSION_MAX) - net_proto_version = SERVER_PROTOCOL_VERSION_MAX; + if (max_net_proto_version > LATEST_PROTOCOL_VERSION) + net_proto_version = LATEST_PROTOCOL_VERSION; // Else go with client's maximum else net_proto_version = max_net_proto_version; @@ -190,9 +190,6 @@ void Server::handleCommand_Init(NetworkPacket* pkt) } m_clients.setPlayerName(peer_id, playername); - //TODO (later) case insensitivity - - std::string legacyPlayerNameCasing = playerName; if (!isSingleplayer() && strcasecmp(playername, "singleplayer") == 0) { actionstream << "Server: Player with the name \"singleplayer\" tried " @@ -279,17 +276,14 @@ void Server::handleCommand_Init(NetworkPacket* pkt) verbosestream << "Sending TOCLIENT_HELLO with auth method field: " << auth_mechs << std::endl; - NetworkPacket resp_pkt(TOCLIENT_HELLO, - 1 + 4 + legacyPlayerNameCasing.size(), peer_id); + NetworkPacket resp_pkt(TOCLIENT_HELLO, 0, peer_id); - u16 depl_compress_mode = NETPROTO_COMPRESSION_NONE; - resp_pkt << depl_serial_v << depl_compress_mode << net_proto_version - << auth_mechs << legacyPlayerNameCasing; + resp_pkt << depl_serial_v << u16(0) << net_proto_version + << auth_mechs << std::string_view(); Send(&resp_pkt); client->allowed_auth_mechs = auth_mechs; - client->setDeployedCompressionMode(depl_compress_mode); m_clients.event(peer_id, CSE_Hello); } @@ -483,12 +477,24 @@ void Server::process_PlayerPos(RemotePlayer *player, PlayerSAO *playersao, u8 bits = 0; // bits instead of bool so it is extensible later *pkt >> keyPressed; + player->control.unpackKeysPressed(keyPressed); + *pkt >> f32fov; fov = (f32)f32fov / 80.0f; *pkt >> wanted_range; + if (pkt->getRemainingBytes() >= 1) *pkt >> bits; + if (pkt->getRemainingBytes() >= 8) { + *pkt >> player->control.movement_speed; + *pkt >> player->control.movement_direction; + } else { + player->control.movement_speed = 0.0f; + player->control.movement_direction = 0.0f; + player->control.setMovementFromKeys(); + } + v3f position((f32)ps.X / 100.0f, (f32)ps.Y / 100.0f, (f32)ps.Z / 100.0f); v3f speed((f32)ss.X / 100.0f, (f32)ss.Y / 100.0f, (f32)ss.Z / 100.0f); @@ -507,8 +513,6 @@ void Server::process_PlayerPos(RemotePlayer *player, PlayerSAO *playersao, playersao->setWantedRange(wanted_range); playersao->setCameraInverted(bits & 0x01); - player->control.unpackKeysPressed(keyPressed); - if (playersao->checkMovementCheat()) { // Call callbacks m_script->on_cheat(playersao, "moved_too_fast"); @@ -864,33 +868,6 @@ void Server::handleCommand_PlayerItem(NetworkPacket* pkt) playersao->getPlayer()->setWieldIndex(item); } -void Server::handleCommand_Respawn(NetworkPacket* pkt) -{ - session_t peer_id = pkt->getPeerId(); - RemotePlayer *player = m_env->getPlayer(peer_id); - if (player == NULL) { - errorstream << - "Server::ProcessData(): Canceling: No player for peer_id=" << - peer_id << " disconnecting peer!" << std::endl; - DisconnectPeer(peer_id); - return; - } - - PlayerSAO *playersao = player->getPlayerSAO(); - assert(playersao); - - if (!playersao->isDead()) - return; - - RespawnPlayer(peer_id); - - actionstream << player->getName() << " respawns at " - << (playersao->getBasePosition() / BS) << std::endl; - - // ActiveObject is added to environment in AsyncRunStep after - // the previous addition has been successfully removed -} - bool Server::checkInteractDistance(RemotePlayer *player, const f32 d, const std::string &what) { ItemStack selected_item, hand_item; @@ -1034,12 +1011,12 @@ void Server::handleCommand_Interact(NetworkPacket *pkt) /* Check that target is reasonably close */ - static thread_local const bool enable_anticheat = - !g_settings->getBool("disable_anticheat"); + static thread_local const u32 anticheat_flags = + g_settings->getFlagStr("anticheat_flags", flagdesc_anticheat, nullptr); if ((action == INTERACT_START_DIGGING || action == INTERACT_DIGGING_COMPLETED || action == INTERACT_PLACE || action == INTERACT_USE) && - enable_anticheat && !isSingleplayer()) { + (anticheat_flags & AC_INTERACTION) && !isSingleplayer()) { v3f target_pos = player_pos; if (pointed.type == POINTEDTHING_NODE) { target_pos = intToFloat(pointed.node_undersurface, BS); @@ -1142,7 +1119,7 @@ void Server::handleCommand_Interact(NetworkPacket *pkt) /* Cheat prevention */ bool is_valid_dig = true; - if (enable_anticheat && !isSingleplayer()) { + if ((anticheat_flags & AC_DIGGING) && !isSingleplayer()) { v3s16 nocheat_p = playersao->getNoCheatDigPos(); float nocheat_t = playersao->getNoCheatDigTime(); playersao->noCheatDigEnd(); @@ -1351,15 +1328,22 @@ static bool pkt_read_formspec_fields(NetworkPacket *pkt, StringMap &fields) u16 field_count; *pkt >> field_count; - u64 length = 0; + size_t length = 0; for (u16 k = 0; k < field_count; k++) { - std::string fieldname; + std::string fieldname, fieldvalue; *pkt >> fieldname; - fields[fieldname] = pkt->readLongString(); + fieldvalue = pkt->readLongString(); - length += fieldname.size(); - length += fields[fieldname].size(); + fieldname = sanitize_untrusted(fieldname, false); + // We'd love to strip escapes here but some formspec elements reflect data + // from the server (e.g. dropdown), which can contain translations. + fieldvalue = sanitize_untrusted(fieldvalue); + + length += fieldname.size() + fieldvalue.size(); + + fields[std::move(fieldname)] = std::move(fieldvalue); } + // 640K ought to be enough for anyone return length < 640 * 1024; } diff --git a/src/network/socket.cpp b/src/network/socket.cpp index 9fbbaa34e..17e71d860 100644 --- a/src/network/socket.cpp +++ b/src/network/socket.cpp @@ -50,9 +50,6 @@ typedef int socklen_t; #define SOCKET_ERR_STR(e) strerror(e) #endif -// Set to true to enable verbose debug output -bool socket_enable_debug_output = false; // yuck - static bool g_sockets_initialized = false; // Initialize sockets @@ -104,12 +101,6 @@ bool UDPSocket::init(bool ipv6, bool noExceptions) m_addr_family = ipv6 ? AF_INET6 : AF_INET; m_handle = socket(m_addr_family, SOCK_DGRAM, IPPROTO_UDP); - if (socket_enable_debug_output) { - tracestream << "UDPSocket(" << (int)m_handle - << ")::UDPSocket(): ipv6 = " << (ipv6 ? "true" : "false") - << std::endl; - } - if (m_handle < 0) { if (noExceptions) { return false; @@ -135,11 +126,6 @@ bool UDPSocket::init(bool ipv6, bool noExceptions) UDPSocket::~UDPSocket() { - if (socket_enable_debug_output) { - tracestream << "UDPSocket( " << (int)m_handle << ")::~UDPSocket()" - << std::endl; - } - if (m_handle >= 0) { #ifdef _WIN32 closesocket(m_handle); @@ -151,12 +137,6 @@ UDPSocket::~UDPSocket() void UDPSocket::Bind(Address addr) { - if (socket_enable_debug_output) { - tracestream << "UDPSocket(" << (int)m_handle - << ")::Bind(): " << addr.serializeString() << ":" - << addr.getPort() << std::endl; - } - if (addr.getFamily() != m_addr_family) { const char *errmsg = "Socket and bind address families do not match"; @@ -202,30 +182,6 @@ void UDPSocket::Send(const Address &destination, const void *data, int size) if (INTERNET_SIMULATOR) dumping_packet = myrand() % INTERNET_SIMULATOR_PACKET_LOSS == 0; - if (socket_enable_debug_output) { - // Print packet destination and size - tracestream << (int)m_handle << " -> "; - destination.print(tracestream); - tracestream << ", size=" << size; - - // Print packet contents - tracestream << ", data="; - for (int i = 0; i < size && i < 20; i++) { - if (i % 2 == 0) - tracestream << " "; - unsigned int a = ((const unsigned char *)data)[i]; - tracestream << std::hex << std::setw(2) << std::setfill('0') << a; - } - - if (size > 20) - tracestream << "..."; - - if (dumping_packet) - tracestream << " (DUMPED BY INTERNET_SIMULATOR)"; - - tracestream << std::endl; - } - if (dumping_packet) { // Lol let's forget it tracestream << "UDPSocket::Send(): INTERNET_SIMULATOR: dumping packet." @@ -302,26 +258,6 @@ int UDPSocket::Receive(Address &sender, void *data, int size) sender = Address(address_ip, address_port); } - if (socket_enable_debug_output) { - // Print packet sender and size - tracestream << (int)m_handle << " <- "; - sender.print(tracestream); - tracestream << ", size=" << received; - - // Print packet contents - tracestream << ", data="; - for (int i = 0; i < received && i < 20; i++) { - if (i % 2 == 0) - tracestream << " "; - unsigned int a = ((const unsigned char *)data)[i]; - tracestream << std::hex << std::setw(2) << std::setfill('0') << a; - } - if (received > 20) - tracestream << "..."; - - tracestream << std::endl; - } - return received; } diff --git a/src/network/socket.h b/src/network/socket.h index c3758a9d8..28b69c7b8 100644 --- a/src/network/socket.h +++ b/src/network/socket.h @@ -25,8 +25,6 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "irrlichttypes.h" #include "networkexceptions.h" -extern bool socket_enable_debug_output; - void sockets_init(); void sockets_cleanup(); diff --git a/src/nodedef.cpp b/src/nodedef.cpp index 2914cc3aa..811753c89 100644 --- a/src/nodedef.cpp +++ b/src/nodedef.cpp @@ -712,8 +712,6 @@ static void fillTileAttribs(ITextureSource *tsrc, TileLayer *layer, if (!tile.world_aligned) layer->scale = 1; - layer->flags_texture = tsrc->getShaderFlagsTexture(layer->normal_texture ? true : false); - // Material flags layer->material_flags = 0; if (backface_culling) @@ -753,18 +751,13 @@ static void fillTileAttribs(ITextureSource *tsrc, TileLayer *layer, std::ostringstream os(std::ios::binary); for (int i = 0; i < frame_count; i++) { - FrameSpec frame; - os.str(""); os << tiledef.name; tiledef.animation.getTextureModifer(os, layer->texture->getOriginalSize(), i); + FrameSpec &frame = (*layer->frames)[i]; frame.texture = tsrc->getTextureForMesh(os.str(), &frame.texture_id); - if (layer->normal_texture) - frame.normal_texture = tsrc->getNormalTexture(os.str()); - frame.flags_texture = layer->flags_texture; - (*layer->frames)[i] = frame; } } } diff --git a/src/nodemetadata.cpp b/src/nodemetadata.cpp index a11503ebe..a86db15ad 100644 --- a/src/nodemetadata.cpp +++ b/src/nodemetadata.cpp @@ -62,14 +62,14 @@ void NodeMetadata::serialize(std::ostream &os, u8 version, bool disk) const void NodeMetadata::deSerialize(std::istream &is, u8 version) { clear(); - int num_vars = readU32(is); - for(int i=0; i= 2) { if (readU8(is) == 1) - markPrivate(name, true); + m_privatevars.insert(name); } } @@ -89,12 +89,12 @@ bool NodeMetadata::empty() const } -void NodeMetadata::markPrivate(const std::string &name, bool set) +bool NodeMetadata::markPrivate(const std::string &name, bool set) { if (set) - m_privatevars.insert(name); + return m_privatevars.insert(name).second; else - m_privatevars.erase(name); + return m_privatevars.erase(name) > 0; } int NodeMetadata::countNonPrivate() const @@ -144,6 +144,8 @@ void NodeMetadataList::serialize(std::ostream &os, u8 blockver, bool disk, writeS16(os, p.Z); } else { // Serialize positions within a mapblock + static_assert(MAP_BLOCKSIZE * MAP_BLOCKSIZE * MAP_BLOCKSIZE <= U16_MAX, + "position too big to serialize"); u16 p16 = (p.Z * MAP_BLOCKSIZE + p.Y) * MAP_BLOCKSIZE + p.X; writeU16(os, p16); } @@ -246,8 +248,7 @@ void NodeMetadataList::set(v3s16 p, NodeMetadata *d) void NodeMetadataList::clear() { if (m_is_metadata_owner) { - NodeMetadataMap::const_iterator it; - for (it = m_data.begin(); it != m_data.end(); ++it) + for (auto it = m_data.begin(); it != m_data.end(); ++it) delete it->second; } m_data.clear(); diff --git a/src/nodemetadata.h b/src/nodemetadata.h index da277aabd..3c2a67f53 100644 --- a/src/nodemetadata.h +++ b/src/nodemetadata.h @@ -57,7 +57,10 @@ public: { return m_privatevars.count(name) != 0; } - void markPrivate(const std::string &name, bool set); + + /// Marks a key as private. + /// @return metadata modified? + bool markPrivate(const std::string &name, bool set); private: int countNonPrivate() const; diff --git a/src/object_properties.cpp b/src/object_properties.cpp index 5fb6a7d41..7a70714a2 100644 --- a/src/object_properties.cpp +++ b/src/object_properties.cpp @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "log.h" #include "util/serialize.h" #include +#include static const video::SColor NULL_BGCOLOR{0, 1, 1, 1}; @@ -85,6 +86,27 @@ std::string ObjectProperties::dump() const return os.str(); } +static auto tie(const ObjectProperties &o) +{ + // Make sure to add new members to this list! + return std::tie( + o.textures, o.colors, o.collisionbox, o.selectionbox, o.visual, o.mesh, + o.damage_texture_modifier, o.nametag, o.infotext, o.wield_item, o.visual_size, + o.nametag_color, o.nametag_bgcolor, o.spritediv, o.initial_sprite_basepos, + o.stepheight, o.automatic_rotate, o.automatic_face_movement_dir_offset, + o.automatic_face_movement_max_rotation_per_sec, o.eye_height, o.zoom_fov, + o.hp_max, o.breath_max, o.glow, o.pointable, o.physical, o.collideWithObjects, + o.rotate_selectionbox, o.is_visible, o.makes_footstep_sound, + o.automatic_face_movement_dir, o.backface_culling, o.static_save, o.use_texture_alpha, + o.shaded, o.show_on_minimap + ); +} + +bool ObjectProperties::operator==(const ObjectProperties &other) const +{ + return tie(*this) == tie(other); +} + bool ObjectProperties::validate() { const char *func = "ObjectProperties::validate(): "; diff --git a/src/object_properties.h b/src/object_properties.h index 1f8384c77..88c2a2678 100644 --- a/src/object_properties.h +++ b/src/object_properties.h @@ -20,11 +20,9 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once #include -#include #include #include "irrlichttypes_bloated.h" #include -#include #include #include "util/pointabilities.h" @@ -77,28 +75,10 @@ struct ObjectProperties std::string dump() const; -private: - auto tie() const { - // Make sure to add new members to this list! - return std::tie( - textures, colors, collisionbox, selectionbox, visual, mesh, damage_texture_modifier, - nametag, infotext, wield_item, visual_size, nametag_color, nametag_bgcolor, - spritediv, initial_sprite_basepos, stepheight, automatic_rotate, - automatic_face_movement_dir_offset, automatic_face_movement_max_rotation_per_sec, - eye_height, zoom_fov, hp_max, breath_max, glow, pointable, physical, - collideWithObjects, rotate_selectionbox, is_visible, makes_footstep_sound, - automatic_face_movement_dir, backface_culling, static_save, use_texture_alpha, - shaded, show_on_minimap - ); - } - -public: - bool operator==(const ObjectProperties &other) const { - return tie() == other.tie(); - }; + bool operator==(const ObjectProperties &other) const; bool operator!=(const ObjectProperties &other) const { - return tie() != other.tie(); - }; + return !(*this == other); + } /** * Check limits of some important properties that'd cause exceptions later on. diff --git a/src/pathfinder.cpp b/src/pathfinder.cpp index 5420431f5..8b90a139c 100644 --- a/src/pathfinder.cpp +++ b/src/pathfinder.cpp @@ -40,6 +40,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #endif +#include + /******************************************************************************/ /* Typedefs and macros */ /******************************************************************************/ diff --git a/src/player.cpp b/src/player.cpp index fd902aa83..7361549e0 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -28,6 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "settings.h" #include "log.h" #include "porting.h" // strlcpy +#include bool is_valid_player_name(std::string_view name) { @@ -172,6 +173,42 @@ u16 Player::getMaxHotbarItemcount() return mainlist ? std::min(mainlist->getSize(), (u32) hud_hotbar_itemcount) : 0; } +void PlayerControl::setMovementFromKeys() +{ + bool a_up = direction_keys & (1 << 0), + a_down = direction_keys & (1 << 1), + a_left = direction_keys & (1 << 2), + a_right = direction_keys & (1 << 3); + + if (a_up || a_down || a_left || a_right) { + // if contradictory keys pressed, stay still + if (a_up && a_down && a_left && a_right) + movement_speed = 0.0f; + else if (a_up && a_down && !a_left && !a_right) + movement_speed = 0.0f; + else if (!a_up && !a_down && a_left && a_right) + movement_speed = 0.0f; + else + // If there is a keyboard event, assume maximum speed + movement_speed = 1.0f; + } + + // Check keyboard for input + float x = 0, y = 0; + if (a_up) + y += 1; + if (a_down) + y -= 1; + if (a_left) + x -= 1; + if (a_right) + x += 1; + + if (x != 0 || y != 0) + // If there is a keyboard event, it takes priority + movement_direction = std::atan2(x, y); +} + #ifndef SERVER u32 PlayerControl::getKeysPressed() const @@ -229,3 +266,24 @@ void PlayerControl::unpackKeysPressed(u32 keypress_bits) place = keypress_bits & (1 << 8); zoom = keypress_bits & (1 << 9); } + +v2f PlayerControl::getMovement() const +{ + return v2f(std::sin(movement_direction), std::cos(movement_direction)) * movement_speed; +} + +static auto tie(const PlayerPhysicsOverride &o) +{ + // Make sure to add new members to this list! + return std::tie( + o.speed, o.jump, o.gravity, o.sneak, o.sneak_glitch, o.new_move, o.speed_climb, + o.speed_crouch, o.liquid_fluidity, o.liquid_fluidity_smooth, o.liquid_sink, + o.acceleration_default, o.acceleration_air, o.speed_fast, o.acceleration_fast, + o.speed_walk + ); +} + +bool PlayerPhysicsOverride::operator==(const PlayerPhysicsOverride &other) const +{ + return tie(*this) == tie(other); +} diff --git a/src/player.h b/src/player.h index dd2be4986..c729f98a0 100644 --- a/src/player.h +++ b/src/player.h @@ -22,13 +22,10 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "irrlichttypes_bloated.h" #include "inventory.h" #include "constants.h" -#include "network/networkprotocol.h" #include "util/basic_macros.h" #include "util/string.h" -#include #include #include -#include #include #define PLAYERNAME_SIZE 20 @@ -89,6 +86,11 @@ struct PlayerControl movement_direction = a_movement_direction; } + // Sets movement_speed and movement_direction according to direction_keys + // if direction_keys != 0, otherwise leaves them unchanged to preserve + // joystick input. + void setMovementFromKeys(); + #ifndef SERVER // For client use u32 getKeysPressed() const; @@ -97,6 +99,7 @@ struct PlayerControl // For server use void unpackKeysPressed(u32 keypress_bits); + v2f getMovement() const; u8 direction_keys = 0; bool jump = false; @@ -105,7 +108,7 @@ struct PlayerControl bool zoom = false; bool dig = false; bool place = false; - // Note: These four are NOT available on the server + // Note: These two are NOT available on the server float pitch = 0.0f; float yaw = 0.0f; float movement_speed = 0.0f; @@ -134,23 +137,10 @@ struct PlayerPhysicsOverride float acceleration_fast = 1.f; float speed_walk = 1.f; -private: - auto tie() const { - // Make sure to add new members to this list! - return std::tie( - speed, jump, gravity, sneak, sneak_glitch, new_move, speed_climb, speed_crouch, - liquid_fluidity, liquid_fluidity_smooth, liquid_sink, acceleration_default, - acceleration_air, speed_fast, acceleration_fast, speed_walk - ); - } - -public: - bool operator==(const PlayerPhysicsOverride &other) const { - return tie() == other.tie(); - }; + bool operator==(const PlayerPhysicsOverride &other) const; bool operator!=(const PlayerPhysicsOverride &other) const { - return tie() != other.tie(); - }; + return !(*this == other); + } }; class Map; @@ -213,7 +203,7 @@ public: f32 movement_liquid_sink; f32 movement_gravity; - v2s32 local_animations[4]; + v2f local_animations[4]; float local_animation_speed; std::string inventory_formspec; diff --git a/src/porting.cpp b/src/porting.cpp index da972926a..f6409c56c 100644 --- a/src/porting.cpp +++ b/src/porting.cpp @@ -73,6 +73,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "filesys.h" #include "log.h" #include "util/string.h" +#include "util/tracy_wrapper.h" #include #include #include @@ -960,6 +961,8 @@ void TrackFreedMemory(size_t amount) void TriggerMemoryTrim() { + ZoneScoped; + constexpr auto MO = std::memory_order_relaxed; if (memory_freed.load(MO) >= MEMORY_TRIM_THRESHOLD) { // Synchronize call diff --git a/src/remoteplayer.h b/src/remoteplayer.h index e0c7ab744..cbfc80d91 100644 --- a/src/remoteplayer.h +++ b/src/remoteplayer.h @@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "player.h" #include "skyparams.h" #include "lighting.h" +#include "network/networkprotocol.h" // session_t class PlayerSAO; @@ -112,14 +113,14 @@ public: inline void setModified(const bool x) { m_dirty = x; } - void setLocalAnimations(v2s32 frames[4], float frame_speed) + void setLocalAnimations(v2f frames[4], float frame_speed) { for (int i = 0; i < 4; i++) local_animations[i] = frames[i]; local_animation_speed = frame_speed; } - void getLocalAnimations(v2s32 *frames, float *frame_speed) + void getLocalAnimations(v2f *frames, float *frame_speed) { for (int i = 0; i < 4; i++) frames[i] = local_animations[i]; diff --git a/src/script/common/c_content.cpp b/src/script/common/c_content.cpp index 6f7d86447..348c2559a 100644 --- a/src/script/common/c_content.cpp +++ b/src/script/common/c_content.cpp @@ -291,7 +291,7 @@ const std::array object_property_keys = { "use_texture_alpha", "shaded", "damage_texture_modifier", - "show_on_minimap" + "show_on_minimap", }; /******************************************************************************/ @@ -2063,6 +2063,10 @@ bool read_tree_def(lua_State *L, int idx, const NodeDefManager *ndef, } /******************************************************************************/ +#if defined(JSONCPP_STRING) || !(JSONCPP_VERSION_MAJOR < 1 || JSONCPP_VERSION_MINOR < 9) +#define HAVE_JSON_STRING +#endif + // Returns depth of json value tree static int push_json_value_getdepth(const Json::Value &value) { @@ -2070,11 +2074,8 @@ static int push_json_value_getdepth(const Json::Value &value) return 1; int maxdepth = 0; - for (const auto &it : value) { - int elemdepth = push_json_value_getdepth(it); - if (elemdepth > maxdepth) - maxdepth = elemdepth; - } + for (const auto &it : value) + maxdepth = std::max(push_json_value_getdepth(it), maxdepth); return maxdepth + 1; } // Recursive function to convert JSON --> Lua table @@ -2087,41 +2088,40 @@ static bool push_json_value_helper(lua_State *L, const Json::Value &value, lua_pushvalue(L, nullindex); break; case Json::intValue: - lua_pushinteger(L, value.asLargestInt()); - break; case Json::uintValue: - lua_pushinteger(L, value.asLargestUInt()); - break; case Json::realValue: + // push everything as a double since Lua integers may be too small lua_pushnumber(L, value.asDouble()); break; - case Json::stringValue: - { - const char *str = value.asCString(); - lua_pushstring(L, str ? str : ""); - } + case Json::stringValue: { +#ifdef HAVE_JSON_STRING + const auto &str = value.asString(); + lua_pushlstring(L, str.c_str(), str.size()); +#else + const char *str = value.asCString(); + lua_pushstring(L, str ? str : ""); +#endif break; + } case Json::booleanValue: lua_pushboolean(L, value.asInt()); break; case Json::arrayValue: lua_createtable(L, value.size(), 0); - for (Json::Value::const_iterator it = value.begin(); - it != value.end(); ++it) { + for (auto it = value.begin(); it != value.end(); ++it) { push_json_value_helper(L, *it, nullindex); lua_rawseti(L, -2, it.index() + 1); } break; case Json::objectValue: lua_createtable(L, 0, value.size()); - for (Json::Value::const_iterator it = value.begin(); - it != value.end(); ++it) { -#if !defined(JSONCPP_STRING) && (JSONCPP_VERSION_MAJOR < 1 || JSONCPP_VERSION_MINOR < 9) + for (auto it = value.begin(); it != value.end(); ++it) { +#ifdef HAVE_JSON_STRING + const auto &str = it.name(); + lua_pushlstring(L, str.c_str(), str.size()); +#else const char *str = it.memberName(); lua_pushstring(L, str ? str : ""); -#else - std::string str = it.name(); - lua_pushstring(L, str.c_str()); #endif push_json_value_helper(L, *it, nullindex); lua_rawset(L, -3); @@ -2130,6 +2130,7 @@ static bool push_json_value_helper(lua_State *L, const Json::Value &value, } return true; } + // converts JSON --> Lua table; returns false if lua stack limit exceeded // nullindex: Lua stack index of value to use in place of JSON null bool push_json_value(lua_State *L, const Json::Value &value, int nullindex) @@ -2236,12 +2237,14 @@ void push_pointed_thing(lua_State *L, const PointedThing &pointed, bool csm, void push_objectRef(lua_State *L, const u16 id) { + assert(id != 0); // Get core.object_refs[i] lua_getglobal(L, "core"); lua_getfield(L, -1, "object_refs"); luaL_checktype(L, -1, LUA_TTABLE); lua_pushinteger(L, id); lua_gettable(L, -2); + assert(!lua_isnoneornil(L, -1)); lua_remove(L, -2); // object_refs lua_remove(L, -2); // core } @@ -2306,7 +2309,10 @@ void read_hud_element(lua_State *L, HudElement *elem) elem->dir = getintfield_default(L, 2, "dir", 0); lua_getfield(L, 2, "alignment"); - elem->align = lua_istable(L, -1) ? read_v2f(L, -1) : v2f(); + if (lua_istable(L, -1)) + elem->align = read_v2f(L, -1); + else + elem->align = elem->type == HUD_ELEM_INVENTORY ? v2f(1.0f, 1.0f) : v2f(); lua_pop(L, 1); lua_getfield(L, 2, "offset"); diff --git a/src/script/common/c_internal.cpp b/src/script/common/c_internal.cpp index ae83b8df0..e6bfafdcd 100644 --- a/src/script/common/c_internal.cpp +++ b/src/script/common/c_internal.cpp @@ -122,27 +122,30 @@ void script_error(lua_State *L, int pcall_result, const char *mod, const char *f throw LuaError(err_msg); } -static void script_log_add_source(lua_State *L, std::string &message, int stack_depth) +[[nodiscard]] static std::string script_log_add_source(lua_State *L, + std::string_view message, int stack_depth) { + std::string ret(message); if (stack_depth <= 0) - return; + return ret; lua_Debug ar; if (lua_getstack(L, stack_depth, &ar)) { FATAL_ERROR_IF(!lua_getinfo(L, "Sl", &ar), "lua_getinfo() failed"); - message.append(" (at " + std::string(ar.short_src) + ":" + ret.append(" (at ").append(ar.short_src).append(":" + std::to_string(ar.currentline) + ")"); } else { - message.append(" (at ?:?)"); + ret.append(" (at ?:?)"); } + return ret; } -bool script_log_unique(lua_State *L, std::string message, std::ostream &log_to, +bool script_log_unique(lua_State *L, std::string_view message_in, std::ostream &log_to, int stack_depth) { thread_local std::vector logged_messages; - script_log_add_source(L, message, stack_depth); + auto message = script_log_add_source(L, message_in, stack_depth); u64 hash = murmur_hash_64_ua(message.data(), message.length(), 0xBADBABE); if (std::find(logged_messages.begin(), logged_messages.end(), hash) @@ -174,7 +177,7 @@ DeprecatedHandlingMode get_deprecated_handling_mode() return ret; } -void log_deprecated(lua_State *L, std::string message, int stack_depth, bool once) +void log_deprecated(lua_State *L, std::string_view message, int stack_depth, bool once) { DeprecatedHandlingMode mode = get_deprecated_handling_mode(); if (mode == DeprecatedHandlingMode::Ignore) @@ -184,12 +187,12 @@ void log_deprecated(lua_State *L, std::string message, int stack_depth, bool onc if (once) { log = script_log_unique(L, message, warningstream, stack_depth); } else { - script_log_add_source(L, message, stack_depth); - warningstream << message << std::endl; + auto message2 = script_log_add_source(L, message, stack_depth); + warningstream << message2 << std::endl; } if (mode == DeprecatedHandlingMode::Error) - throw LuaError(message); + throw LuaError(std::string(message)); else if (log) infostream << script_get_backtrace(L) << std::endl; } diff --git a/src/script/common/c_internal.h b/src/script/common/c_internal.h index a67b0dad9..a9f9fe226 100644 --- a/src/script/common/c_internal.h +++ b/src/script/common/c_internal.h @@ -26,6 +26,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once +#include + extern "C" { #include #include @@ -127,7 +129,7 @@ int script_error_handler(lua_State *L); // Takes an error from lua_pcall and throws it as a LuaError void script_error(lua_State *L, int pcall_result, const char *mod, const char *fxn); -bool script_log_unique(lua_State *L, std::string message, std::ostream &log_to, +bool script_log_unique(lua_State *L, std::string_view message, std::ostream &log_to, int stack_depth = 1); enum DeprecatedHandlingMode { @@ -152,7 +154,8 @@ DeprecatedHandlingMode get_deprecated_handling_mode(); * (ie: not builtin or core). -1 to disabled. * @param once Log the deprecation warning only once per callsite. */ -void log_deprecated(lua_State *L, std::string message, int stack_depth = 1, bool once = false); +void log_deprecated(lua_State *L, std::string_view message, + int stack_depth = 1, bool once = false); // Safely call string.dump on a function value // (does not pop, leaves one value on stack) diff --git a/src/script/common/c_packer.cpp b/src/script/common/c_packer.cpp index 579167952..bbef89c1f 100644 --- a/src/script/common/c_packer.cpp +++ b/src/script/common/c_packer.cpp @@ -507,6 +507,7 @@ PackedValue *script_pack(lua_State *L, int idx) void script_unpack(lua_State *L, PackedValue *pv) { + assert(pv); // table that tracks objects for keep_ref / PUSHREF (key = instr index) lua_newtable(L); const int top = lua_gettop(L); diff --git a/src/script/cpp_api/s_async.cpp b/src/script/cpp_api/s_async.cpp index 75b1a8205..bfcfb4f7d 100644 --- a/src/script/cpp_api/s_async.cpp +++ b/src/script/cpp_api/s_async.cpp @@ -50,11 +50,12 @@ AsyncEngine::~AsyncEngine() } // Wait for threads to finish + infostream << "AsyncEngine: Waiting for " << workerThreads.size() + << " threads" << std::endl; for (AsyncWorkerThread *workerThread : workerThreads) { workerThread->wait(); } - // Force kill all threads for (AsyncWorkerThread *workerThread : workerThreads) { delete workerThread; } diff --git a/src/script/cpp_api/s_base.cpp b/src/script/cpp_api/s_base.cpp index e9907f304..cd74b7cfd 100644 --- a/src/script/cpp_api/s_base.cpp +++ b/src/script/cpp_api/s_base.cpp @@ -32,6 +32,9 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "client/client.h" #endif +#if BUILD_WITH_TRACY + #include "tracy/TracyLua.hpp" +#endif extern "C" { #include "lualib.h" @@ -95,6 +98,11 @@ ScriptApiBase::ScriptApiBase(ScriptingType type): lua_pushstring(m_luastack, LUA_BITLIBNAME); lua_call(m_luastack, 1, 0); +#if BUILD_WITH_TRACY + // Load tracy lua bindings + tracy::LuaRegister(m_luastack); +#endif + // Make the ScriptApiBase* accessible to ModApiBase #if INDIRECT_SCRIPTAPI_RIDX *(void **)(lua_newuserdata(m_luastack, sizeof(void *))) = this; @@ -466,7 +474,7 @@ void ScriptApiBase::addObjectReference(ServerActiveObject *cobj) int objectstable = lua_gettop(L); // object_refs[id] = object - lua_pushnumber(L, cobj->getId()); // Push id + lua_pushinteger(L, cobj->getId()); // Push id lua_pushvalue(L, object); // Copy object to top of stack lua_settable(L, objectstable); } @@ -483,24 +491,29 @@ void ScriptApiBase::removeObjectReference(ServerActiveObject *cobj) int objectstable = lua_gettop(L); // Get object_refs[id] - lua_pushnumber(L, cobj->getId()); // Push id + lua_pushinteger(L, cobj->getId()); // Push id lua_gettable(L, objectstable); // Set object reference to NULL - ObjectRef::set_null(L); + ObjectRef::set_null(L, cobj); lua_pop(L, 1); // pop object // Set object_refs[id] = nil - lua_pushnumber(L, cobj->getId()); // Push id + lua_pushinteger(L, cobj->getId()); // Push id lua_pushnil(L); lua_settable(L, objectstable); } -// Creates a new anonymous reference if cobj=NULL or id=0 -void ScriptApiBase::objectrefGetOrCreate(lua_State *L, - ServerActiveObject *cobj) +void ScriptApiBase::objectrefGetOrCreate(lua_State *L, ServerActiveObject *cobj) { assert(getType() == ScriptingType::Server); - if (cobj == NULL || cobj->getId() == 0) { + if (!cobj) { + ObjectRef::create(L, nullptr); // dummy reference + } else if (cobj->getId() == 0) { + // TODO after 5.10.0: convert this to a FATAL_ERROR + errorstream << "ScriptApiBase::objectrefGetOrCreate(): " + << "Pushing orphan ObjectRef. Please open a bug report for this." + << std::endl; + assert(0); ObjectRef::create(L, cobj); } else { push_objectRef(L, cobj->getId()); diff --git a/src/script/cpp_api/s_client.cpp b/src/script/cpp_api/s_client.cpp index 6faa0695c..78d0ec44d 100644 --- a/src/script/cpp_api/s_client.cpp +++ b/src/script/cpp_api/s_client.cpp @@ -125,21 +125,6 @@ void ScriptApiClient::on_hp_modification(int32_t newhp) } } -void ScriptApiClient::on_death() -{ - SCRIPTAPI_PRECHECKHEADER - - // Get registered shutdown hooks - lua_getglobal(L, "core"); - lua_getfield(L, -1, "registered_on_death"); - // Call callbacks - try { - runCallbacks(0, RUN_CALLBACKS_MODE_FIRST); - } catch (LuaError &e) { - getClient()->setFatalError(e); - } -} - void ScriptApiClient::environment_step(float dtime) { SCRIPTAPI_PRECHECKHEADER diff --git a/src/script/cpp_api/s_client.h b/src/script/cpp_api/s_client.h index 8a5523d0d..74cc0d898 100644 --- a/src/script/cpp_api/s_client.h +++ b/src/script/cpp_api/s_client.h @@ -49,7 +49,6 @@ public: void on_damage_taken(int32_t damage_amount); void on_hp_modification(int32_t newhp); - void on_death(); void environment_step(float dtime); void on_formspec_input(const std::string &formname, const StringMap &fields); diff --git a/src/script/cpp_api/s_env.cpp b/src/script/cpp_api/s_env.cpp index 3cbb13cd2..007622d52 100644 --- a/src/script/cpp_api/s_env.cpp +++ b/src/script/cpp_api/s_env.cpp @@ -25,8 +25,110 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "mapgen/mapgen.h" #include "lua_api/l_env.h" #include "server.h" +#include "scripting_server.h" #include "script/common/c_content.h" +/* + LuaABM & LuaLBM +*/ + +class LuaABM : public ActiveBlockModifier { +private: + const int m_id; + + std::vector m_trigger_contents; + std::vector m_required_neighbors; + std::vector m_without_neighbors; + float m_trigger_interval; + u32 m_trigger_chance; + bool m_simple_catch_up; + s16 m_min_y; + s16 m_max_y; +public: + LuaABM(int id, + const std::vector &trigger_contents, + const std::vector &required_neighbors, + const std::vector &without_neighbors, + float trigger_interval, u32 trigger_chance, bool simple_catch_up, + s16 min_y, s16 max_y): + m_id(id), + m_trigger_contents(trigger_contents), + m_required_neighbors(required_neighbors), + m_without_neighbors(without_neighbors), + m_trigger_interval(trigger_interval), + m_trigger_chance(trigger_chance), + m_simple_catch_up(simple_catch_up), + m_min_y(min_y), + m_max_y(max_y) + { + } + virtual const std::vector &getTriggerContents() const + { + return m_trigger_contents; + } + virtual const std::vector &getRequiredNeighbors() const + { + return m_required_neighbors; + } + virtual const std::vector &getWithoutNeighbors() const + { + return m_without_neighbors; + } + virtual float getTriggerInterval() + { + return m_trigger_interval; + } + virtual u32 getTriggerChance() + { + return m_trigger_chance; + } + virtual bool getSimpleCatchUp() + { + return m_simple_catch_up; + } + virtual s16 getMinY() + { + return m_min_y; + } + virtual s16 getMaxY() + { + return m_max_y; + } + + virtual void trigger(ServerEnvironment *env, v3s16 p, MapNode n, + u32 active_object_count, u32 active_object_count_wider) + { + auto *script = env->getScriptIface(); + script->triggerABM(m_id, p, n, active_object_count, active_object_count_wider); + } +}; + +class LuaLBM : public LoadingBlockModifierDef +{ +private: + int m_id; +public: + LuaLBM(int id, + const std::vector &trigger_contents, + const std::string &name, bool run_at_every_load): + m_id(id) + { + this->run_at_every_load = run_at_every_load; + this->trigger_contents = trigger_contents; + this->name = name; + } + + virtual void trigger(ServerEnvironment *env, MapBlock *block, + const std::unordered_set &positions, float dtime_s) + { + auto *script = env->getScriptIface(); + script->triggerLBM(m_id, block, positions, dtime_s); + } +}; + +/* + ScriptApiEnv +*/ void ScriptApiEnv::environment_OnGenerated(v3s16 minp, v3s16 maxp, u32 blockseed) @@ -46,7 +148,6 @@ void ScriptApiEnv::environment_OnGenerated(v3s16 minp, v3s16 maxp, void ScriptApiEnv::environment_Step(float dtime) { SCRIPTAPI_PRECHECKHEADER - //infostream << "scriptapi_environment_step" << std::endl; // Get core.registered_globalsteps lua_getglobal(L, "core"); @@ -76,12 +177,40 @@ void ScriptApiEnv::player_event(ServerActiveObject *player, const std::string &t void ScriptApiEnv::initializeEnvironment(ServerEnvironment *env) { SCRIPTAPI_PRECHECKHEADER + + assert(env); verbosestream << "ScriptApiEnv: Environment initialized" << std::endl; setEnv(env); - /* - Add {Loading,Active}BlockModifiers to environment - */ + readABMs(); + readLBMs(); +} + +// Reads a single or a list of node names into a vector +bool ScriptApiEnv::read_nodenames(lua_State *L, int idx, std::vector &to) +{ + if (lua_istable(L, idx)) { + const int table = idx < 0 ? (lua_gettop(L) + idx + 1) : idx; + lua_pushnil(L); + while (lua_next(L, table)) { + // key at index -2 and value at index -1 + luaL_checktype(L, -1, LUA_TSTRING); + to.emplace_back(readParam(L, -1)); + // removes value, keeps key for next iteration + lua_pop(L, 1); + } + } else if (lua_isstring(L, idx)) { + to.emplace_back(readParam(L, idx)); + } else { + return false; + } + return true; +} + +void ScriptApiEnv::readABMs() +{ + SCRIPTAPI_PRECHECKHEADER + auto *env = reinterpret_cast(getEnv()); // Get core.registered_abms lua_getglobal(L, "core"); @@ -100,36 +229,17 @@ void ScriptApiEnv::initializeEnvironment(ServerEnvironment *env) std::vector trigger_contents; lua_getfield(L, current_abm, "nodenames"); - if (lua_istable(L, -1)) { - int table = lua_gettop(L); - lua_pushnil(L); - while (lua_next(L, table)) { - // key at index -2 and value at index -1 - luaL_checktype(L, -1, LUA_TSTRING); - trigger_contents.emplace_back(readParam(L, -1)); - // removes value, keeps key for next iteration - lua_pop(L, 1); - } - } else if (lua_isstring(L, -1)) { - trigger_contents.emplace_back(readParam(L, -1)); - } + read_nodenames(L, -1, trigger_contents); lua_pop(L, 1); std::vector required_neighbors; lua_getfield(L, current_abm, "neighbors"); - if (lua_istable(L, -1)) { - int table = lua_gettop(L); - lua_pushnil(L); - while (lua_next(L, table)) { - // key at index -2 and value at index -1 - luaL_checktype(L, -1, LUA_TSTRING); - required_neighbors.emplace_back(readParam(L, -1)); - // removes value, keeps key for next iteration - lua_pop(L, 1); - } - } else if (lua_isstring(L, -1)) { - required_neighbors.emplace_back(readParam(L, -1)); - } + read_nodenames(L, -1, required_neighbors); + lua_pop(L, 1); + + std::vector without_neighbors; + lua_getfield(L, current_abm, "without_neighbors"); + read_nodenames(L, -1, without_neighbors); lua_pop(L, 1); float trigger_interval = 10.0; @@ -151,8 +261,9 @@ void ScriptApiEnv::initializeEnvironment(ServerEnvironment *env) luaL_checktype(L, current_abm + 1, LUA_TFUNCTION); lua_pop(L, 1); - LuaABM *abm = new LuaABM(L, id, trigger_contents, required_neighbors, - trigger_interval, trigger_chance, simple_catch_up, min_y, max_y); + LuaABM *abm = new LuaABM(id, trigger_contents, required_neighbors, + without_neighbors, trigger_interval, trigger_chance, + simple_catch_up, min_y, max_y); env->addActiveBlockModifier(abm); @@ -160,6 +271,12 @@ void ScriptApiEnv::initializeEnvironment(ServerEnvironment *env) lua_pop(L, 1); } lua_pop(L, 1); +} + +void ScriptApiEnv::readLBMs() +{ + SCRIPTAPI_PRECHECKHEADER + auto *env = reinterpret_cast(getEnv()); // Get core.registered_lbms lua_getglobal(L, "core"); @@ -177,21 +294,9 @@ void ScriptApiEnv::initializeEnvironment(ServerEnvironment *env) int id = lua_tonumber(L, -2); int current_lbm = lua_gettop(L); - std::set trigger_contents; + std::vector trigger_contents; lua_getfield(L, current_lbm, "nodenames"); - if (lua_istable(L, -1)) { - int table = lua_gettop(L); - lua_pushnil(L); - while (lua_next(L, table)) { - // key at index -2 and value at index -1 - luaL_checktype(L, -1, LUA_TSTRING); - trigger_contents.insert(readParam(L, -1)); - // removes value, keeps key for next iteration - lua_pop(L, 1); - } - } else if (lua_isstring(L, -1)) { - trigger_contents.insert(readParam(L, -1)); - } + read_nodenames(L, -1, trigger_contents); lua_pop(L, 1); std::string name; @@ -200,11 +305,7 @@ void ScriptApiEnv::initializeEnvironment(ServerEnvironment *env) bool run_at_every_load = getboolfield_default(L, current_lbm, "run_at_every_load", false); - lua_getfield(L, current_lbm, "action"); - luaL_checktype(L, current_lbm + 1, LUA_TFUNCTION); - lua_pop(L, 1); - - LuaLBM *lbm = new LuaLBM(L, id, trigger_contents, name, + LuaLBM *lbm = new LuaLBM(id, trigger_contents, name, run_at_every_load); env->addLoadingBlockModifierDef(lbm); @@ -288,7 +389,7 @@ void ScriptApiEnv::on_liquid_transformed( int index = 1; lua_createtable(L, list.size(), 0); lua_createtable(L, list.size(), 0); - for(std::pair p : list) { + for(auto &p : list) { lua_pushnumber(L, index); push_v3s16(L, p.first); lua_rawset(L, -4); @@ -332,3 +433,73 @@ bool ScriptApiEnv::has_on_mapblocks_changed() luaL_checktype(L, -1, LUA_TTABLE); return lua_objlen(L, -1) > 0; } + +void ScriptApiEnv::triggerABM(int id, v3s16 p, MapNode n, + u32 active_object_count, u32 active_object_count_wider) +{ + SCRIPTAPI_PRECHECKHEADER + + int error_handler = PUSH_ERROR_HANDLER(L); + + // Get registered_abms + lua_getglobal(L, "core"); + lua_getfield(L, -1, "registered_abms"); + luaL_checktype(L, -1, LUA_TTABLE); + lua_remove(L, -2); // Remove core + + // Get registered_abms[m_id] + lua_pushinteger(L, id); + lua_gettable(L, -2); + FATAL_ERROR_IF(lua_isnil(L, -1), "Entry with given id not found in registered_abms table"); + lua_remove(L, -2); // Remove registered_abms + + setOriginFromTable(-1); + + // Call action + luaL_checktype(L, -1, LUA_TTABLE); + lua_getfield(L, -1, "action"); + luaL_checktype(L, -1, LUA_TFUNCTION); + lua_remove(L, -2); // Remove registered_abms[m_id] + push_v3s16(L, p); + pushnode(L, n); + lua_pushnumber(L, active_object_count); + lua_pushnumber(L, active_object_count_wider); + + int result = lua_pcall(L, 4, 0, error_handler); + if (result) + scriptError(result, "LuaABM::trigger"); + + lua_pop(L, 1); // Pop error handler +} + +void ScriptApiEnv::triggerLBM(int id, MapBlock *block, + const std::unordered_set &positions, float dtime_s) +{ + SCRIPTAPI_PRECHECKHEADER + + int error_handler = PUSH_ERROR_HANDLER(L); + + const v3s16 pos_of_block = block->getPosRelative(); + + // Get core.run_lbm + lua_getglobal(L, "core"); + lua_getfield(L, -1, "run_lbm"); + luaL_checktype(L, -1, LUA_TFUNCTION); + lua_remove(L, -2); // Remove core + + // Call it + lua_pushinteger(L, id); + lua_createtable(L, positions.size(), 0); + int i = 1; + for (auto &p : positions) { + push_v3s16(L, pos_of_block + p); + lua_rawseti(L, -2, i++); + } + lua_pushnumber(L, dtime_s); + + int result = lua_pcall(L, 3, 0, error_handler); + if (result) + scriptError(result, "LuaLBM::trigger"); + + lua_pop(L, 1); // Pop error handler +} diff --git a/src/script/cpp_api/s_env.h b/src/script/cpp_api/s_env.h index bc4c4cd4d..4722cb522 100644 --- a/src/script/cpp_api/s_env.h +++ b/src/script/cpp_api/s_env.h @@ -26,6 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include class ServerEnvironment; +class MapBlock; struct ScriptCallbackState; class ScriptApiEnv : virtual public ScriptApiBase @@ -55,5 +56,20 @@ public: // Determines whether there are any on_mapblocks_changed callbacks bool has_on_mapblocks_changed(); + // Initializes environment and loads some definitions from Lua void initializeEnvironment(ServerEnvironment *env); + + void triggerABM(int id, v3s16 p, MapNode n, + u32 active_object_count, u32 active_object_count_wider); + + void triggerLBM(int id, MapBlock *block, + const std::unordered_set &positions, float dtime_s); + +private: + void readABMs(); + + void readLBMs(); + + // Reads a single or a list of node names into a vector + static bool read_nodenames(lua_State *L, int idx, std::vector &to); }; diff --git a/src/script/cpp_api/s_security.cpp b/src/script/cpp_api/s_security.cpp index 7c8ba8931..9a4b0763b 100644 --- a/src/script/cpp_api/s_security.cpp +++ b/src/script/cpp_api/s_security.cpp @@ -109,7 +109,12 @@ void ScriptApiSecurity::initializeSecurity() "string", "table", "math", - "bit" + "bit", + // Not sure if completely safe. But if someone enables tracy, they'll + // know what they do. +#if BUILD_WITH_TRACY + "tracy", +#endif }; static const char *io_whitelist[] = { "close", @@ -303,6 +308,11 @@ void ScriptApiSecurity::initializeSecurityClient() "table", "math", "bit", + // Not sure if completely safe. But if someone enables tracy, they'll + // know what they do. +#if BUILD_WITH_TRACY + "tracy", +#endif }; static const char *os_whitelist[] = { "clock", diff --git a/src/script/lua_api/CMakeLists.txt b/src/script/lua_api/CMakeLists.txt index d9405e4fe..2e12f8c56 100644 --- a/src/script/lua_api/CMakeLists.txt +++ b/src/script/lua_api/CMakeLists.txt @@ -6,6 +6,7 @@ set(common_SCRIPT_LUA_API_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/l_env.cpp ${CMAKE_CURRENT_SOURCE_DIR}/l_http.cpp ${CMAKE_CURRENT_SOURCE_DIR}/l_inventory.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/l_ipc.cpp ${CMAKE_CURRENT_SOURCE_DIR}/l_item.cpp ${CMAKE_CURRENT_SOURCE_DIR}/l_itemstackmeta.cpp ${CMAKE_CURRENT_SOURCE_DIR}/l_mapgen.cpp diff --git a/src/script/lua_api/l_client.cpp b/src/script/lua_api/l_client.cpp index da19ed0ea..3bd9fc04d 100644 --- a/src/script/lua_api/l_client.cpp +++ b/src/script/lua_api/l_client.cpp @@ -157,13 +157,6 @@ int ModApiClient::l_show_formspec(lua_State *L) return 1; } -// send_respawn() -int ModApiClient::l_send_respawn(lua_State *L) -{ - getClient(L)->sendRespawn(); - return 0; -} - // disconnect() int ModApiClient::l_disconnect(lua_State *L) { @@ -348,7 +341,6 @@ void ModApiClient::Initialize(lua_State *L, int top) API_FCT(clear_out_chat_queue); API_FCT(get_player_names); API_FCT(show_formspec); - API_FCT(send_respawn); API_FCT(gettext); API_FCT(get_node_or_nil); API_FCT(disconnect); diff --git a/src/script/lua_api/l_client.h b/src/script/lua_api/l_client.h index e960dc4cf..d726bc8a3 100644 --- a/src/script/lua_api/l_client.h +++ b/src/script/lua_api/l_client.h @@ -51,9 +51,6 @@ private: // show_formspec(name, formspec) static int l_show_formspec(lua_State *L); - // send_respawn() - static int l_send_respawn(lua_State *L); - // disconnect() static int l_disconnect(lua_State *L); diff --git a/src/script/lua_api/l_env.cpp b/src/script/lua_api/l_env.cpp index f5ed2804c..726300b07 100644 --- a/src/script/lua_api/l_env.cpp +++ b/src/script/lua_api/l_env.cpp @@ -65,93 +65,6 @@ const EnumString ModApiEnvBase::es_BlockStatusType[] = /////////////////////////////////////////////////////////////////////////////// - -void LuaABM::trigger(ServerEnvironment *env, v3s16 p, MapNode n, - u32 active_object_count, u32 active_object_count_wider) -{ - ServerScripting *scriptIface = env->getScriptIface(); - scriptIface->realityCheck(); - - lua_State *L = scriptIface->getStack(); - sanity_check(lua_checkstack(L, 20)); - StackUnroller stack_unroller(L); - - int error_handler = PUSH_ERROR_HANDLER(L); - - // Get registered_abms - lua_getglobal(L, "core"); - lua_getfield(L, -1, "registered_abms"); - luaL_checktype(L, -1, LUA_TTABLE); - lua_remove(L, -2); // Remove core - - // Get registered_abms[m_id] - lua_pushinteger(L, m_id); - lua_gettable(L, -2); - if(lua_isnil(L, -1)) - FATAL_ERROR(""); - lua_remove(L, -2); // Remove registered_abms - - scriptIface->setOriginFromTable(-1); - - // Call action - luaL_checktype(L, -1, LUA_TTABLE); - lua_getfield(L, -1, "action"); - luaL_checktype(L, -1, LUA_TFUNCTION); - lua_remove(L, -2); // Remove registered_abms[m_id] - push_v3s16(L, p); - pushnode(L, n); - lua_pushnumber(L, active_object_count); - lua_pushnumber(L, active_object_count_wider); - - int result = lua_pcall(L, 4, 0, error_handler); - if (result) - scriptIface->scriptError(result, "LuaABM::trigger"); - - lua_pop(L, 1); // Pop error handler -} - -void LuaLBM::trigger(ServerEnvironment *env, v3s16 p, - const MapNode n, const float dtime_s) -{ - ServerScripting *scriptIface = env->getScriptIface(); - scriptIface->realityCheck(); - - lua_State *L = scriptIface->getStack(); - sanity_check(lua_checkstack(L, 20)); - StackUnroller stack_unroller(L); - - int error_handler = PUSH_ERROR_HANDLER(L); - - // Get registered_lbms - lua_getglobal(L, "core"); - lua_getfield(L, -1, "registered_lbms"); - luaL_checktype(L, -1, LUA_TTABLE); - lua_remove(L, -2); // Remove core - - // Get registered_lbms[m_id] - lua_pushinteger(L, m_id); - lua_gettable(L, -2); - FATAL_ERROR_IF(lua_isnil(L, -1), "Entry with given id not found in registered_lbms table"); - lua_remove(L, -2); // Remove registered_lbms - - scriptIface->setOriginFromTable(-1); - - // Call action - luaL_checktype(L, -1, LUA_TTABLE); - lua_getfield(L, -1, "action"); - luaL_checktype(L, -1, LUA_TFUNCTION); - lua_remove(L, -2); // Remove registered_lbms[m_id] - push_v3s16(L, p); - pushnode(L, n); - lua_pushnumber(L, dtime_s); - - int result = lua_pcall(L, 3, 0, error_handler); - if (result) - scriptIface->scriptError(result, "LuaLBM::trigger"); - - lua_pop(L, 1); // Pop error handler -} - int LuaRaycast::l_next(lua_State *L) { GET_PLAIN_ENV_PTR; @@ -247,7 +160,7 @@ void LuaEmergeAreaCallback(v3s16 blockpos, EmergeAction action, void *param) // state must be protected by envlock Server *server = state->script->getServer(); - MutexAutoLock envlock(server->m_env_mutex); + Server::EnvAutoLock envlock(server); state->refcount--; @@ -340,6 +253,31 @@ int ModApiEnv::l_swap_node(lua_State *L) return 1; } +// bulk_swap_node([pos1, pos2, ...], node) +// pos = {x=num, y=num, z=num} +int ModApiEnv::l_bulk_swap_node(lua_State *L) +{ + GET_ENV_PTR; + + luaL_checktype(L, 1, LUA_TTABLE); + + s32 len = lua_objlen(L, 1); + + MapNode n = readnode(L, 2); + + // Do it + bool succeeded = true; + for (s32 i = 1; i <= len; i++) { + lua_rawgeti(L, 1, i); + if (!env->swapNode(read_v3s16(L, -1), n)) + succeeded = false; + lua_pop(L, 1); + } + + lua_pushboolean(L, succeeded); + return 1; +} + // get_node_raw(x, y, z) -> content, param1, param2, pos_ok int ModApiEnv::l_get_node_raw(lua_State *L) { @@ -836,10 +774,7 @@ int ModApiEnv::l_get_timeofday(lua_State *L) { GET_PLAIN_ENV_PTR; - // Do it - int timeofday_mh = env->getTimeOfDay(); - float timeofday_f = (float)timeofday_mh / 24000.0f; - lua_pushnumber(L, timeofday_f); + lua_pushnumber(L, env->getTimeOfDayF()); return 1; } @@ -857,8 +792,7 @@ int ModApiEnv::l_get_gametime(lua_State *L) { GET_ENV_PTR; - int game_time = env->getGameTime(); - lua_pushnumber(L, game_time); + lua_pushnumber(L, env->getGameTime()); return 1; } @@ -1468,6 +1402,7 @@ void ModApiEnv::Initialize(lua_State *L, int top) API_FCT(bulk_set_node); API_FCT(add_node); API_FCT(swap_node); + API_FCT(bulk_swap_node); API_FCT(add_item); API_FCT(remove_node); API_FCT(get_node_raw); diff --git a/src/script/lua_api/l_env.h b/src/script/lua_api/l_env.h index 2ed0eb114..ba0f2eb61 100644 --- a/src/script/lua_api/l_env.h +++ b/src/script/lua_api/l_env.h @@ -20,9 +20,10 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once #include "lua_api/l_base.h" -#include "serverenvironment.h" #include "raycast.h" +class ServerScripting; + // base class containing helpers class ModApiEnvBase : public ModApiBase { protected: @@ -64,6 +65,10 @@ private: // pos = {x=num, y=num, z=num} static int l_bulk_set_node(lua_State *L); + // bulk_swap_node([pos1, pos2, ...], node) + // pos = {x=num, y=num, z=num} + static int l_bulk_swap_node(lua_State *L); + static int l_add_node(lua_State *L); // remove_node(pos) @@ -281,82 +286,6 @@ public: static void InitializeEmerge(lua_State *L, int top); }; -class LuaABM : public ActiveBlockModifier { -private: - int m_id; - - std::vector m_trigger_contents; - std::vector m_required_neighbors; - float m_trigger_interval; - u32 m_trigger_chance; - bool m_simple_catch_up; - s16 m_min_y; - s16 m_max_y; -public: - LuaABM(lua_State *L, int id, - const std::vector &trigger_contents, - const std::vector &required_neighbors, - float trigger_interval, u32 trigger_chance, bool simple_catch_up, s16 min_y, s16 max_y): - m_id(id), - m_trigger_contents(trigger_contents), - m_required_neighbors(required_neighbors), - m_trigger_interval(trigger_interval), - m_trigger_chance(trigger_chance), - m_simple_catch_up(simple_catch_up), - m_min_y(min_y), - m_max_y(max_y) - { - } - virtual const std::vector &getTriggerContents() const - { - return m_trigger_contents; - } - virtual const std::vector &getRequiredNeighbors() const - { - return m_required_neighbors; - } - virtual float getTriggerInterval() - { - return m_trigger_interval; - } - virtual u32 getTriggerChance() - { - return m_trigger_chance; - } - virtual bool getSimpleCatchUp() - { - return m_simple_catch_up; - } - virtual s16 getMinY() - { - return m_min_y; - } - virtual s16 getMaxY() - { - return m_max_y; - } - virtual void trigger(ServerEnvironment *env, v3s16 p, MapNode n, - u32 active_object_count, u32 active_object_count_wider); -}; - -class LuaLBM : public LoadingBlockModifierDef -{ -private: - int m_id; -public: - LuaLBM(lua_State *L, int id, - const std::set &trigger_contents, - const std::string &name, - bool run_at_every_load): - m_id(id) - { - this->run_at_every_load = run_at_every_load; - this->trigger_contents = trigger_contents; - this->name = name; - } - virtual void trigger(ServerEnvironment *env, v3s16 p, MapNode n, float dtime_s); -}; - //! Lua wrapper for RaycastState objects class LuaRaycast : public ModApiBase { diff --git a/src/script/lua_api/l_ipc.cpp b/src/script/lua_api/l_ipc.cpp new file mode 100644 index 000000000..8b9f2aec9 --- /dev/null +++ b/src/script/lua_api/l_ipc.cpp @@ -0,0 +1,141 @@ +// Minetest +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "lua_api/l_ipc.h" +#include "lua_api/l_internal.h" +#include "common/c_packer.h" +#include "server.h" +#include "debug.h" +#include + +typedef std::shared_lock SharedReadLock; +typedef std::unique_lock SharedWriteLock; + +static inline auto read_pv(lua_State *L, int idx) +{ + std::unique_ptr ret; + if (!lua_isnil(L, idx)) { + ret.reset(script_pack(L, idx)); + if (ret->contains_userdata) + throw LuaError("Userdata not allowed"); + } + return ret; +} + +int ModApiIPC::l_ipc_get(lua_State *L) +{ + auto *store = getGameDef(L)->getModIPCStore(); + + auto key = readParam(L, 1); + + { + SharedReadLock autolock(store->mutex); + auto it = store->map.find(key); + if (it == store->map.end()) + lua_pushnil(L); + else + script_unpack(L, it->second.get()); + } + return 1; +} + +int ModApiIPC::l_ipc_set(lua_State *L) +{ + auto *store = getGameDef(L)->getModIPCStore(); + + auto key = readParam(L, 1); + + luaL_checkany(L, 2); + auto pv = read_pv(L, 2); + + { + SharedWriteLock autolock(store->mutex); + if (pv) + store->map[key] = std::move(pv); + else + store->map.erase(key); // delete the map value for nil + } + store->signal(); + return 0; +} + +int ModApiIPC::l_ipc_cas(lua_State *L) +{ + auto *store = getGameDef(L)->getModIPCStore(); + + auto key = readParam(L, 1); + + luaL_checkany(L, 2); + const int idx_old = 2; + + luaL_checkany(L, 3); + auto pv_new = read_pv(L, 3); + + bool ok = false; + { + SharedWriteLock autolock(store->mutex); + // unpack and compare old value + auto it = store->map.find(key); + if (it == store->map.end()) { + ok = lua_isnil(L, idx_old); + } else { + script_unpack(L, it->second.get()); + ok = lua_equal(L, idx_old, -1); + lua_pop(L, 1); + } + // put new value + if (ok) { + if (pv_new) + store->map[key] = std::move(pv_new); + else + store->map.erase(key); + } + } + + if (ok) + store->signal(); + lua_pushboolean(L, ok); + return 1; +} + +int ModApiIPC::l_ipc_poll(lua_State *L) +{ + auto *store = getGameDef(L)->getModIPCStore(); + + auto key = readParam(L, 1); + + auto timeout = std::chrono::milliseconds( + std::max(0, luaL_checkinteger(L, 2)) + ); + + bool ret; + { + SharedReadLock autolock(store->mutex); + + // wait until value exists or timeout + ret = store->condvar.wait_for(autolock, timeout, [&] () -> bool { + return store->map.count(key) != 0; + }); + } + + lua_pushboolean(L, ret); + return 1; +} + +/* + * Implementation note: + * Iterating over the IPC table is intentionally not supported. + * Mods should know what they have set. + * This has the nice side effect that mods are able to use a randomly generated key + * if they really *really* want to avoid other code touching their data. + */ + +void ModApiIPC::Initialize(lua_State *L, int top) +{ + FATAL_ERROR_IF(!getGameDef(L)->getModIPCStore(), "ModIPCStore missing from gamedef"); + + API_FCT(ipc_get); + API_FCT(ipc_set); + API_FCT(ipc_cas); + API_FCT(ipc_poll); +} diff --git a/src/script/lua_api/l_ipc.h b/src/script/lua_api/l_ipc.h new file mode 100644 index 000000000..dc73a5b86 --- /dev/null +++ b/src/script/lua_api/l_ipc.h @@ -0,0 +1,17 @@ +// Minetest +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "lua_api/l_base.h" + +class ModApiIPC : public ModApiBase { +private: + static int l_ipc_get(lua_State *L); + static int l_ipc_set(lua_State *L); + static int l_ipc_cas(lua_State *L); + static int l_ipc_poll(lua_State *L); + +public: + static void Initialize(lua_State *L, int top); +}; diff --git a/src/script/lua_api/l_itemstackmeta.cpp b/src/script/lua_api/l_itemstackmeta.cpp index ebabf7bae..730fab3b4 100644 --- a/src/script/lua_api/l_itemstackmeta.cpp +++ b/src/script/lua_api/l_itemstackmeta.cpp @@ -41,7 +41,7 @@ void ItemStackMetaRef::clearMeta() void ItemStackMetaRef::reportMetadataChange(const std::string *name) { - // TODO + // nothing to do } // Exported functions @@ -89,7 +89,6 @@ ItemStackMetaRef::~ItemStackMetaRef() void ItemStackMetaRef::create(lua_State *L, LuaItemStack *istack) { ItemStackMetaRef *o = new ItemStackMetaRef(istack); - //infostream<<"NodeMetaRef::create: o="<m_rendering_engine->get_gui_env(), + engine->m_parent, -1, engine->m_menumanager, + engine->m_texture_source.get(), url); + openURLMenu->drop(); + return 1; +} + /******************************************************************************/ int ModApiMainMenu::l_open_dir(lua_State *L) { @@ -1128,7 +1160,9 @@ void ModApiMainMenu::Initialize(lua_State *L, int top) API_FCT(get_active_irrlicht_device); API_FCT(get_min_supp_proto); API_FCT(get_max_supp_proto); + API_FCT(get_formspec_version); API_FCT(open_url); + API_FCT(open_url_dialog); API_FCT(open_dir); API_FCT(share_file); API_FCT(do_async_callback); @@ -1158,6 +1192,7 @@ void ModApiMainMenu::InitializeAsync(lua_State *L, int top) API_FCT(download_file); API_FCT(get_min_supp_proto); API_FCT(get_max_supp_proto); + API_FCT(get_formspec_version); API_FCT(get_language); API_FCT(gettext); } diff --git a/src/script/lua_api/l_mainmenu.h b/src/script/lua_api/l_mainmenu.h index 5535d2170..cb3e7f9ca 100644 --- a/src/script/lua_api/l_mainmenu.h +++ b/src/script/lua_api/l_mainmenu.h @@ -159,9 +159,13 @@ private: static int l_get_max_supp_proto(lua_State *L); + static int l_get_formspec_version(lua_State *L); + // other static int l_open_url(lua_State *L); + static int l_open_url_dialog(lua_State *L); + static int l_open_dir(lua_State *L); static int l_share_file(lua_State *L); diff --git a/src/script/lua_api/l_nodemeta.cpp b/src/script/lua_api/l_nodemeta.cpp index f1a2d5c4b..07bdced99 100644 --- a/src/script/lua_api/l_nodemeta.cpp +++ b/src/script/lua_api/l_nodemeta.cpp @@ -58,6 +58,8 @@ void NodeMetaRef::reportMetadataChange(const std::string *name) // Inform other things that the metadata has changed NodeMetadata *meta = dynamic_cast(getmeta(false)); + bool is_private_change = meta && name && meta->isPrivate(*name); + // If the metadata is now empty, get rid of it if (meta && meta->empty()) { clearMeta(); @@ -67,7 +69,7 @@ void NodeMetaRef::reportMetadataChange(const std::string *name) MapEditEvent event; event.type = MEET_BLOCK_NODE_METADATA_CHANGED; event.setPositionModified(m_p); - event.is_private_change = name && meta && meta->isPrivate(*name); + event.is_private_change = is_private_change; m_env->getMap().dispatchEvent(event); } @@ -94,21 +96,24 @@ int NodeMetaRef::l_mark_as_private(lua_State *L) NodeMetaRef *ref = checkObject(L, 1); NodeMetadata *meta = dynamic_cast(ref->getmeta(true)); - assert(meta); + if (!meta) + return 0; + bool modified = false; if (lua_istable(L, 2)) { lua_pushnil(L); while (lua_next(L, 2) != 0) { // key at index -2 and value at index -1 luaL_checktype(L, -1, LUA_TSTRING); - meta->markPrivate(readParam(L, -1), true); + modified |= meta->markPrivate(readParam(L, -1), true); // removes value, keeps key for next iteration lua_pop(L, 1); } } else if (lua_isstring(L, 2)) { - meta->markPrivate(readParam(L, 2), true); + modified |= meta->markPrivate(readParam(L, 2), true); } - ref->reportMetadataChange(); + if (modified) + ref->reportMetadataChange(); return 0; } @@ -145,12 +150,13 @@ bool NodeMetaRef::handleFromTable(lua_State *L, int table, IMetadata *_meta) Inventory *inv = meta->getInventory(); lua_getfield(L, table, "inventory"); if (lua_istable(L, -1)) { + auto *gamedef = getGameDef(L); int inventorytable = lua_gettop(L); lua_pushnil(L); while (lua_next(L, inventorytable) != 0) { // key at index -2 and value at index -1 - std::string name = luaL_checkstring(L, -2); - read_inventory_list(L, -1, inv, name.c_str(), getServer(L)); + const char *name = luaL_checkstring(L, -2); + read_inventory_list(L, -1, inv, name, gamedef); lua_pop(L, 1); // Remove value, keep key for next iteration } lua_pop(L, 1); @@ -177,7 +183,6 @@ NodeMetaRef::NodeMetaRef(IMetadata *meta): void NodeMetaRef::create(lua_State *L, v3s16 p, ServerEnvironment *env) { NodeMetaRef *o = new NodeMetaRef(p, env); - //infostream<<"NodeMetaRef::create: o="<> std::hex >> state[0]; s_state_1 >> std::hex >> state[1]; - + o->m_rnd.setState(state); - + return 0; } @@ -641,10 +641,9 @@ int LuaSecureRandom::create_object(lua_State *L) { LuaSecureRandom *o = new LuaSecureRandom(); - // Fail and return nil if we can't securely fill the buffer if (!o->fillRandBuf()) { delete o; - return 0; + throw LuaError("SecureRandom: Failed to find secure random device on system"); } *(void **)(lua_newuserdata(L, sizeof(void *))) = o; diff --git a/src/script/lua_api/l_object.cpp b/src/script/lua_api/l_object.cpp index 00c825ddc..cd9be5428 100644 --- a/src/script/lua_api/l_object.cpp +++ b/src/script/lua_api/l_object.cpp @@ -433,10 +433,10 @@ int ObjectRef::l_set_local_animation(lua_State *L) if (player == nullptr) return 0; - v2s32 frames[4]; + v2f frames[4]; for (int i=0;i<4;i++) { if (!lua_isnil(L, 2+1)) - frames[i] = read_v2s32(L, 2+i); + frames[i] = read_v2f(L, 2+i); } float frame_speed = readParam(L, 6, 30.0f); @@ -453,12 +453,12 @@ int ObjectRef::l_get_local_animation(lua_State *L) if (player == nullptr) return 0; - v2s32 frames[4]; + v2f frames[4]; float frame_speed; player->getLocalAnimations(frames, &frame_speed); - for (const v2s32 &frame : frames) { - push_v2s32(L, frame); + for (const v2f &frame : frames) { + push_v2f(L, frame); } lua_pushnumber(L, frame_speed); @@ -562,8 +562,10 @@ int ObjectRef::l_set_bone_position(lua_State *L) BoneOverride props; if (!lua_isnoneornil(L, 3)) props.position.vector = check_v3f(L, 3); - if (!lua_isnoneornil(L, 4)) - props.rotation.next = core::quaternion(check_v3f(L, 4) * core::DEGTORAD); + if (!lua_isnoneornil(L, 4)) { + props.rotation.next_radians = check_v3f(L, 4) * core::DEGTORAD; + props.rotation.next = core::quaternion(props.rotation.next_radians); + } props.position.absolute = true; props.rotation.absolute = true; sao->setBoneOverride(bone, props); @@ -585,9 +587,9 @@ int ObjectRef::l_get_bone_position(lua_State *L) std::string bone = readParam(L, 2, ""); BoneOverride props = sao->getBoneOverride(bone); push_v3f(L, props.position.vector); - v3f euler_rot; - props.rotation.next.toEuler(euler_rot); - push_v3f(L, euler_rot * core::RADTODEG); + // In order to give modders back the euler angles they passed in, + // this **must not** compute equivalent euler angles from the quaternion + push_v3f(L, props.rotation.next_radians * core::RADTODEG); return 2; } @@ -633,8 +635,10 @@ int ObjectRef::l_set_bone_override(lua_State *L) lua_getfield(L, 3, "rotation"); if (!lua_isnil(L, -1)) { lua_getfield(L, -1, "vec"); - if (!lua_isnil(L, -1)) - props.rotation.next = core::quaternion(check_v3f(L, -1)); + if (!lua_isnil(L, -1)) { + props.rotation.next_radians = check_v3f(L, -1); + props.rotation.next = core::quaternion(props.rotation.next_radians); + } lua_pop(L, 1); read_prop_attrs(props.rotation); @@ -672,9 +676,9 @@ static void push_bone_override(lua_State *L, const BoneOverride &props) push_prop("position", props.position, props.position.vector); - v3f euler_rot; - props.rotation.next.toEuler(euler_rot); - push_prop("rotation", props.rotation, euler_rot); + // In order to give modders back the euler angles they passed in, + // this **must not** compute equivalent euler angles from the quaternion + push_prop("rotation", props.rotation, props.rotation.next_radians); push_prop("scale", props.scale, props.scale.vector); @@ -1517,7 +1521,7 @@ int ObjectRef::l_get_meta(lua_State *L) if (playersao == nullptr) return 0; - PlayerMetaRef::create(L, &playersao->getMeta()); + PlayerMetaRef::create(L, &getServer(L)->getEnv(), playersao->getPlayer()->getName()); return 1; } @@ -1618,6 +1622,13 @@ int ObjectRef::l_get_player_control(lua_State *L) lua_setfield(L, -2, "dig"); lua_pushboolean(L, control.place); lua_setfield(L, -2, "place"); + + v2f movement = control.getMovement(); + lua_pushnumber(L, movement.X); + lua_setfield(L, -2, "movement_x"); + lua_pushnumber(L, movement.Y); + lua_setfield(L, -2, "movement_y"); + // Legacy fields to ensure mod compatibility lua_pushboolean(L, control.dig); lua_setfield(L, -2, "LMB"); @@ -2444,6 +2455,10 @@ int ObjectRef::l_set_clouds(lua_State *L) if (!lua_isnil(L, -1)) read_color(L, -1, &cloud_params.color_ambient); lua_pop(L, 1); + lua_getfield(L, 2, "shadow"); + if (!lua_isnil(L, -1)) + read_color(L, -1, &cloud_params.color_shadow); + lua_pop(L, 1); cloud_params.height = getfloatfield_default(L, 2, "height", cloud_params.height); cloud_params.thickness = getfloatfield_default(L, 2, "thickness", cloud_params.thickness); @@ -2479,6 +2494,8 @@ int ObjectRef::l_get_clouds(lua_State *L) lua_setfield(L, -2, "color"); push_ARGB8(L, cloud_params.color_ambient); lua_setfield(L, -2, "ambient"); + push_ARGB8(L, cloud_params.color_shadow); + lua_setfield(L, -2, "shadow"); lua_pushnumber(L, cloud_params.height); lua_setfield(L, -2, "height"); lua_pushnumber(L, cloud_params.thickness); @@ -2607,6 +2624,9 @@ int ObjectRef::l_set_lighting(lua_State *L) lua_getfield(L, 2, "shadows"); if (lua_istable(L, -1)) { getfloatfield(L, -1, "intensity", lighting.shadow_intensity); + lua_getfield(L, -1, "tint"); + read_color(L, -1, &lighting.shadow_tint); + lua_pop(L, 1); // tint } lua_pop(L, 1); // shadows @@ -2629,6 +2649,14 @@ int ObjectRef::l_set_lighting(lua_State *L) lighting.volumetric_light_strength = rangelim(lighting.volumetric_light_strength, 0.0f, 1.0f); } lua_pop(L, 1); // volumetric_light + + lua_getfield(L, 2, "bloom"); + if (lua_istable(L, -1)) { + lighting.bloom_intensity = getfloatfield_default(L, -1, "intensity", lighting.bloom_intensity); + lighting.bloom_strength_factor = getfloatfield_default(L, -1, "strength_factor", lighting.bloom_strength_factor); + lighting.bloom_radius = getfloatfield_default(L, -1, "radius", lighting.bloom_radius); + } + lua_pop(L, 1); // bloom } getServer(L)->setLighting(player, lighting); @@ -2650,6 +2678,8 @@ int ObjectRef::l_get_lighting(lua_State *L) lua_newtable(L); // "shadows" lua_pushnumber(L, lighting.shadow_intensity); lua_setfield(L, -2, "intensity"); + push_ARGB8(L, lighting.shadow_tint); + lua_setfield(L, -2, "tint"); lua_setfield(L, -2, "shadows"); lua_pushnumber(L, lighting.saturation); lua_setfield(L, -2, "saturation"); @@ -2671,6 +2701,14 @@ int ObjectRef::l_get_lighting(lua_State *L) lua_pushnumber(L, lighting.volumetric_light_strength); lua_setfield(L, -2, "strength"); lua_setfield(L, -2, "volumetric_light"); + lua_newtable(L); // "bloom" + lua_pushnumber(L, lighting.bloom_intensity); + lua_setfield(L, -2, "intensity"); + lua_pushnumber(L, lighting.bloom_strength_factor); + lua_setfield(L, -2, "strength_factor"); + lua_pushnumber(L, lighting.bloom_radius); + lua_setfield(L, -2, "radius"); + lua_setfield(L, -2, "bloom"); return 1; } @@ -2679,15 +2717,50 @@ int ObjectRef::l_respawn(lua_State *L) { NO_MAP_LOCK_REQUIRED; ObjectRef *ref = checkObject(L, 1); - RemotePlayer *player = getplayer(ref); - if (player == nullptr) + auto *psao = getplayersao(ref); + if (psao == nullptr) return 0; - getServer(L)->RespawnPlayer(player->getPeerId()); + psao->respawn(); lua_pushboolean(L, true); return 1; } +// set_flags(self, flags) +int ObjectRef::l_set_flags(lua_State *L) +{ + NO_MAP_LOCK_REQUIRED; + ObjectRef *ref = checkObject(L, 1); + auto *psao = getplayersao(ref); + if (psao == nullptr) + return 0; + if (!lua_istable(L, -1)) + throw LuaError("expected a table of flags"); + auto &flags = psao->m_flags; + flags.drowning = getboolfield_default(L, -1, "drowning", flags.drowning); + flags.breathing = getboolfield_default(L, -1, "breathing", flags.breathing); + flags.node_damage = getboolfield_default(L, -1, "node_damage", flags.node_damage); + return 0; +} + +// get_flags(self) +int ObjectRef::l_get_flags(lua_State *L) +{ + NO_MAP_LOCK_REQUIRED; + ObjectRef *ref = checkObject(L, 1); + const auto *psao = getplayersao(ref); + if (psao == nullptr) + return 0; + lua_createtable(L, 0, 3); + lua_pushboolean(L, psao->m_flags.drowning); + lua_setfield(L, -2, "drowning"); + lua_pushboolean(L, psao->m_flags.breathing); + lua_setfield(L, -2, "breathing"); + lua_pushboolean(L, psao->m_flags.node_damage); + lua_setfield(L, -2, "node_damage"); + return 1; +} + ObjectRef::ObjectRef(ServerActiveObject *object): m_object(object) @@ -2703,9 +2776,11 @@ void ObjectRef::create(lua_State *L, ServerActiveObject *object) lua_setmetatable(L, -2); } -void ObjectRef::set_null(lua_State *L) +void ObjectRef::set_null(lua_State *L, void *expect) { ObjectRef *obj = checkObject(L, -1); + assert(obj); + FATAL_ERROR_IF(obj->m_object != expect, "ObjectRef table was messed with"); obj->m_object = nullptr; } @@ -2838,6 +2913,8 @@ luaL_Reg ObjectRef::methods[] = { luamethod(ObjectRef, set_lighting), luamethod(ObjectRef, get_lighting), luamethod(ObjectRef, respawn), + luamethod(ObjectRef, set_flags), + luamethod(ObjectRef, get_flags), {0,0} }; diff --git a/src/script/lua_api/l_object.h b/src/script/lua_api/l_object.h index ace19e1f0..bc131f4f2 100644 --- a/src/script/lua_api/l_object.h +++ b/src/script/lua_api/l_object.h @@ -38,10 +38,12 @@ public: ~ObjectRef() = default; // Creates an ObjectRef and leaves it on top of stack - // Not callable from Lua; all references are created on the C side. + // NOTE: do not call this, use `ScriptApiBase::objectrefGetOrCreate()`! static void create(lua_State *L, ServerActiveObject *object); - static void set_null(lua_State *L); + // Clear the pointer in the ObjectRef (at -1). + // Throws an fatal error if the object pointer wasn't `expect`. + static void set_null(lua_State *L, void *expect); static void Register(lua_State *L); @@ -411,4 +413,10 @@ private: // respawn(self) static int l_respawn(lua_State *L); + + // set_flags(self, flags) + static int l_set_flags(lua_State *L); + + // get_flags(self) + static int l_get_flags(lua_State *L); }; diff --git a/src/script/lua_api/l_playermeta.cpp b/src/script/lua_api/l_playermeta.cpp index e2e6ed8da..a3377c524 100644 --- a/src/script/lua_api/l_playermeta.cpp +++ b/src/script/lua_api/l_playermeta.cpp @@ -21,6 +21,9 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "lua_api/l_playermeta.h" #include "lua_api/l_internal.h" #include "common/c_content.h" +#include "serverenvironment.h" +#include "remoteplayer.h" +#include "server/player_sao.h" /* PlayerMetaRef @@ -28,24 +31,27 @@ with this program; if not, write to the Free Software Foundation, Inc., IMetadata *PlayerMetaRef::getmeta(bool auto_create) { - return metadata; + auto *player = m_env->getPlayer(m_name); + auto *sao = player ? player->getPlayerSAO() : nullptr; + return sao ? &sao->getMeta() : nullptr; } void PlayerMetaRef::clearMeta() { - metadata->clear(); + if (auto *meta = getmeta(true)) + meta->clear(); } void PlayerMetaRef::reportMetadataChange(const std::string *name) { - // TODO + // the server saves these on its own } // Creates an PlayerMetaRef and leaves it on top of stack // Not callable from Lua; all references are created on the C side. -void PlayerMetaRef::create(lua_State *L, IMetadata *metadata) +void PlayerMetaRef::create(lua_State *L, ServerEnvironment *env, std::string_view name) { - PlayerMetaRef *o = new PlayerMetaRef(metadata); + PlayerMetaRef *o = new PlayerMetaRef(env, name); *(void **)(lua_newuserdata(L, sizeof(void *))) = o; luaL_getmetatable(L, className); lua_setmetatable(L, -2); @@ -54,9 +60,6 @@ void PlayerMetaRef::create(lua_State *L, IMetadata *metadata) void PlayerMetaRef::Register(lua_State *L) { registerMetadataClass(L, className, methods); - - // Cannot be created from Lua - // lua_register(L, className, create_object); } const char PlayerMetaRef::className[] = "PlayerMetaRef"; diff --git a/src/script/lua_api/l_playermeta.h b/src/script/lua_api/l_playermeta.h index f07bdcd09..3f39c3755 100644 --- a/src/script/lua_api/l_playermeta.h +++ b/src/script/lua_api/l_playermeta.h @@ -24,12 +24,14 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "lua_api/l_metadata.h" #include "irrlichttypes_bloated.h" #include "inventory.h" -#include "metadata.h" + +class ServerEnvironment; class PlayerMetaRef : public MetaDataRef { private: - IMetadata *metadata = nullptr; + ServerEnvironment *m_env; + std::string m_name; static const luaL_Reg methods[]; @@ -40,12 +42,17 @@ private: virtual void reportMetadataChange(const std::string *name = nullptr); public: - PlayerMetaRef(IMetadata *metadata) : metadata(metadata) {} + PlayerMetaRef(ServerEnvironment *env, std::string_view name) : + m_env(env), m_name(name) + { + assert(m_env); + assert(!m_name.empty()); + } ~PlayerMetaRef() = default; - // Creates an ItemStackMetaRef and leaves it on top of stack + // Creates an PlayerMetaRef and leaves it on top of stack // Not callable from Lua; all references are created on the C side. - static void create(lua_State *L, IMetadata *metadata); + static void create(lua_State *L, ServerEnvironment *env, std::string_view name); static void Register(lua_State *L); diff --git a/src/script/lua_api/l_server.cpp b/src/script/lua_api/l_server.cpp index af9a526e0..82170f936 100644 --- a/src/script/lua_api/l_server.cpp +++ b/src/script/lua_api/l_server.cpp @@ -365,7 +365,7 @@ int ModApiServer::l_ban_player(lua_State *L) return 1; } -// disconnect_player(name, [reason]) -> success +// disconnect_player(name[, reason[, reconnect]]) -> success int ModApiServer::l_disconnect_player(lua_State *L) { NO_MAP_LOCK_REQUIRED; @@ -388,7 +388,9 @@ int ModApiServer::l_disconnect_player(lua_State *L) return 1; } - server->DenyAccess(player->getPeerId(), SERVER_ACCESSDENIED_CUSTOM_STRING, message); + bool reconnect = readParam(L, 3, false); + + server->DenyAccess(player->getPeerId(), SERVER_ACCESSDENIED_CUSTOM_STRING, message, reconnect); lua_pushboolean(L, true); return 1; } @@ -426,18 +428,8 @@ int ModApiServer::l_show_formspec(lua_State *L) NO_MAP_LOCK_REQUIRED; const char *playername = luaL_checkstring(L, 1); const char *formname = luaL_checkstring(L, 2); - if (*formname == '\0') { - log_deprecated(L, "Deprecated call to `minetest.show_formspec`:" - "`formname` must not be empty"); - } const char *formspec = luaL_checkstring(L, 3); - - if(getServer(L)->showFormspec(playername,formspec,formname)) - { - lua_pushboolean(L, true); - }else{ - lua_pushboolean(L, false); - } + lua_pushboolean(L, getServer(L)->showFormspec(playername,formspec,formname)); return 1; } diff --git a/src/script/lua_api/l_server.h b/src/script/lua_api/l_server.h index 505dce735..a0fae79bd 100644 --- a/src/script/lua_api/l_server.h +++ b/src/script/lua_api/l_server.h @@ -106,7 +106,7 @@ private: // unban_player_or_ip() static int l_unban_player_or_ip(lua_State *L); - // disconnect_player(name, [reason]) -> success + // disconnect_player(name[, reason[, reconnect]]) -> success static int l_disconnect_player(lua_State *L); // remove_player(name) diff --git a/src/script/lua_api/l_util.cpp b/src/script/lua_api/l_util.cpp index fac3e54d1..c899e55f4 100644 --- a/src/script/lua_api/l_util.cpp +++ b/src/script/lua_api/l_util.cpp @@ -33,6 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "convert_json.h" #include "debug.h" #include "log.h" +#include "log_internal.h" #include "tool.h" #include "filesys.h" #include "settings.h" @@ -45,6 +46,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "my_sha256.h" #include "util/png.h" #include "player.h" +#include "daynightratio.h" #include // only available in zstd 1.3.5+ @@ -61,13 +63,13 @@ with this program; if not, write to the Free Software Foundation, Inc., int ModApiUtil::l_log(lua_State *L) { NO_MAP_LOCK_REQUIRED; - std::string text; + std::string_view text; LogLevel level = LL_NONE; - if (lua_isnone(L, 2)) { - text = luaL_checkstring(L, 1); + if (lua_isnoneornil(L, 2)) { + text = readParam(L, 1); } else { - std::string name = luaL_checkstring(L, 1); - text = luaL_checkstring(L, 2); + auto name = readParam(L, 1); + text = readParam(L, 2); if (name == "deprecated") { log_deprecated(L, text, 2); return 0; @@ -75,7 +77,7 @@ int ModApiUtil::l_log(lua_State *L) level = Logger::stringToLevel(name); if (level == LL_MAX) { warningstream << "Tried to log at unknown level '" << name - << "'. Defaulting to \"none\"." << std::endl; + << "'. Defaulting to \"none\"." << std::endl; level = LL_NONE; } } @@ -530,7 +532,7 @@ int ModApiUtil::l_get_version(lua_State *L) lua_pushnumber(L, SERVER_PROTOCOL_VERSION_MIN); lua_setfield(L, table, "proto_min"); - lua_pushnumber(L, SERVER_PROTOCOL_VERSION_MAX); + lua_pushnumber(L, LATEST_PROTOCOL_VERSION); lua_setfield(L, table, "proto_max"); if (strcmp(g_version_string, g_version_hash) != 0) { @@ -626,6 +628,31 @@ int ModApiUtil::l_colorspec_to_bytes(lua_State *L) return 0; } +// colorspec_to_table(colorspec) +int ModApiUtil::l_colorspec_to_table(lua_State *L) +{ + NO_MAP_LOCK_REQUIRED; + + video::SColor color(0); + if (read_color(L, 1, &color)) { + push_ARGB8(L, color); + return 1; + } + + return 0; +} + +// time_to_day_night_ratio(time_of_day) +int ModApiUtil::l_time_to_day_night_ratio(lua_State *L) +{ + NO_MAP_LOCK_REQUIRED; + + float time_of_day = lua_tonumber(L, 1) * 24000; + u32 dnr = time_to_daynight_ratio(time_of_day, true); + lua_pushnumber(L, dnr / 1000.0f); + return 1; +} + // encode_png(w, h, data, level) int ModApiUtil::l_encode_png(lua_State *L) { @@ -726,6 +753,8 @@ void ModApiUtil::Initialize(lua_State *L, int top) API_FCT(sha256); API_FCT(colorspec_to_colorstring); API_FCT(colorspec_to_bytes); + API_FCT(colorspec_to_table); + API_FCT(time_to_day_night_ratio); API_FCT(encode_png); @@ -761,6 +790,8 @@ void ModApiUtil::InitializeClient(lua_State *L, int top) API_FCT(sha256); API_FCT(colorspec_to_colorstring); API_FCT(colorspec_to_bytes); + API_FCT(colorspec_to_table); + API_FCT(time_to_day_night_ratio); API_FCT(get_last_run_mod); API_FCT(set_last_run_mod); @@ -805,6 +836,8 @@ void ModApiUtil::InitializeAsync(lua_State *L, int top) API_FCT(sha256); API_FCT(colorspec_to_colorstring); API_FCT(colorspec_to_bytes); + API_FCT(colorspec_to_table); + API_FCT(time_to_day_night_ratio); API_FCT(encode_png); diff --git a/src/script/lua_api/l_util.h b/src/script/lua_api/l_util.h index e0daf3e79..89cc684e1 100644 --- a/src/script/lua_api/l_util.h +++ b/src/script/lua_api/l_util.h @@ -122,6 +122,12 @@ private: // colorspec_to_bytes(colorspec) static int l_colorspec_to_bytes(lua_State *L); + // colorspec_to_table(colorspec) + static int l_colorspec_to_table(lua_State *L); + + // time_to_day_night_ratio(time_of_day) + static int l_time_to_day_night_ratio(lua_State *L); + // encode_png(w, h, data, level) static int l_encode_png(lua_State *L); diff --git a/src/script/lua_api/l_vmanip.cpp b/src/script/lua_api/l_vmanip.cpp index 76b5aff0f..33f6f7407 100644 --- a/src/script/lua_api/l_vmanip.cpp +++ b/src/script/lua_api/l_vmanip.cpp @@ -71,8 +71,7 @@ int LuaVoxelManip::l_get_data(lua_State *L) bool use_buffer = lua_istable(L, 2); MMVManip *vm = o->vm; - - u32 volume = vm->m_area.getVolume(); + const u32 volume = vm->m_area.getVolume(); if (use_buffer) lua_pushvalue(L, 2); @@ -80,7 +79,8 @@ int LuaVoxelManip::l_get_data(lua_State *L) lua_createtable(L, volume, 0); for (u32 i = 0; i != volume; i++) { - lua_Integer cid = vm->m_data[i].getContent(); + // Do not push unintialized data to Lua + lua_Integer cid = (vm->m_flags[i] & VOXELFLAG_NO_DATA) ? CONTENT_IGNORE : vm->m_data[i].getContent(); lua_pushinteger(L, cid); lua_rawseti(L, -2, i + 1); } @@ -108,6 +108,12 @@ int LuaVoxelManip::l_set_data(lua_State *L) lua_pop(L, 1); } + // FIXME: in theory we should clear VOXELFLAG_NO_DATA here + // However there is no way to tell which values Lua code has intended to set + // (if they were VOXELFLAG_NO_DATA before), and which were just not touched. + // In practice this doesn't cause problems because read_from_map() will cause + // all covered blocks to be loaded anyway. + return 0; } @@ -231,8 +237,7 @@ int LuaVoxelManip::l_get_light_data(lua_State *L) bool use_buffer = lua_istable(L, 2); MMVManip *vm = o->vm; - - u32 volume = vm->m_area.getVolume(); + const u32 volume = vm->m_area.getVolume(); if (use_buffer) lua_pushvalue(L, 2); @@ -240,7 +245,8 @@ int LuaVoxelManip::l_get_light_data(lua_State *L) lua_createtable(L, volume, 0); for (u32 i = 0; i != volume; i++) { - lua_Integer light = vm->m_data[i].param1; + // Do not push unintialized data to Lua + lua_Integer light = (vm->m_flags[i] & VOXELFLAG_NO_DATA) ? 0 : vm->m_data[i].getParam1(); lua_pushinteger(L, light); lua_rawseti(L, -2, i + 1); } @@ -280,8 +286,7 @@ int LuaVoxelManip::l_get_param2_data(lua_State *L) bool use_buffer = lua_istable(L, 2); MMVManip *vm = o->vm; - - u32 volume = vm->m_area.getVolume(); + const u32 volume = vm->m_area.getVolume(); if (use_buffer) lua_pushvalue(L, 2); @@ -289,7 +294,8 @@ int LuaVoxelManip::l_get_param2_data(lua_State *L) lua_createtable(L, volume, 0); for (u32 i = 0; i != volume; i++) { - lua_Integer param2 = vm->m_data[i].param2; + // Do not push unintialized data to Lua + lua_Integer param2 = (vm->m_flags[i] & VOXELFLAG_NO_DATA) ? 0 : vm->m_data[i].getParam2(); lua_pushinteger(L, param2); lua_rawseti(L, -2, i + 1); } @@ -333,6 +339,9 @@ int LuaVoxelManip::l_was_modified(lua_State *L) LuaVoxelManip *o = checkObject(L, 1); MMVManip *vm = o->vm; + if (!o->is_mapgen_vm) + log_deprecated(L, "was_modified called for a non-mapgen VoxelManip object"); + lua_pushboolean(L, vm->m_is_dirty); return 1; @@ -360,36 +369,35 @@ LuaVoxelManip::LuaVoxelManip(Map *map) : vm(new MMVManip(map)) { } -LuaVoxelManip::LuaVoxelManip(Map *map, v3s16 p1, v3s16 p2) -{ - vm = new MMVManip(map); - - v3s16 bp1 = getNodeBlockPos(p1); - v3s16 bp2 = getNodeBlockPos(p2); - sortBoxVerticies(bp1, bp2); - vm->initialEmerge(bp1, bp2); -} - LuaVoxelManip::~LuaVoxelManip() { if (!is_mapgen_vm) delete vm; } -// LuaVoxelManip() +// LuaVoxelManip([p1, p2]) // Creates an LuaVoxelManip and leaves it on top of stack int LuaVoxelManip::create_object(lua_State *L) { GET_ENV_PTR; - Map *map = &(env->getMap()); - LuaVoxelManip *o = (lua_istable(L, 1) && lua_istable(L, 2)) ? - new LuaVoxelManip(map, check_v3s16(L, 1), check_v3s16(L, 2)) : - new LuaVoxelManip(map); + LuaVoxelManip *o = new LuaVoxelManip(&env->getMap()); *(void **)(lua_newuserdata(L, sizeof(void *))) = o; luaL_getmetatable(L, className); lua_setmetatable(L, -2); + + // Call read_from_map so we don't have to duplicate it here + const int top = lua_gettop(L); + if (lua_istable(L, 1) && lua_istable(L, 2)) { + lua_pushcfunction(L, l_read_from_map); + lua_pushvalue(L, top); + lua_pushvalue(L, 1); + lua_pushvalue(L, 2); + lua_call(L, 3, 0); + } + lua_settop(L, top); + return 1; } diff --git a/src/script/lua_api/l_vmanip.h b/src/script/lua_api/l_vmanip.h index 04670bef6..820f89d5a 100644 --- a/src/script/lua_api/l_vmanip.h +++ b/src/script/lua_api/l_vmanip.h @@ -64,7 +64,6 @@ public: MMVManip *vm = nullptr; LuaVoxelManip(MMVManip *mmvm, bool is_mapgen_vm); - LuaVoxelManip(Map *map, v3s16 p1, v3s16 p2); LuaVoxelManip(Map *map); ~LuaVoxelManip(); diff --git a/src/script/scripting_emerge.cpp b/src/script/scripting_emerge.cpp index 3467b1495..f96a6c294 100644 --- a/src/script/scripting_emerge.cpp +++ b/src/script/scripting_emerge.cpp @@ -35,6 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "lua_api/l_util.h" #include "lua_api/l_vmanip.h" #include "lua_api/l_settings.h" +#include "lua_api/l_ipc.h" extern "C" { #include @@ -89,5 +90,6 @@ void EmergeScripting::InitializeModApi(lua_State *L, int top) ModApiMapgen::InitializeEmerge(L, top); ModApiServer::InitializeAsync(L, top); ModApiUtil::InitializeAsync(L, top); + ModApiIPC::Initialize(L, top); // TODO ^ these should also be renamed to InitializeRO or such } diff --git a/src/script/scripting_server.cpp b/src/script/scripting_server.cpp index 324850011..d7d2513bb 100644 --- a/src/script/scripting_server.cpp +++ b/src/script/scripting_server.cpp @@ -46,6 +46,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "lua_api/l_settings.h" #include "lua_api/l_http.h" #include "lua_api/l_storage.h" +#include "lua_api/l_ipc.h" extern "C" { #include @@ -121,6 +122,7 @@ void ServerScripting::initAsync() asyncEngine.registerStateInitializer(ModApiCraft::InitializeAsync); asyncEngine.registerStateInitializer(ModApiItem::InitializeAsync); asyncEngine.registerStateInitializer(ModApiServer::InitializeAsync); + asyncEngine.registerStateInitializer(ModApiIPC::Initialize); // not added: ModApiMapgen is a minefield for thread safety // not added: ModApiHttp async api can't really work together with our jobs // not added: ModApiStorage is probably not thread safe(?) @@ -176,6 +178,7 @@ void ServerScripting::InitializeModApi(lua_State *L, int top) ModApiHttp::Initialize(L, top); ModApiStorage::Initialize(L, top); ModApiChannels::Initialize(L, top); + ModApiIPC::Initialize(L, top); } void ServerScripting::InitializeAsync(lua_State *L, int top) diff --git a/src/serialization.cpp b/src/serialization.cpp index 4134126ca..d35e0f23f 100644 --- a/src/serialization.cpp +++ b/src/serialization.cpp @@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include +#include /* report a zlib or i/o error */ static void zerr(int ret) @@ -262,6 +263,8 @@ void decompressZstd(std::istream &is, std::ostream &os) is.read(input_buffer, bufsize); input.size = is.gcount(); input.pos = 0; + if (input.size == 0) + throw SerializationError("decompressZstd: data ended too early"); } ret = ZSTD_decompressStream(stream.get(), &output, &input); diff --git a/src/server.cpp b/src/server.cpp index 0b0786209..ab219043e 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -21,6 +21,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include +#include "irr_v2d.h" #include "network/connection.h" #include "network/networkprotocol.h" #include "network/serveropcodes.h" @@ -75,6 +76,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "gameparams.h" #include "particles.h" #include "gettext.h" +#include "util/tracy_wrapper.h" class ClientNotFoundException : public BaseException { @@ -84,6 +86,15 @@ public: {} }; +ModIPCStore::~ModIPCStore() +{ + // we don't have to do this, it's pure debugging aid + if (!std::unique_lock(mutex, std::try_to_lock).owns_lock()) { + errorstream << FUNCTION_NAME << ": lock is still in use!" << std::endl; + assert(0); + } +} + class ServerThread : public Thread { public: @@ -101,6 +112,8 @@ private: void *ServerThread::run() { + ZoneScoped; + BEGIN_DEBUG_EXCEPTION_HANDLER /* @@ -110,6 +123,7 @@ void *ServerThread::run() * server-step frequency. Receive() is used for waiting between the steps. */ + auto framemarker = FrameMarker("ServerThread::run()-frame").started(); try { m_server->AsyncRunStep(0.0f, true); } catch (con::ConnectionBindFailed &e) { @@ -119,17 +133,23 @@ void *ServerThread::run() } catch (ModError &e) { m_server->setAsyncFatalError(e.what()); } + framemarker.end(); float dtime = 0.0f; while (!stopRequested()) { + framemarker.start(); ScopeProfiler spm(g_profiler, "Server::RunStep() (max)", SPT_MAX); u64 t0 = porting::getTimeUs(); - const Server::StepSettings step_settings = m_server->getStepSettings(); + const auto step_settings = m_server->getStepSettings(); try { + // see explanation inside + if (dtime > step_settings.steplen) + m_server->yieldToOtherThreads(dtime); + m_server->AsyncRunStep(step_settings.pause ? 0.0f : dtime); const float remaining_time = step_settings.steplen @@ -149,6 +169,7 @@ void *ServerThread::run() } dtime = 1e-6f * (porting::getTimeUs() - t0); + framemarker.end(); } END_DEBUG_EXCEPTION_HANDLER @@ -258,11 +279,7 @@ Server::Server( m_simple_singleplayer_mode(simple_singleplayer_mode), m_dedicated(dedicated), m_async_fatal_error(""), - m_con(std::make_shared(PROTOCOL_ID, - 512, - CONNECTION_TIMEOUT, - m_bind_addr.isIPv6(), - this)), + m_con(con::createMTP(CONNECTION_TIMEOUT, m_bind_addr.isIPv6(), this)), m_itemdef(createItemDefManager()), m_nodedef(createNodeDefManager()), m_craftdef(createCraftDefManager()), @@ -350,7 +367,7 @@ Server::~Server() m_emerge->stopThreads(); if (m_env) { - MutexAutoLock envlock(m_env_mutex); + EnvAutoLock envlock(this); infostream << "Server: Executing shutdown hooks" << std::endl; try { @@ -386,6 +403,10 @@ Server::~Server() infostream << "Server: Saving environment metadata" << std::endl; m_env->saveMeta(); + // Delete classes that depend on the environment + m_inventory_mgr.reset(); + m_script.reset(); + // Note that this also deletes and saves the map. delete m_env; m_env = nullptr; @@ -402,6 +423,9 @@ Server::~Server() } } + // emerge may depend on definition managers, so destroy first + m_emerge.reset(); + // Delete the rest in the reverse order of creation delete m_game_settings; delete m_banmanager; @@ -458,7 +482,7 @@ void Server::init() } //lock environment - MutexAutoLock envlock(m_env_mutex); + EnvAutoLock envlock(this); // Create the Map (loads map_meta.txt, overriding configured mapgen params) auto startup_server_map = std::make_unique(m_path_world, this, @@ -555,7 +579,6 @@ void Server::start() m_thread->stop(); // Initialize connection - m_con->SetTimeoutMs(30); m_con->Serve(m_bind_addr); // Start thread @@ -612,6 +635,9 @@ void Server::step() void Server::AsyncRunStep(float dtime, bool initial_step) { + ZoneScoped; + auto framemarker = FrameMarker("Server::AsyncRunStep()-frame").started(); + { // Send blocks to clients SendBlocks(dtime); @@ -628,8 +654,6 @@ void Server::AsyncRunStep(float dtime, bool initial_step) */ m_uptime_counter->increment(dtime); - handlePeerChanges(); - /* Update time of day and overall game time */ @@ -650,9 +674,9 @@ void Server::AsyncRunStep(float dtime, bool initial_step) } { - MutexAutoLock lock(m_env_mutex); + EnvAutoLock lock(this); float max_lag = m_env->getMaxLagEstimate(); - constexpr float lag_warn_threshold = 2.0f; + constexpr float lag_warn_threshold = 1.0f; // Decrease value gradually, halve it every minute. if (m_max_lag_decrease.step(dtime, 0.5f)) { @@ -683,7 +707,7 @@ void Server::AsyncRunStep(float dtime, bool initial_step) static const float map_timer_and_unload_dtime = 2.92; if(m_map_timer_and_unload_interval.step(dtime, map_timer_and_unload_dtime)) { - MutexAutoLock lock(m_env_mutex); + EnvAutoLock lock(this); // Run Map's timers and unload unused data ScopeProfiler sp(g_profiler, "Server: map timer and unload"); m_env->getMap().timerUpdate(map_timer_and_unload_dtime, @@ -701,7 +725,7 @@ void Server::AsyncRunStep(float dtime, bool initial_step) */ if (m_admin_chat) { if (!m_admin_chat->command_queue.empty()) { - MutexAutoLock lock(m_env_mutex); + EnvAutoLock lock(this); while (!m_admin_chat->command_queue.empty()) { ChatEvent *evt = m_admin_chat->command_queue.pop_frontNoEx(); handleChatInterfaceEvent(evt); @@ -722,7 +746,7 @@ void Server::AsyncRunStep(float dtime, bool initial_step) { m_liquid_transform_timer -= m_liquid_transform_every; - MutexAutoLock lock(m_env_mutex); + EnvAutoLock lock(this); ScopeProfiler sp(g_profiler, "Server: liquid transform"); @@ -783,7 +807,7 @@ void Server::AsyncRunStep(float dtime, bool initial_step) */ { //infostream<<"Server: Checking added and deleted active objects"<getFloat("server_map_save_interval"); if (counter >= save_interval) { counter = 0.0; - MutexAutoLock lock(m_env_mutex); + EnvAutoLock lock(this); ScopeProfiler sp(g_profiler, "Server: map saving (sum)"); @@ -1062,6 +1086,9 @@ void Server::AsyncRunStep(float dtime, bool initial_step) void Server::Receive(float timeout) { + ZoneScoped; + auto framemarker = FrameMarker("Server::Receive()-frame").started(); + const u64 t0 = porting::getTimeUs(); const float timeout_us = timeout * 1e6f; auto remaining_time_us = [&]() -> float { @@ -1081,6 +1108,8 @@ void Server::Receive(float timeout) // and a faster server-step is better than busy waiting. if (remaining_time_us() < 1000.0f) break; + else + continue; } peer_id = pkt.getPeerId(); @@ -1105,6 +1134,52 @@ void Server::Receive(float timeout) } } +void Server::yieldToOtherThreads(float dtime) +{ + /* + * Problem: the server thread and emerge thread compete for the envlock. + * While the emerge thread needs it just once or twice for every processed item + * the server thread uses it much more generously. + * This is usually not a problem as the server sleeps between steps, which leaves + * enough chance. But if the server is overloaded it's busy all the time and + * - even with a fair envlock - the emerge thread can't get up to speed. + * This generally has a much worse impact on gameplay than server lag itself + * ever would. + * + * Workaround: If we detect that the server is overloaded, introduce some careful + * artificial sleeps to leave the emerge threads enough chance to do their job. + * + * In the future the emerge code should be reworked to exclusively use a result + * queue, thereby avoiding this problem (and terrible workaround). + */ + + // don't activate workaround too quickly + constexpr size_t MIN_EMERGE_QUEUE_SIZE = 32; + const size_t qs_initial = m_emerge->getQueueSize(); + if (qs_initial < MIN_EMERGE_QUEUE_SIZE) + return; + + // give the thread a chance to run for every 28ms (on average) + // this was experimentally determined + const float QUANTUM = 28.0f / 1000; + // put an upper limit to not cause too much lag, also so this doesn't become self-sustaining + const int SLEEP_MAX = 10; + + int sleep_count = std::clamp(dtime / QUANTUM, 1, SLEEP_MAX); + + ScopeProfiler sp(g_profiler, "Server::yieldTo...() sleep", SPT_AVG); + size_t qs = qs_initial; + while (sleep_count-- > 0) { + sleep_ms(1); + // abort if we don't make progress + size_t qs2 = m_emerge->getQueueSize(); + if (qs2 >= qs || qs2 == 0) + break; + qs = qs2; + } + g_profiler->avg("Server::yieldTo...() progress [#]", qs_initial - qs); +} + PlayerSAO* Server::StageTwoClientInit(session_t peer_id) { std::string playername; @@ -1151,10 +1226,6 @@ PlayerSAO* Server::StageTwoClientInit(session_t peer_id) // Send HP SendPlayerHP(playersao, false); - // Send death screen - if (playersao->isDead()) - SendDeathscreen(peer_id, false, v3f(0,0,0)); - // Send Breath SendPlayerBreath(playersao); @@ -1187,7 +1258,7 @@ inline void Server::handleCommand(NetworkPacket *pkt) void Server::ProcessData(NetworkPacket *pkt) { // Environment is locked first. - MutexAutoLock envlock(m_env_mutex); + EnvAutoLock envlock(this); ScopeProfiler sp(g_profiler, "Server: Process network packet (sum)"); u32 peer_id = pkt->getPeerId(); @@ -1258,21 +1329,20 @@ void Server::onMapEditEvent(const MapEditEvent &event) m_unsent_map_edit_queue.push(new MapEditEvent(event)); } -void Server::peerAdded(con::Peer *peer) +void Server::peerAdded(con::IPeer *peer) { - verbosestream<<"Server::peerAdded(): peer->id=" - <id<id=" - <id<<", timeout="<= 46) { + pkt << animation_frames[i]; + } else { + pkt << v2s32::from(animation_frames[i]); + } + } + + pkt << animation_speed; Send(&pkt); } @@ -2401,8 +2439,7 @@ void Server::SendBlockNoLock(session_t peer_id, MapBlock *block, u8 ver, void Server::SendBlocks(float dtime) { - MutexAutoLock envlock(m_env_mutex); - //TODO check if one big lock could be faster then multiple small ones + EnvAutoLock envlock(this); std::vector queue; @@ -2499,9 +2536,9 @@ bool Server::addMediaFile(const std::string &filename, const char *supported_ext[] = { ".png", ".jpg", ".bmp", ".tga", ".ogg", - ".x", ".b3d", ".obj", - // Custom translation file format - ".tr", + ".x", ".b3d", ".obj", ".gltf", ".glb", + // Translation file formats + ".tr", ".po", ".mo", NULL }; if (removeStringEnd(filename, supported_ext).empty()) { @@ -2584,14 +2621,20 @@ void Server::fillMediaCache() void Server::sendMediaAnnouncement(session_t peer_id, const std::string &lang_code) { - std::string lang_suffix = "."; - lang_suffix.append(lang_code).append(".tr"); + std::string translation_formats[3] = { ".tr", ".po", ".mo" }; + std::string lang_suffixes[3]; + for (size_t i = 0; i < 3; i++) { + lang_suffixes[i].append(".").append(lang_code).append(translation_formats[i]); + } - auto include = [&] (const std::string &name, const MediaInfo &info) -> bool { + auto include = [&] (const std::string &name, const MediaInfo &info) -> bool { if (info.no_announce) return false; - if (str_ends_with(name, ".tr") && !str_ends_with(name, lang_suffix)) - return false; + for (size_t j = 0; j < 3; j++) { + if (str_ends_with(name, translation_formats[j]) && !str_ends_with(name, lang_suffixes[j])) { + return false; + } + } return true; }; @@ -2733,7 +2776,7 @@ void Server::sendRequestedMedia(session_t peer_id, void Server::stepPendingDynMediaCallbacks(float dtime) { - MutexAutoLock lock(m_env_mutex); + EnvAutoLock lock(this); for (auto it = m_pending_dyn_media.begin(); it != m_pending_dyn_media.end();) { it->second.expiry_timer -= dtime; @@ -2829,32 +2872,8 @@ void Server::HandlePlayerDeath(PlayerSAO *playersao, const PlayerHPChangeReason // Trigger scripted stuff m_script->on_dieplayer(playersao, reason); - - SendDeathscreen(playersao->getPeerID(), false, v3f(0,0,0)); } -void Server::RespawnPlayer(session_t peer_id) -{ - PlayerSAO *playersao = getPlayerSAO(peer_id); - assert(playersao); - - infostream << "Server::RespawnPlayer(): Player " - << playersao->getPlayer()->getName() - << " respawns" << std::endl; - - const auto *prop = playersao->accessObjectProperties(); - playersao->setHP(prop->hp_max, - PlayerHPChangeReason(PlayerHPChangeReason::RESPAWN)); - playersao->setBreath(prop->breath_max); - - bool repositioned = m_script->on_respawnplayer(playersao); - if (!repositioned) { - // setPos will send the new position to client - playersao->setPos(findSpawnPos()); - } -} - - void Server::DenySudoAccess(session_t peer_id) { NetworkPacket pkt(TOCLIENT_DENY_SUDO_MODE, 0, peer_id); @@ -2863,7 +2882,7 @@ void Server::DenySudoAccess(session_t peer_id) void Server::DenyAccess(session_t peer_id, AccessDeniedCode reason, - const std::string &custom_reason, bool reconnect) + std::string_view custom_reason, bool reconnect) { SendAccessDenied(peer_id, reason, custom_reason, reconnect); m_clients.event(peer_id, CSE_SetDenied); @@ -2976,7 +2995,7 @@ void Server::DeleteClient(session_t peer_id, ClientDeletionReason reason) } } { - MutexAutoLock env_lock(m_env_mutex); + EnvAutoLock envlock(this); m_clients.DeleteClient(peer_id); } } @@ -3181,14 +3200,11 @@ std::string Server::getStatusString() bool first = true; os << " | clients: "; if (m_env) { - std::vector clients = m_clients.getClientIDs(); - for (session_t client_id : clients) { - RemotePlayer *player = m_env->getPlayer(client_id); + std::vector player_names = m_clients.getPlayerNames(); - // Get name of player - const std::string name = player ? player->getName() : ""; + std::sort(player_names.begin(), player_names.end()); - // Add name to information string + for (const std::string& name : player_names) { if (!first) os << ", "; else @@ -3431,7 +3447,7 @@ Address Server::getPeerAddress(session_t peer_id) } void Server::setLocalPlayerAnimations(RemotePlayer *player, - v2s32 animation_frames[4], f32 frame_speed) + v2f animation_frames[4], f32 frame_speed) { sanity_check(player); player->setLocalAnimations(animation_frames, frame_speed); @@ -4157,12 +4173,11 @@ Translations *Server::getTranslationLanguage(const std::string &lang_code) // [] will create an entry auto *translations = &server_translations[lang_code]; - std::string suffix = "." + lang_code + ".tr"; for (const auto &i : m_media) { - if (str_ends_with(i.first, suffix)) { + if (Translations::getFileLanguage(i.first) == lang_code) { std::string data; if (fs::ReadFile(i.second.path, data, true)) { - translations->loadTranslation(data); + translations->loadTranslation(i.first, data); } } } @@ -4172,7 +4187,7 @@ Translations *Server::getTranslationLanguage(const std::string &lang_code) std::unordered_map Server::getMediaList() { - MutexAutoLock env_lock(m_env_mutex); + EnvAutoLock envlock(this); std::unordered_map ret; for (auto &it : m_media) { @@ -4301,12 +4316,10 @@ u16 Server::getProtocolVersionMin() min_proto = LATEST_PROTOCOL_VERSION; return rangelim(min_proto, SERVER_PROTOCOL_VERSION_MIN, - SERVER_PROTOCOL_VERSION_MAX); + LATEST_PROTOCOL_VERSION); } u16 Server::getProtocolVersionMax() { - return g_settings->getBool("strict_protocol_version_checking") - ? LATEST_PROTOCOL_VERSION - : SERVER_PROTOCOL_VERSION_MAX; + return LATEST_PROTOCOL_VERSION; } diff --git a/src/server.h b/src/server.h index 6d439cc67..f2a9083b6 100644 --- a/src/server.h +++ b/src/server.h @@ -28,13 +28,14 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "inventorymanager.h" #include "content/subgames.h" #include "network/peerhandler.h" -#include "network/address.h" +#include "network/connection.h" #include "util/numeric.h" #include "util/thread.h" #include "util/basic_macros.h" #include "util/metricsbackend.h" #include "serverenvironment.h" #include "server/clientiface.h" +#include "threading/ordered_mutex.h" #include "chatmessage.h" #include "sound.h" #include "translation.h" @@ -46,6 +47,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include +#include +#include class ChatEvent; struct ChatEventChat; @@ -78,6 +81,20 @@ struct PackedValue; struct ParticleParameters; struct ParticleSpawnerParameters; +// Anticheat flags +enum { + AC_DIGGING = 0x01, + AC_INTERACTION = 0x02, + AC_MOVEMENT = 0x04 +}; + +constexpr const static FlagDesc flagdesc_anticheat[] = { + {"digging", AC_DIGGING}, + {"interaction", AC_INTERACTION}, + {"movement", AC_MOVEMENT}, + {NULL, 0} +}; + enum ClientDeletionReason { CDR_LEAVE, CDR_TIMEOUT, @@ -141,6 +158,25 @@ struct ClientInfo { std::string vers_string, lang_code; }; +struct ModIPCStore { + ModIPCStore() = default; + ~ModIPCStore(); + + /// RW lock for this entire structure + std::shared_mutex mutex; + /// Signalled on any changes to the map contents + std::condition_variable_any condvar; + /** + * Map storing the data + * + * @note Do not store `nil` data in this map, instead remove the whole key. + */ + std::unordered_map> map; + + /// @note Should be called without holding the lock. + inline void signal() { condvar.notify_all(); } +}; + class Server : public con::PeerHandler, public MapEventReceiver, public IGameDef { @@ -166,9 +202,12 @@ public: // Actual processing is done in another thread. // This just checks if there was an error in that thread. void step(); + // This is run by ServerThread and does the actual processing void AsyncRunStep(float dtime, bool initial_step = false); void Receive(float timeout); + void yieldToOtherThreads(float dtime); + PlayerSAO* StageTwoClientInit(session_t peer_id); /* @@ -193,7 +232,6 @@ public: void handleCommand_ChatMessage(NetworkPacket* pkt); void handleCommand_Damage(NetworkPacket* pkt); void handleCommand_PlayerItem(NetworkPacket* pkt); - void handleCommand_Respawn(NetworkPacket* pkt); void handleCommand_Interact(NetworkPacket* pkt); void handleCommand_RemovedSounds(NetworkPacket* pkt); void handleCommand_NodeMetaFields(NetworkPacket* pkt); @@ -298,12 +336,14 @@ public: NodeDefManager* getWritableNodeDefManager(); IWritableCraftDefManager* getWritableCraftDefManager(); + // Not under envlock virtual const std::vector &getMods() const; virtual const ModSpec* getModSpec(const std::string &modname) const; virtual const SubgameSpec* getGameSpec() const { return &m_gamespec; } static std::string getBuiltinLuaPath(); virtual std::string getWorldPath() const { return m_path_world; } virtual std::string getModDataPath() const { return m_path_mod_data; } + virtual ModIPCStore *getModIPCStore() { return &m_ipcstore; } inline bool isSingleplayer() const { return m_simple_singleplayer_mode; } @@ -341,7 +381,7 @@ public: Address getPeerAddress(session_t peer_id); - void setLocalPlayerAnimations(RemotePlayer *player, v2s32 animation_frames[4], + void setLocalPlayerAnimations(RemotePlayer *player, v2f animation_frames[4], f32 frame_speed); void setPlayerEyeOffset(RemotePlayer *player, v3f first, v3f third, v3f third_front); @@ -356,15 +396,13 @@ public: void setLighting(RemotePlayer *player, const Lighting &lighting); - void RespawnPlayer(session_t peer_id); - /* con::PeerHandler implementation. */ - void peerAdded(con::Peer *peer); - void deletingPeer(con::Peer *peer, bool timeout); + void peerAdded(con::IPeer *peer); + void deletingPeer(con::IPeer *peer, bool timeout); void DenySudoAccess(session_t peer_id); void DenyAccess(session_t peer_id, AccessDeniedCode reason, - const std::string &custom_reason = "", bool reconnect = false); + std::string_view custom_reason = "", bool reconnect = false); void kickAllPlayers(AccessDeniedCode reason, const std::string &str_reason, bool reconnect); void acceptAuth(session_t peer_id, bool forSudoMode); @@ -427,8 +465,14 @@ public: // Bind address Address m_bind_addr; - // Environment mutex (envlock) - std::mutex m_env_mutex; + // Public helper for taking the envlock in a scope + class EnvAutoLock { + public: + EnvAutoLock(Server *server): m_lock(server->m_env_mutex) {} + + private: + std::lock_guard m_lock; + }; protected: /* Do not add more members here, this is only required to make unit tests work. */ @@ -485,9 +529,7 @@ private: void SendHP(session_t peer_id, u16 hp, bool effect); void SendBreath(session_t peer_id, u16 breath); void SendAccessDenied(session_t peer_id, AccessDeniedCode reason, - const std::string &custom_reason, bool reconnect = false); - void SendDeathscreen(session_t peer_id, bool set_camera_point_target, - v3f camera_point_target); + std::string_view custom_reason, bool reconnect = false); void SendItemDef(session_t peer_id, IItemDefManager *itemdef, u16 protocol_version); void SendNodeDef(session_t peer_id, const NodeDefManager *nodedef, u16 protocol_version); @@ -496,7 +538,7 @@ private: virtual void SendChatMessage(session_t peer_id, const ChatMessage &message); void SendTimeOfDay(session_t peer_id, u16 time, f32 time_speed); - void SendLocalPlayerAnimations(session_t peer_id, v2s32 animation_frames[4], + void SendLocalPlayerAnimations(session_t peer_id, v2f animation_frames[4], f32 animation_speed); void SendEyeOffset(session_t peer_id, v3f first, v3f third, v3f third_front); void SendPlayerPrivileges(session_t peer_id); @@ -600,11 +642,13 @@ private: */ PlayerSAO *emergePlayer(const char *name, session_t peer_id, u16 proto_version); - void handlePeerChanges(); - /* Variables */ + + // Environment mutex (envlock) + ordered_mutex m_env_mutex; + // World directory std::string m_path_world; std::string m_path_mod_data; @@ -637,7 +681,7 @@ private: ServerEnvironment *m_env = nullptr; // server connection - std::shared_ptr m_con; + std::shared_ptr m_con; // Ban checking BanManager *m_banmanager = nullptr; @@ -659,6 +703,8 @@ private: std::unordered_map server_translations; + ModIPCStore m_ipcstore; + /* Threads */ @@ -673,13 +719,6 @@ private: */ ClientInterface m_clients; - /* - Peer change queue. - Queues stuff from peerAdded() and deletingPeer() to - handlePeerChanges() - */ - std::queue m_peer_change_queue; - std::unordered_map m_formspec_state_data; /* diff --git a/src/server/activeobjectmgr.cpp b/src/server/activeobjectmgr.cpp index c6f1010ea..f0216d9e3 100644 --- a/src/server/activeobjectmgr.cpp +++ b/src/server/activeobjectmgr.cpp @@ -91,7 +91,7 @@ bool ActiveObjectMgr::registerObject(std::unique_ptr obj) return false; } - auto obj_id = obj->getId(); + auto obj_id = obj->getId(); m_active_objects.put(obj_id, std::move(obj)); auto new_size = m_active_objects.size(); diff --git a/src/server/clientiface.cpp b/src/server/clientiface.cpp index e5c07b3d8..0e5140f00 100644 --- a/src/server/clientiface.cpp +++ b/src/server/clientiface.cpp @@ -648,13 +648,14 @@ void RemoteClient::setLangCode(const std::string &code) m_lang_code = string_sanitize_ascii(code, 12); } -ClientInterface::ClientInterface(const std::shared_ptr & con) +ClientInterface::ClientInterface(const std::shared_ptr &con) : m_con(con), m_env(nullptr) { } + ClientInterface::~ClientInterface() { /* diff --git a/src/server/clientiface.h b/src/server/clientiface.h index ac41b00ca..5b20d8f70 100644 --- a/src/server/clientiface.h +++ b/src/server/clientiface.h @@ -33,6 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include +#include #include #include #include @@ -126,7 +127,7 @@ class EmergeManager; | TOCLIENT_INVENTORY | | | | | TOCLIENT_HP (opt) | \-----------------/ | | TOCLIENT_BREATH | | -| TOCLIENT_DEATHSCREEN | | +| TOCLIENT_DEATHSCREEN_LEGACY | | +-----------------------------+ | | | v | @@ -168,7 +169,7 @@ class EmergeManager; */ namespace con { - class Connection; + class IConnection; } @@ -321,9 +322,6 @@ public: void setPendingSerializationVersion(u8 version) { m_pending_serialization_version = version; } - void setDeployedCompressionMode(u16 byteFlag) - { m_deployed_compression = byteFlag; } - void confirmSerializationVersion() { serialization_version = m_pending_serialization_version; } @@ -449,8 +447,6 @@ private: std::string m_full_version = "unknown"; - u16 m_deployed_compression = 0; - /* time this client was created */ @@ -464,7 +460,7 @@ public: friend class Server; - ClientInterface(const std::shared_ptr &con); + ClientInterface(const std::shared_ptr &con); ~ClientInterface(); /* run sync step */ @@ -543,7 +539,7 @@ private: void UpdatePlayerList(); // Connection - std::shared_ptr m_con; + std::shared_ptr m_con; std::recursive_mutex m_clients_mutex; // Connected clients (behind the con mutex) RemoteClientMap m_clients; diff --git a/src/server/player_sao.cpp b/src/server/player_sao.cpp index 4abb1f920..57b39d403 100644 --- a/src/server/player_sao.cpp +++ b/src/server/player_sao.cpp @@ -156,7 +156,10 @@ void PlayerSAO::getStaticData(std::string * result) const void PlayerSAO::step(float dtime, bool send_recommended) { - if (!isImmortal() && m_drowning_interval.step(dtime, 2.0f)) { + bool not_immortal = !isImmortal(); + + if (not_immortal && m_flags.drowning + && m_drowning_interval.step(dtime, 2.0f)) { // Get nose/mouth position, approximate with eye position v3s16 p = floatToInt(getEyePosition(), BS); MapNode n = m_env->getMap().getNode(p); @@ -174,7 +177,8 @@ void PlayerSAO::step(float dtime, bool send_recommended) } } - if (m_breathing_interval.step(dtime, 0.5f) && !isImmortal()) { + if (not_immortal && m_flags.breathing + && m_breathing_interval.step(dtime, 0.5f)) { // Get nose/mouth position, approximate with eye position v3s16 p = floatToInt(getEyePosition(), BS); MapNode n = m_env->getMap().getNode(p); @@ -185,7 +189,8 @@ void PlayerSAO::step(float dtime, bool send_recommended) setBreath(m_breath + 1); } - if (!isImmortal() && m_node_hurt_interval.step(dtime, 1.0f)) { + if (not_immortal && m_flags.node_damage + && m_node_hurt_interval.step(dtime, 1.0f)) { u32 damage_per_second = 0; std::string nodename; v3s16 node_pos; @@ -514,12 +519,13 @@ void PlayerSAO::rightClick(ServerActiveObject *clicker) void PlayerSAO::setHP(s32 target_hp, const PlayerHPChangeReason &reason, bool from_client) { - target_hp = rangelim(target_hp, 0, U16_MAX); - - if (target_hp == m_hp) + if (target_hp == m_hp || (m_hp == 0 && target_hp < 0)) return; // Nothing to do - s32 hp_change = m_env->getScriptIface()->on_player_hpchange(this, target_hp - (s32)m_hp, reason); + // Protect against overflow. + s32 hp_change = std::max((s64)target_hp - (s64)m_hp, S32_MIN); + + hp_change = m_env->getScriptIface()->on_player_hpchange(this, hp_change, reason); hp_change = std::min(hp_change, U16_MAX); // Protect against overflow s32 hp = (s32)m_hp + hp_change; @@ -553,6 +559,21 @@ void PlayerSAO::setBreath(const u16 breath, bool send) m_env->getGameDef()->SendPlayerBreath(this); } +void PlayerSAO::respawn() +{ + infostream << "PlayerSAO::respawn(): Player " << m_player->getName() + << " respawns" << std::endl; + + setHP(m_prop.hp_max, PlayerHPChangeReason(PlayerHPChangeReason::RESPAWN)); + setBreath(m_prop.breath_max); + + bool repositioned = m_env->getScriptIface()->on_respawnplayer(this); + if (!repositioned) { + // setPos will send the new position to client + setPos(m_env->getGameDef()->findSpawnPos()); + } +} + Inventory *PlayerSAO::getInventory() const { return m_player ? &m_player->inventory : nullptr; @@ -625,9 +646,12 @@ void PlayerSAO::setMaxSpeedOverride(const v3f &vel) bool PlayerSAO::checkMovementCheat() { + static thread_local const u32 anticheat_flags = + g_settings->getFlagStr("anticheat_flags", flagdesc_anticheat, nullptr); + if (m_is_singleplayer || isAttached() || - g_settings->getBool("disable_anticheat")) { + !(anticheat_flags & AC_MOVEMENT)) { m_last_good_position = m_base_position; return false; } @@ -708,6 +732,11 @@ bool PlayerSAO::checkMovementCheat() required_time = MYMAX(required_time, d_vert / s); } + static thread_local float anticheat_movement_tolerance = + std::max(g_settings->getFloat("anticheat_movement_tolerance"), 1.0f); + + required_time /= anticheat_movement_tolerance; + if (m_move_pool.grab(required_time)) { m_last_good_position = m_base_position; } else { diff --git a/src/server/player_sao.h b/src/server/player_sao.h index b26304589..487e37957 100644 --- a/src/server/player_sao.h +++ b/src/server/player_sao.h @@ -124,6 +124,7 @@ public: void setHPRaw(u16 hp) { m_hp = hp; } u16 getBreath() const { return m_breath; } void setBreath(const u16 breath, bool send = true); + void respawn(); /* Inventory interface @@ -228,6 +229,12 @@ private: SimpleMetadata m_meta; public: + struct { + bool breathing : 1; + bool drowning : 1; + bool node_damage : 1; + } m_flags = {true, true, true}; + bool m_physics_override_sent = false; }; diff --git a/src/server/serveractiveobject.h b/src/server/serveractiveobject.h index d734b8469..486b3b230 100644 --- a/src/server/serveractiveobject.h +++ b/src/server/serveractiveobject.h @@ -259,9 +259,6 @@ protected: virtual void onMarkedForDeactivation() {} virtual void onMarkedForRemoval() {} - virtual void onAttach(object_t parent_id) {} - virtual void onDetach(object_t parent_id) {} - ServerEnvironment *m_env; v3f m_base_position; std::unordered_set m_attached_particle_spawners; diff --git a/src/server/unit_sao.cpp b/src/server/unit_sao.cpp index d764b5d16..a4f547146 100644 --- a/src/server/unit_sao.cpp +++ b/src/server/unit_sao.cpp @@ -55,10 +55,7 @@ const ItemGroupList &UnitSAO::getArmorGroups() const void UnitSAO::setAnimation( v2f frame_range, float frame_speed, float frame_blend, bool frame_loop) { - if (std::tie(m_animation_range, m_animation_speed, m_animation_blend, - m_animation_loop) == - std::tie(frame_range, frame_speed, frame_blend, frame_loop)) - return; // no change + // Note: Always resend (even if parameters are unchanged) to restart animations. m_animation_range = frame_range; m_animation_speed = frame_speed; m_animation_blend = frame_blend; diff --git a/src/server/unit_sao.h b/src/server/unit_sao.h index c7f8c4aec..32b5dd30e 100644 --- a/src/server/unit_sao.h +++ b/src/server/unit_sao.h @@ -31,7 +31,7 @@ public: UnitSAO(ServerEnvironment *env, v3f pos); virtual ~UnitSAO() = default; - u16 getHP() const { return m_hp; } + u16 getHP() const override { return m_hp; } // Use a function, if isDead can be defined by other conditions bool isDead() const { return m_hp == 0; } @@ -59,39 +59,39 @@ public: { return itemgroup_get(getArmorGroups(), "immortal"); } - void setArmorGroups(const ItemGroupList &armor_groups); - const ItemGroupList &getArmorGroups() const; + void setArmorGroups(const ItemGroupList &armor_groups) override; + const ItemGroupList &getArmorGroups() const override; // Animation void setAnimation(v2f frame_range, float frame_speed, float frame_blend, - bool frame_loop); + bool frame_loop) override; void getAnimation(v2f *frame_range, float *frame_speed, float *frame_blend, - bool *frame_loop); - void setAnimationSpeed(float frame_speed); + bool *frame_loop) override; + void setAnimationSpeed(float frame_speed) override; // Bone position - void setBoneOverride(const std::string &bone, const BoneOverride &props); - BoneOverride getBoneOverride(const std::string &bone); + void setBoneOverride(const std::string &bone, const BoneOverride &props) override; + BoneOverride getBoneOverride(const std::string &bone) override; const std::unordered_map - &getBoneOverrides() const { return m_bone_override; }; + &getBoneOverrides() const override { return m_bone_override; }; // Attachments - ServerActiveObject *getParent() const; + ServerActiveObject *getParent() const override; inline bool isAttached() const { return m_attachment_parent_id != 0; } void setAttachment(object_t parent_id, const std::string &bone, v3f position, - v3f rotation, bool force_visible); + v3f rotation, bool force_visible) override; void getAttachment(object_t *parent_id, std::string *bone, v3f *position, - v3f *rotation, bool *force_visible) const; + v3f *rotation, bool *force_visible) const override; void clearChildAttachments() override; void addAttachmentChild(object_t child_id) override; void removeAttachmentChild(object_t child_id) override; - const std::unordered_set &getAttachmentChildIds() const { + const std::unordered_set &getAttachmentChildIds() const override { return m_attachment_child_ids; } // Object properties - ObjectProperties *accessObjectProperties(); - void notifyObjectPropertiesModified(); + ObjectProperties *accessObjectProperties() override; + void notifyObjectPropertiesModified() override; void sendOutdatedData(); // Update packets @@ -125,11 +125,11 @@ protected: object_t m_attachment_parent_id = 0; void clearAnyAttachments(); - virtual void onMarkedForDeactivation() { + virtual void onMarkedForDeactivation() override { ServerActiveObject::onMarkedForDeactivation(); clearAnyAttachments(); } - virtual void onMarkedForRemoval() { + virtual void onMarkedForRemoval() override { ServerActiveObject::onMarkedForRemoval(); clearAnyAttachments(); } diff --git a/src/serverenvironment.cpp b/src/serverenvironment.cpp index 24e4a587e..813184de1 100644 --- a/src/serverenvironment.cpp +++ b/src/serverenvironment.cpp @@ -77,11 +77,11 @@ ABMWithState::ABMWithState(ActiveBlockModifier *abm_): LBMManager */ -void LBMContentMapping::deleteContents() +LBMContentMapping::~LBMContentMapping() { - for (auto &it : lbm_list) { + map.clear(); + for (auto &it : lbm_list) delete it; - } } void LBMContentMapping::addLBM(LoadingBlockModifierDef *lbm_def, IGameDef *gamedef) @@ -90,29 +90,32 @@ void LBMContentMapping::addLBM(LoadingBlockModifierDef *lbm_def, IGameDef *gamed // Unknown names get added to the global NameIdMapping. const NodeDefManager *nodedef = gamedef->ndef(); + FATAL_ERROR_IF(CONTAINS(lbm_list, lbm_def), "Same LBM registered twice"); lbm_list.push_back(lbm_def); - for (const std::string &nodeTrigger: lbm_def->trigger_contents) { - std::vector c_ids; - bool found = nodedef->getIds(nodeTrigger, c_ids); + std::vector c_ids; + + for (const auto &node : lbm_def->trigger_contents) { + bool found = nodedef->getIds(node, c_ids); if (!found) { - content_t c_id = gamedef->allocateUnknownNodeId(nodeTrigger); + content_t c_id = gamedef->allocateUnknownNodeId(node); if (c_id == CONTENT_IGNORE) { // Seems it can't be allocated. - warningstream << "Could not internalize node name \"" << nodeTrigger + warningstream << "Could not internalize node name \"" << node << "\" while loading LBM \"" << lbm_def->name << "\"." << std::endl; continue; } c_ids.push_back(c_id); } - - for (content_t c_id : c_ids) { - map[c_id].push_back(lbm_def); - } } + + SORT_AND_UNIQUE(c_ids); + + for (content_t c_id : c_ids) + map[c_id].push_back(lbm_def); } -const std::vector * +const LBMContentMapping::lbm_vector * LBMContentMapping::lookup(content_t c) const { lbm_map::const_iterator it = map.find(c); @@ -130,9 +133,7 @@ LBMManager::~LBMManager() delete m_lbm_def.second; } - for (auto &it : m_lbm_lookup) { - (it.second).deleteContents(); - } + m_lbm_lookup.clear(); } void LBMManager::addLBMDef(LoadingBlockModifierDef *lbm_def) @@ -236,7 +237,7 @@ std::string LBMManager::createIntroductionTimesString() std::ostringstream oss; for (const auto &it : m_lbm_lookup) { u32 time = it.first; - const std::vector &lbm_list = it.second.lbm_list; + auto &lbm_list = it.second.getList(); for (const auto &lbm_def : lbm_list) { // Don't add if the LBM runs at every load, // then introducement time is hardcoded @@ -255,41 +256,74 @@ void LBMManager::applyLBMs(ServerEnvironment *env, MapBlock *block, // Precondition, we need m_lbm_lookup to be initialized FATAL_ERROR_IF(!m_query_mode, "attempted to query on non fully set up LBMManager"); - v3s16 pos_of_block = block->getPosRelative(); - v3s16 pos; - MapNode n; - content_t c; - auto it = getLBMsIntroducedAfter(stamp); - for (; it != m_lbm_lookup.end(); ++it) { - // Cache previous version to speedup lookup which has a very high performance - // penalty on each call + + // Collect a list of all LBMs and associated positions + struct LBMToRun { + std::unordered_set p; // node positions + std::unordered_set l; + }; + std::unordered_map to_run; + + // Note: the iteration count of this outer loop is typically very low, so it's ok. + for (auto it = getLBMsIntroducedAfter(stamp); it != m_lbm_lookup.end(); ++it) { + v3s16 pos; + content_t c; + + // Cache previous lookups since it has a high performance penalty. content_t previous_c = CONTENT_IGNORE; - const std::vector *lbm_list = nullptr; + const LBMContentMapping::lbm_vector *lbm_list = nullptr; + LBMToRun *batch = nullptr; for (pos.Z = 0; pos.Z < MAP_BLOCKSIZE; pos.Z++) for (pos.Y = 0; pos.Y < MAP_BLOCKSIZE; pos.Y++) for (pos.X = 0; pos.X < MAP_BLOCKSIZE; pos.X++) { - n = block->getNodeNoCheck(pos); - c = n.getContent(); + c = block->getNodeNoCheck(pos).getContent(); - // If content_t are not matching perform an LBM lookup + bool c_changed = false; if (previous_c != c) { + c_changed = true; lbm_list = it->second.lookup(c); + batch = &to_run[c]; previous_c = c; } if (!lbm_list) continue; - for (auto lbmdef : *lbm_list) { - lbmdef->trigger(env, pos + pos_of_block, n, dtime_s); - if (block->isOrphan()) - return; - n = block->getNodeNoCheck(pos); - if (n.getContent() != c) - break; // The node was changed and the LBMs no longer apply + batch->p.insert(pos); + if (c_changed) { + batch->l.insert(lbm_list->begin(), lbm_list->end()); + } else { + // we were here before so the list must be filled + assert(!batch->l.empty()); } } } + + // Actually run them + bool first = true; + for (auto &[c, batch] : to_run) { + for (auto &lbm_def : batch.l) { + if (!first) { + // The fun part: since any LBM call can change the nodes inside of he + // block, we have to recheck the positions to see if the wanted node + // is still there. + // Note that we don't rescan the whole block, we don't want to include new changes. + for (auto it2 = batch.p.begin(); it2 != batch.p.end(); ) { + if (block->getNodeNoCheck(*it2).getContent() != c) + it2 = batch.p.erase(it2); + else + ++it2; + } + } + first = false; + + if (batch.p.empty()) + break; + lbm_def->trigger(env, block, batch.p, dtime_s); + if (block->isOrphan()) + return; + } + } } /* @@ -792,11 +826,10 @@ void ServerEnvironment::loadDefaultMeta() struct ActiveABM { ActiveBlockModifier *abm; - int chance; std::vector required_neighbors; - bool check_required_neighbors; // false if required_neighbors is known to be empty - s16 min_y; - s16 max_y; + std::vector without_neighbors; + int chance; + s16 min_y, max_y; }; #define CONTENT_TYPE_CACHE_MAX 64 @@ -812,16 +845,16 @@ public: bool use_timers): m_env(env) { - if(dtime_s < 0.001) + if (dtime_s < 0.001f) return; const NodeDefManager *ndef = env->getGameDef()->ndef(); for (ABMWithState &abmws : abms) { ActiveBlockModifier *abm = abmws.abm; float trigger_interval = abm->getTriggerInterval(); - if(trigger_interval < 0.001) - trigger_interval = 0.001; + if (trigger_interval < 0.001f) + trigger_interval = 0.001f; float actual_interval = dtime_s; - if(use_timers){ + if (use_timers) { abmws.timer += dtime_s; if(abmws.timer < trigger_interval) continue; @@ -831,6 +864,7 @@ public: float chance = abm->getTriggerChance(); if (chance == 0) chance = 1; + ActiveABM aabm; aabm.abm = abm; if (abm->getSimpleCatchUp()) { @@ -848,25 +882,25 @@ public: aabm.max_y = abm->getMaxY(); // Trigger neighbors - const std::vector &required_neighbors_s = - abm->getRequiredNeighbors(); - for (const std::string &required_neighbor_s : required_neighbors_s) { - ndef->getIds(required_neighbor_s, aabm.required_neighbors); - } - aabm.check_required_neighbors = !required_neighbors_s.empty(); + for (const auto &s : abm->getRequiredNeighbors()) + ndef->getIds(s, aabm.required_neighbors); + SORT_AND_UNIQUE(aabm.required_neighbors); + + for (const auto &s : abm->getWithoutNeighbors()) + ndef->getIds(s, aabm.without_neighbors); + SORT_AND_UNIQUE(aabm.without_neighbors); // Trigger contents - const std::vector &contents_s = abm->getTriggerContents(); - for (const std::string &content_s : contents_s) { - std::vector ids; - ndef->getIds(content_s, ids); - for (content_t c : ids) { - if (c >= m_aabms.size()) - m_aabms.resize(c + 256, NULL); - if (!m_aabms[c]) - m_aabms[c] = new std::vector; - m_aabms[c]->push_back(aabm); - } + std::vector ids; + for (const auto &s : abm->getTriggerContents()) + ndef->getIds(s, ids); + SORT_AND_UNIQUE(ids); + for (content_t c : ids) { + if (c >= m_aabms.size()) + m_aabms.resize(c + 256, nullptr); + if (!m_aabms[c]) + m_aabms[c] = new std::vector; + m_aabms[c]->push_back(aabm); } } } @@ -967,8 +1001,11 @@ public: continue; // Check neighbors - if (aabm.check_required_neighbors) { + const bool check_required_neighbors = !aabm.required_neighbors.empty(); + const bool check_without_neighbors = !aabm.without_neighbors.empty(); + if (check_required_neighbors || check_without_neighbors) { v3s16 p1; + bool have_required = false; for(p1.X = p0.X-1; p1.X <= p0.X+1; p1.X++) for(p1.Y = p0.Y-1; p1.Y <= p0.Y+1; p1.Y++) for(p1.Z = p0.Z-1; p1.Z <= p0.Z+1; p1.Z++) @@ -986,12 +1023,25 @@ public: MapNode n = map->getNode(p1 + block->getPosRelative()); c = n.getContent(); } - if (CONTAINS(aabm.required_neighbors, c)) - goto neighbor_found; + if (check_required_neighbors && !have_required) { + if (CONTAINS(aabm.required_neighbors, c)) { + if (!check_without_neighbors) + goto neighbor_found; + have_required = true; + } + } + if (check_without_neighbors) { + if (CONTAINS(aabm.without_neighbors, c)) + goto neighbor_invalid; + } } + if (have_required || !check_required_neighbors) + goto neighbor_found; // No required neighbor found + neighbor_invalid: continue; } + neighbor_found: abms_run++; diff --git a/src/serverenvironment.h b/src/serverenvironment.h index 7a388d21c..0b00fac91 100644 --- a/src/serverenvironment.h +++ b/src/serverenvironment.h @@ -63,6 +63,9 @@ public: // Set of required neighbors (trigger doesn't happen if none are found) // Empty = do not check neighbors virtual const std::vector &getRequiredNeighbors() const = 0; + // Set of without neighbors (trigger doesn't happen if any are found) + // Empty = do not check neighbors + virtual const std::vector &getWithoutNeighbors() const = 0; // Trigger interval in seconds virtual float getTriggerInterval() = 0; // Random chance of (1 / return value), 0 is disallowed @@ -90,29 +93,40 @@ struct ABMWithState struct LoadingBlockModifierDef { // Set of contents to trigger on - std::set trigger_contents; + std::vector trigger_contents; std::string name; bool run_at_every_load = false; virtual ~LoadingBlockModifierDef() = default; - virtual void trigger(ServerEnvironment *env, v3s16 p, - MapNode n, float dtime_s) {}; + /// @brief Called to invoke LBM + /// @param env environment + /// @param block the block in question + /// @param positions set of node positions (block-relative!) + /// @param dtime_s game time since last deactivation + virtual void trigger(ServerEnvironment *env, MapBlock *block, + const std::unordered_set &positions, float dtime_s) {}; }; -struct LBMContentMapping +class LBMContentMapping { - typedef std::unordered_map> lbm_map; - lbm_map map; +public: + typedef std::vector lbm_vector; + typedef std::unordered_map lbm_map; - std::vector lbm_list; - - // Needs to be separate method (not inside destructor), - // because the LBMContentMapping may be copied and destructed - // many times during operation in the lbm_lookup_map. - void deleteContents(); + LBMContentMapping() = default; void addLBM(LoadingBlockModifierDef *lbm_def, IGameDef *gamedef); const lbm_map::mapped_type *lookup(content_t c) const; + const lbm_vector &getList() const { return lbm_list; } + + // This struct owns the LBM pointers. + ~LBMContentMapping(); + DISABLE_CLASS_COPY(LBMContentMapping); + ALLOW_CLASS_MOVE(LBMContentMapping); + +private: + lbm_vector lbm_list; + lbm_map map; }; class LBMManager diff --git a/src/servermap.cpp b/src/servermap.cpp index eeabd74f9..f57e5b5e4 100644 --- a/src/servermap.cpp +++ b/src/servermap.cpp @@ -52,7 +52,19 @@ with this program; if not, write to the Free Software Foundation, Inc., #endif /* - ServerMap + Helpers +*/ + +void MapDatabaseAccessor::loadBlock(v3s16 blockpos, std::string &ret) +{ + ret.clear(); + dbase->loadBlock(blockpos, &ret); + if (ret.empty() && dbase_ro) + dbase_ro->loadBlock(blockpos, &ret); +} + +/* + ServerMap */ ServerMap::ServerMap(const std::string &savedir, IGameDef *gamedef, @@ -67,7 +79,7 @@ ServerMap::ServerMap(const std::string &savedir, IGameDef *gamedef, emerge->map_settings_mgr = &settings_mgr; /* - Try to load map; if not found, create a new one. + Try to open map; if not found, create a new one. */ // Determine which database backend to use @@ -79,10 +91,10 @@ ServerMap::ServerMap(const std::string &savedir, IGameDef *gamedef, conf.set("backend", "sqlite3"); } std::string backend = conf.get("backend"); - dbase = createDatabase(backend, savedir, conf); + m_db.dbase = createDatabase(backend, savedir, conf); if (conf.exists("readonly_backend")) { std::string readonly_dir = savedir + DIR_DELIM + "readonly"; - dbase_ro = createDatabase(conf.get("readonly_backend"), readonly_dir, conf); + m_db.dbase_ro = createDatabase(conf.get("readonly_backend"), readonly_dir, conf); } if (!conf.updateConfigFile(conf_path.c_str())) errorstream << "ServerMap::ServerMap(): Failed to update world.mt!" << std::endl; @@ -90,6 +102,9 @@ ServerMap::ServerMap(const std::string &savedir, IGameDef *gamedef, m_savedir = savedir; m_map_saving_enabled = false; + // Inform EmergeManager of db handles + m_emerge->initMap(&m_db); + m_save_time_counter = mb->addCounter( "minetest_map_save_time", "Time spent saving blocks (in microseconds)"); m_save_count_counter = mb->addCounter( @@ -159,11 +174,15 @@ ServerMap::~ServerMap() << ", exception: " << e.what() << std::endl; } - /* - Close database if it was opened - */ - delete dbase; - delete dbase_ro; + m_emerge->resetMap(); + + { + MutexAutoLock dblock(m_db.mutex); + delete m_db.dbase; + m_db.dbase = nullptr; + delete m_db.dbase_ro; + m_db.dbase_ro = nullptr; + } deleteDetachedBlocks(); } @@ -547,9 +566,10 @@ void ServerMap::save(ModifiedState save_level) void ServerMap::listAllLoadableBlocks(std::vector &dst) { - dbase->listAllLoadableBlocks(dst); - if (dbase_ro) - dbase_ro->listAllLoadableBlocks(dst); + MutexAutoLock dblock(m_db.mutex); + m_db.dbase->listAllLoadableBlocks(dst); + if (m_db.dbase_ro) + m_db.dbase_ro->listAllLoadableBlocks(dst); } void ServerMap::listAllLoadedBlocks(std::vector &dst) @@ -597,17 +617,21 @@ MapDatabase *ServerMap::createDatabase( void ServerMap::beginSave() { - dbase->beginSave(); + MutexAutoLock dblock(m_db.mutex); + m_db.dbase->beginSave(); } void ServerMap::endSave() { - dbase->endSave(); + MutexAutoLock dblock(m_db.mutex); + m_db.dbase->endSave(); } bool ServerMap::saveBlock(MapBlock *block) { - return saveBlock(block, dbase, m_map_compression_level); + // FIXME: serialization happens under mutex + MutexAutoLock dblock(m_db.mutex); + return saveBlock(block, m_db.dbase, m_map_compression_level); } bool ServerMap::saveBlock(MapBlock *block, MapDatabase *db, int compression_level) @@ -634,18 +658,27 @@ bool ServerMap::saveBlock(MapBlock *block, MapDatabase *db, int compression_leve return ret; } -void ServerMap::loadBlock(std::string *blob, v3s16 p3d, MapSector *sector, bool save_after_load) +void ServerMap::deSerializeBlock(MapBlock *block, std::istream &is) { + ScopeProfiler sp(g_profiler, "ServerMap: deSer block", SPT_AVG, PRECISION_MICRO); + + u8 version = readU8(is); + if (is.fail()) + throw SerializationError("Failed to read MapBlock version"); + + block->deSerialize(is, version, true); +} + +MapBlock *ServerMap::loadBlock(const std::string &blob, v3s16 p3d, bool save_after_load) +{ + ScopeProfiler sp(g_profiler, "ServerMap: load block", SPT_AVG, PRECISION_MICRO); + MapBlock *block = nullptr; + bool created_new = false; + try { - std::istringstream is(*blob, std::ios_base::binary); + v2s16 p2d(p3d.X, p3d.Z); + MapSector *sector = createSector(p2d); - u8 version = readU8(is); - - if(is.fail()) - throw SerializationError("ServerMap::loadBlock(): Failed" - " to read MapBlock version"); - - MapBlock *block = nullptr; std::unique_ptr block_created_new; block = sector->getBlockNoCreateNoEx(p3d.Y); if (!block) { @@ -654,31 +687,16 @@ void ServerMap::loadBlock(std::string *blob, v3s16 p3d, MapSector *sector, bool } { - ScopeProfiler sp(g_profiler, "ServerMap: deSer block", SPT_AVG, PRECISION_MICRO); - block->deSerialize(is, version, true); + std::istringstream iss(blob, std::ios_base::binary); + deSerializeBlock(block, iss); } // If it's a new block, insert it to the map if (block_created_new) { sector->insertBlock(std::move(block_created_new)); - ReflowScan scanner(this, m_emerge->ndef); - scanner.scan(block, &m_transforming_liquid); + created_new = true; } - - /* - Save blocks loaded in old format in new format - */ - - //if(version < SER_FMT_VER_HIGHEST_READ || save_after_load) - // Only save if asked to; no need to update version - if(save_after_load) - saveBlock(block); - - // We just loaded it from, so it's up-to-date. - block->resetModified(); - } - catch(SerializationError &e) - { + } catch (SerializationError &e) { errorstream<<"Invalid block data in database" <<" ("<ndef); + scanner.scan(block, &m_transforming_liquid); - std::string ret; - dbase->loadBlock(blockpos, &ret); - if (!ret.empty()) { - loadBlock(&ret, blockpos, createSector(p2d), false); - } else if (dbase_ro) { - dbase_ro->loadBlock(blockpos, &ret); - if (!ret.empty()) { - loadBlock(&ret, blockpos, createSector(p2d), false); - } - } else { - return NULL; - } - - MapBlock *block = getBlockNoCreateNoEx(blockpos); - if (created_new && (block != NULL)) { std::map modified_blocks; // Fix lighting if necessary voxalgo::update_block_border_lighting(this, block, modified_blocks); if (!modified_blocks.empty()) { - //Modified lighting, send event MapEditEvent event; event.type = MEET_OTHER; event.setModifiedBlocks(modified_blocks); dispatchEvent(event); } } + + if (save_after_load) + saveBlock(block); + + // We just loaded it, so it's up-to-date. + block->resetModified(); + return block; } +MapBlock* ServerMap::loadBlock(v3s16 blockpos) +{ + std::string data; + { + ScopeProfiler sp(g_profiler, "ServerMap: load block - sync (sum)"); + MutexAutoLock dblock(m_db.mutex); + m_db.loadBlock(blockpos, data); + } + + if (!data.empty()) + return loadBlock(data, blockpos); + return getBlockNoCreateNoEx(blockpos); +} + bool ServerMap::deleteBlock(v3s16 blockpos) { - if (!dbase->deleteBlock(blockpos)) + MutexAutoLock dblock(m_db.mutex); + if (!m_db.dbase->deleteBlock(blockpos)) return false; MapBlock *block = getBlockNoCreateNoEx(blockpos); diff --git a/src/servermap.h b/src/servermap.h index 7a8a84b9b..3a2102668 100644 --- a/src/servermap.h +++ b/src/servermap.h @@ -33,9 +33,22 @@ class IRollbackManager; class EmergeManager; class ServerEnvironment; struct BlockMakeData; - class MetricsBackend; +// TODO: this could wrap all calls to MapDatabase, including locking +struct MapDatabaseAccessor { + /// Lock, to be taken for any operation + std::mutex mutex; + /// Main database + MapDatabase *dbase = nullptr; + /// Fallback database for read operations + MapDatabase *dbase_ro = nullptr; + + /// Load a block, taking dbase_ro into account. + /// @note call locked + void loadBlock(v3s16 blockpos, std::string &ret); +}; + /* ServerMap @@ -75,7 +88,7 @@ public: MapBlock *createBlock(v3s16 p); /* - Forcefully get a block from somewhere. + Forcefully get a block from somewhere (blocking!). - Memory - Load from disk - Create blank filled with CONTENT_IGNORE @@ -114,9 +127,16 @@ public: bool saveBlock(MapBlock *block) override; static bool saveBlock(MapBlock *block, MapDatabase *db, int compression_level = -1); - MapBlock* loadBlock(v3s16 p); - // Database version - void loadBlock(std::string *blob, v3s16 p3d, MapSector *sector, bool save_after_load=false); + + // Load block in a synchronous fashion + MapBlock *loadBlock(v3s16 p); + /// Load a block that was already read from disk. Used by EmergeManager. + /// @return non-null block (but can be blank) + MapBlock *loadBlock(const std::string &blob, v3s16 p, bool save_after_load=false); + + // Helper for deserializing blocks from disk + // @throws SerializationError + static void deSerializeBlock(MapBlock *block, std::istream &is); // Blocks are removed from the map but not deleted from memory until // deleteDetachedBlocks() is called, since pointers to them may still exist @@ -185,8 +205,8 @@ private: This is reset to false when written on disk. */ bool m_map_metadata_changed = true; - MapDatabase *dbase = nullptr; - MapDatabase *dbase_ro = nullptr; + + MapDatabaseAccessor m_db; // Map metrics MetricGaugePtr m_loaded_blocks_gauge; diff --git a/src/skyparams.h b/src/skyparams.h index a4d0fadac..52a15810b 100644 --- a/src/skyparams.h +++ b/src/skyparams.h @@ -46,7 +46,7 @@ struct SkyboxParams float body_orbit_tilt { INVALID_SKYBOX_TILT }; s16 fog_distance { -1 }; float fog_start { -1.0f }; - video::SColor fog_color; // override, only used if alpha > 0 + video::SColor fog_color { 0 }; // override, only used if alpha > 0 }; struct SunParams @@ -81,6 +81,7 @@ struct CloudParams float density; video::SColor color_bright; video::SColor color_ambient; + video::SColor color_shadow; float thickness; float height; v2f speed; @@ -160,6 +161,7 @@ public: clouds.density = 0.4f; clouds.color_bright = video::SColor(229, 240, 240, 255); clouds.color_ambient = video::SColor(255, 0, 0, 0); + clouds.color_shadow = video::SColor(255, 204, 204, 204); clouds.thickness = 16.0f; clouds.height = 120; clouds.speed = v2f(0.0f, -2.0f); diff --git a/src/terminal_chat_console.h b/src/terminal_chat_console.h index 825c76ef4..7ce2c5c2b 100644 --- a/src/terminal_chat_console.h +++ b/src/terminal_chat_console.h @@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "threading/thread.h" #include "util/container.h" #include "log.h" +#include "log_internal.h" #include #include @@ -32,14 +33,14 @@ struct ChatInterface; class TermLogOutput : public ILogOutput { public: - void logRaw(LogLevel lev, const std::string &line) + void logRaw(LogLevel lev, std::string_view line) { - queue.push_back(std::make_pair(lev, line)); + queue.push_back(std::make_pair(lev, std::string(line))); } virtual void log(LogLevel lev, const std::string &combined, const std::string &time, const std::string &thread_name, - const std::string &payload_text) + std::string_view payload_text) { std::ostringstream os(std::ios_base::binary); os << time << ": [" << thread_name << "] " << payload_text; diff --git a/src/texture_override.cpp b/src/texture_override.cpp index 1b8b4671d..e8386afe2 100644 --- a/src/texture_override.cpp +++ b/src/texture_override.cpp @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "util/string.h" #include #include +#include #define override_cast static_cast diff --git a/src/threading/event.cpp b/src/threading/event.cpp index 885e732c8..1b0c5ac07 100644 --- a/src/threading/event.cpp +++ b/src/threading/event.cpp @@ -28,7 +28,7 @@ DEALINGS IN THE SOFTWARE. void Event::wait() { - MutexAutoLock lock(mutex); + std::unique_lock lock(mutex); while (!notified) { cv.wait(lock); } @@ -38,7 +38,9 @@ void Event::wait() void Event::signal() { - MutexAutoLock lock(mutex); - notified = true; + { + std::lock_guard lock(mutex); + notified = true; + } cv.notify_one(); } diff --git a/src/threading/mutex_auto_lock.h b/src/threading/mutex_auto_lock.h index c809ff8f5..9a2522557 100644 --- a/src/threading/mutex_auto_lock.h +++ b/src/threading/mutex_auto_lock.h @@ -26,5 +26,8 @@ DEALINGS IN THE SOFTWARE. #pragma once #include -using MutexAutoLock = std::unique_lock; -using RecursiveMutexAutoLock = std::unique_lock; + +/// @deprecated use std::lock_guard directly +using MutexAutoLock = std::lock_guard; +/// @deprecated use std::lock_guard directly +using RecursiveMutexAutoLock = std::lock_guard; diff --git a/src/threading/ordered_mutex.h b/src/threading/ordered_mutex.h new file mode 100644 index 000000000..f7fb4d309 --- /dev/null +++ b/src/threading/ordered_mutex.h @@ -0,0 +1,46 @@ +// Minetest +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include + +/* + Fair mutex based on ticketing approach. + Satisfies `Mutex` C++11 requirements. +*/ +class ordered_mutex { +public: + ordered_mutex() : next_ticket(0), counter(0) {} + + void lock() + { + std::unique_lock autolock(cv_lock); + const auto ticket = next_ticket++; + cv.wait(autolock, [&] { return counter == ticket; }); + } + + bool try_lock() + { + std::lock_guard autolock(cv_lock); + if (counter != next_ticket) + return false; + next_ticket++; + return true; + } + + void unlock() + { + { + std::lock_guard autolock(cv_lock); + counter++; + } + cv.notify_all(); // intentionally outside lock + } + +private: + std::condition_variable cv; + std::mutex cv_lock; + uint_fast32_t next_ticket, counter; +}; diff --git a/src/threading/thread.cpp b/src/threading/thread.cpp index 21143f231..a4405287d 100644 --- a/src/threading/thread.cpp +++ b/src/threading/thread.cpp @@ -25,7 +25,7 @@ DEALINGS IN THE SOFTWARE. #include "threading/thread.h" #include "threading/mutex_auto_lock.h" -#include "log.h" +#include "log_internal.h" #include "porting.h" // for setName @@ -117,7 +117,7 @@ bool Thread::start() // The mutex may already be locked if the thread is being restarted // FIXME: what if this fails, or if already locked by same thread? - MutexAutoLock sf_lock(m_start_finished_mutex, std::try_to_lock); + std::unique_lock sf_lock(m_start_finished_mutex, std::try_to_lock); try { m_thread_obj = new std::thread(threadProc, this); @@ -189,7 +189,7 @@ void Thread::threadProc(Thread *thr) // Wait for the thread that started this one to finish initializing the // thread handle so that getThreadId/getThreadHandle will work. - MutexAutoLock sf_lock(thr->m_start_finished_mutex); + std::unique_lock sf_lock(thr->m_start_finished_mutex); thr->m_retval = thr->run(); diff --git a/src/tool.cpp b/src/tool.cpp index 9df69eccd..0cb1724de 100644 --- a/src/tool.cpp +++ b/src/tool.cpp @@ -44,18 +44,25 @@ void ToolGroupCap::toJson(Json::Value &object) const void ToolGroupCap::fromJson(const Json::Value &json) { - if (json.isObject()) { - if (json["maxlevel"].isInt()) - maxlevel = json["maxlevel"].asInt(); - if (json["uses"].isInt()) - uses = json["uses"].asInt(); - const Json::Value ×_object = json["times"]; - if (times_object.isArray()) { - Json::ArrayIndex size = times_object.size(); - for (Json::ArrayIndex i = 0; i < size; ++i) - if (times_object[i].isDouble()) - times[i] = times_object[i].asFloat(); - } + if (!json.isObject()) + return; + + if (json["maxlevel"].isInt()) + maxlevel = json["maxlevel"].asInt(); + + if (json["uses"].isInt()) + uses = json["uses"].asInt(); + + const Json::Value ×_object = json["times"]; + + if (!times_object.isArray()) + return; + + Json::ArrayIndex size = times_object.size(); + + for (Json::ArrayIndex i = 0; i < size; ++i) { + if (times_object[i].isDouble()) + times[i] = times_object[i].asFloat(); } } @@ -65,9 +72,11 @@ void ToolCapabilities::serialize(std::ostream &os, u16 protocol_version) const writeU8(os, 5); else writeU8(os, 4); // proto == 37 + writeF32(os, full_punch_interval); writeS16(os, max_drop_level); writeU32(os, groupcaps.size()); + for (const auto &groupcap : groupcaps) { const std::string *name = &groupcap.first; const ToolGroupCap *cap = &groupcap.second; @@ -102,6 +111,7 @@ void ToolCapabilities::deSerialize(std::istream &is) max_drop_level = readS16(is); groupcaps.clear(); u32 groupcaps_size = readU32(is); + for (u32 i = 0; i < groupcaps_size; i++) { std::string name = deSerializeString16(is); ToolGroupCap cap; @@ -135,15 +145,19 @@ void ToolCapabilities::serializeJson(std::ostream &os) const root["punch_attack_uses"] = punch_attack_uses; Json::Value groupcaps_object; + for (const auto &groupcap : groupcaps) { groupcap.second.toJson(groupcaps_object[groupcap.first]); } + root["groupcaps"] = std::move(groupcaps_object); Json::Value damage_groups_object; + for (const auto &damagegroup : damageGroups) { damage_groups_object[damagegroup.first] = damagegroup.second; } + root["damage_groups"] = std::move(damage_groups_object); fastWriteJson(root, os); @@ -153,36 +167,44 @@ void ToolCapabilities::deserializeJson(std::istream &is) { Json::Value root; is >> root; - if (root.isObject()) { - if (root["full_punch_interval"].isDouble()) - full_punch_interval = root["full_punch_interval"].asFloat(); - if (root["max_drop_level"].isInt()) - max_drop_level = root["max_drop_level"].asInt(); - if (root["punch_attack_uses"].isInt()) - punch_attack_uses = root["punch_attack_uses"].asInt(); - Json::Value &groupcaps_object = root["groupcaps"]; - if (groupcaps_object.isObject()) { - Json::ValueIterator gciter; - for (gciter = groupcaps_object.begin(); - gciter != groupcaps_object.end(); ++gciter) { - ToolGroupCap groupcap; - groupcap.fromJson(*gciter); - groupcaps[gciter.key().asString()] = groupcap; - } - } + if (!root.isObject()) + return; - Json::Value &damage_groups_object = root["damage_groups"]; - if (damage_groups_object.isObject()) { - Json::ValueIterator dgiter; - for (dgiter = damage_groups_object.begin(); - dgiter != damage_groups_object.end(); ++dgiter) { - Json::Value &value = *dgiter; - if (value.isInt()) - damageGroups[dgiter.key().asString()] = - value.asInt(); - } - } + if (root["full_punch_interval"].isDouble()) + full_punch_interval = root["full_punch_interval"].asFloat(); + + if (root["max_drop_level"].isInt()) + max_drop_level = root["max_drop_level"].asInt(); + + if (root["punch_attack_uses"].isInt()) + punch_attack_uses = root["punch_attack_uses"].asInt(); + + deserializeJsonGroupcaps(root["groupcaps"]); + deserializeJsonDamageGroups(root["damage_groups"]); +} + +void ToolCapabilities::deserializeJsonGroupcaps(Json::Value &json) +{ + if (!json.isObject()) + return; + + for (Json::ValueIterator iter = json.begin(); iter != json.end(); ++iter) { + ToolGroupCap value; + value.fromJson(*iter); + groupcaps[iter.key().asString()] = value; + } +} + +void ToolCapabilities::deserializeJsonDamageGroups(Json::Value &json) +{ + if (!json.isObject()) + return; + + for (Json::ValueIterator iter = json.begin(); iter != json.end(); ++iter) { + Json::Value &value = *iter; + if (value.isInt()) + damageGroups[iter.key().asString()] = value.asInt(); } } @@ -362,9 +384,8 @@ u32 calculateResultWear(const u32 uses, const u16 initial_wear) player. */ u16 wear_extra_at = blocks_normal * wear_normal; - if (initial_wear >= wear_extra_at) { + if (initial_wear >= wear_extra_at) wear_extra = 1; - } } result_wear = wear_normal + wear_extra; return result_wear; @@ -410,6 +431,7 @@ DigParams getDigParams(const ItemGroupList &groups, if (leveldiff > 1) time /= leveldiff; + if (!result_diggable || time < result_time) { result_time = time; result_diggable = true; @@ -509,4 +531,3 @@ f32 getToolRange(const ItemStack &wielded_item, const ItemStack &hand_item, return max_d; } - diff --git a/src/tool.h b/src/tool.h index 4b25d3a62..c3d811dd4 100644 --- a/src/tool.h +++ b/src/tool.h @@ -83,6 +83,10 @@ struct ToolCapabilities void deSerialize(std::istream &is); void serializeJson(std::ostream &os) const; void deserializeJson(std::istream &is); + +private: + void deserializeJsonGroupcaps(Json::Value &json); + void deserializeJsonDamageGroups(Json::Value &json); }; struct WearBarParams diff --git a/src/translation.cpp b/src/translation.cpp index 5d5491e56..728789acc 100644 --- a/src/translation.cpp +++ b/src/translation.cpp @@ -19,7 +19,9 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "translation.h" #include "log.h" +#include "util/hex.h" #include "util/string.h" +#include "gettext.h" #include @@ -29,10 +31,22 @@ static Translations client_translations; Translations *g_client_translations = &client_translations; #endif +const std::string_view Translations::getFileLanguage(const std::string &filename) +{ + const char *translate_ext[] = { + ".tr", ".po", ".mo", NULL + }; + auto basename = removeStringEnd(filename, translate_ext); + auto pos = basename.rfind('.'); + if (pos == basename.npos) + return ""; + return basename.substr(pos+1); +} void Translations::clear() { m_translations.clear(); + m_plural_translations.clear(); } const std::wstring &Translations::getTranslation( @@ -45,7 +59,52 @@ const std::wstring &Translations::getTranslation( return s; } -void Translations::loadTranslation(const std::string &data) +const std::wstring &Translations::getPluralTranslation( + const std::wstring &textdomain, const std::wstring &s, unsigned long int number) const +{ + std::wstring key = textdomain + L"|" + s; + auto it = m_plural_translations.find(key); + if (it != m_plural_translations.end()) { + auto n = (*(it->second.first))(number); + const std::vector &v = it->second.second; + if (n < v.size()) { + if (v[n].empty()) + return s; + return v[n]; + } + } + return s; +} + + +void Translations::addTranslation( + const std::wstring &textdomain, const std::wstring &original, const std::wstring &translated) +{ + std::wstring key = textdomain + L"|" + original; + if (!translated.empty()) { + m_translations.emplace(std::move(key), std::move(translated)); + } +} + +void Translations::addPluralTranslation( + const std::wstring &textdomain, const GettextPluralForm::Ptr &plural, const std::wstring &original, std::vector &translated) +{ + static bool warned = false; + if (!plural) { + warned = true; + if (!warned) + errorstream << "Translations: plural translation entry defined without Plural-Forms" << std::endl; + return; + } else if (translated.size() != plural->size()) { + errorstream << "Translations: incorrect number of plural translations (expected " << plural->size() << ", got " << translated.size() << ")" << std::endl; + return; + } + std::wstring key = textdomain + L"|" + original; + m_plural_translations.emplace(std::move(key), std::pair(plural, translated)); +} + + +void Translations::loadTrTranslation(const std::string &data) { std::istringstream is(data); std::string textdomain_narrow; @@ -145,11 +204,455 @@ void Translations::loadTranslation(const std::string &data) } } - std::wstring oword1 = word1.str(), oword2 = word2.str(); - if (!oword2.empty()) { - std::wstring translation_index = textdomain + L"|"; - translation_index.append(oword1); - m_translations.emplace(std::move(translation_index), std::move(oword2)); - } + addTranslation(textdomain, word1.str(), word2.str()); + } +} + + +std::wstring Translations::unescapeC(const std::wstring &str) +{ + // Process escape sequences in str as if it were a C string + std::wstring result; + size_t i = 0; + while (i < str.length()) { + if (str[i] != L'\\') { + result.push_back(str[i]); + i++; + continue; + } + i++; + if (i == str.length()) { + errorstream << "Unfinished escape sequence at the end of \"" << wide_to_utf8(str) << "\"" << std::endl; + break; + } + switch (str[i]) { + // From https://en.wikipedia.org/wiki/Escape_sequences_in_C#Table_of_escape_sequences + case L'a': result.push_back(L'\a'); break; + case L'b': result.push_back(L'\b'); break; + case L'e': result.push_back(L'\x1b'); break; + case L'f': result.push_back(L'\f'); break; + case L'n': result.push_back(L'\n'); break; + case L'r': result.push_back(L'\r'); break; + case L't': result.push_back(L'\t'); break; + case L'v': result.push_back(L'\v'); break; + case L'\\': result.push_back(L'\\'); break; + case L'\'': result.push_back(L'\''); break; + case L'"': result.push_back(L'"'); break; + case L'?': result.push_back(L'?'); break; + case L'0': case L'1': case L'2': case L'3': case L'4': case L'5': case L'6': case L'7': { + size_t j = 0; + wchar_t c = 0; + for (; j < 3 && i+j < str.length() && L'0' <= str[i+j] && str[i+j] <= L'7'; j++) { + c = c * 8 + (str[i+j] - L'0'); + } + if (c <= 0xff) { + result.push_back(c); + } + i += j; + continue; + } + case L'x': { + i++; + if (i >= str.length()) { + errorstream << "Unfinished escape sequence at the end of \"" << wide_to_utf8(str) << "\"" << std::endl; + } + char32_t c = 0; + size_t j = 0; + unsigned char v; + for (; i+j < str.length() && hex_digit_decode((char)str[i+j], v); j++) { + c = c * 16 + v; + } + if (j == 0) { + errorstream << "Invalid escape sequence \\x, ignoring" << std::endl; + continue; + } + // If character fits in 16 bits and is not part of surrogate pair, insert it. + // Otherwise, silently drop it: this is valid since \x escape sequences with + // values above 0xff are implementation-defined + if ((c < 0xd800) || (0xe000 <= c && c <= 0xffff)) { + result.push_back(c); + } + i += j; + continue; + } + case L'u': { + i++; + if (i + 4 > str.length()) { + errorstream << "Unfinished escape sequence at the end of \"" << wide_to_utf8(str) << "\"" << std::endl; + } + char16_t c = 0; + bool ok = true; + for (size_t j = 0; j < 4; j++) { + unsigned char v; + if (str[i+j] <= 0xff && hex_digit_decode((char)str[i+j], v)) { + c = c * 16 + v; + } else { + errorstream << "Invalid unicode escape sequence \"\\u" << wide_to_utf8(str.substr(i, 4)) << "\", ignoring" << std::endl; + ok = false; + break; + } + } + if (ok) { + wide_add_codepoint(result, c); + } + i += 4; + continue; + } + case L'U': { + i++; + if (i + 8 > str.length()) { + errorstream << "Unfinished escape sequence at the end of \"" << wide_to_utf8(str) << "\"" << std::endl; + } + char32_t c = 0; + bool ok = true; + for (size_t j = 0; j < 8; j++) { + unsigned char v; + if (str[i+j] <= 0xff && hex_digit_decode((char)str[i+j], v)) { + c = c * 16 + v; + } else { + errorstream << "Invalid unicode escape sequence \"\\U" << wide_to_utf8(str.substr(i, 8)) << "\", ignoring" << std::endl; + ok = false; + break; + } + } + if (ok) { + wide_add_codepoint(result, c); + } + i += 8; + continue; + } + default: { + errorstream << "Unknown escape sequence \"\\" << str[i] << "\", ignoring" << std::endl; + break; + } + } + i++; + } + return result; +} + +void Translations::loadPoEntry(const std::wstring &basefilename, const GettextPluralForm::Ptr &plural_form, const std::map &entry) +{ + // Process an entry from a PO file and add it to the translation table + // Assumes that entry[L"msgid"] is always defined + std::wstring textdomain; + auto ctx = entry.find(L"msgctxt"); + if (ctx != entry.end()) { + textdomain = ctx->second; + } else { + textdomain = basefilename; + } + std::wstring original = entry.at(L"msgid"); + + auto plural = entry.find(L"msgid_plural"); + if (plural == entry.end()) { + auto translated = entry.find(L"msgstr"); + if (translated == entry.end()) { + errorstream << "Could not load translation: entry for msgid \"" << wide_to_utf8(original) << "\" does not contain a msgstr field" << std::endl; + return; + } + addTranslation(textdomain, original, translated->second); + } else { + std::vector translations; + for (int i = 0; ; i++) { + auto translated = entry.find(L"msgstr[" + std::to_wstring(i) + L"]"); + if (translated == entry.end()) + break; + translations.push_back(translated->second); + } + addPluralTranslation(textdomain, plural_form, original, translations); + addPluralTranslation(textdomain, plural_form, plural->second, translations); + } +} + +bool Translations::inEscape(const std::wstring &line, size_t pos) +{ + if (pos == std::wstring::npos || pos == 0) + return false; + pos--; + size_t count = 0; + for (; line[pos] == L'\\'; pos--) { + count++; + if (pos == 0) + break; + } + return count % 2 == 1; +} + +std::optional> Translations::parsePoLine(const std::string &line) +{ + if (line.empty()) + return std::nullopt; + if (line[0] == '#') + return std::pair(L"#", utf8_to_wide(line.substr(1))); + + std::wstring wline = utf8_to_wide(line); + // Defend against some possibly malformed utf8 string, which + // is empty after converting to wide string + if (wline.empty()) + return std::nullopt; + + std::size_t pos = wline.find(L'"'); + std::wstring s; + if (pos == std::wstring::npos) { + errorstream << "Unable to parse po file line: " << line << std::endl; + return std::nullopt; + } + auto prefix = trim(wline.substr(0, pos)); + auto begin = pos; + while (pos < wline.size()) { + begin = wline.find(L'"', pos); + if (begin == std::wstring::npos) { + if (trim(wline.substr(pos)).empty()) { + break; + } else { + errorstream << "Excessive content at the end of po file line: " << line << std::endl; + return std::nullopt; + } + } + if (!trim(wline.substr(pos, begin-pos)).empty()) { + errorstream << "Excessive content within string concatenation in po file line: " << line << std::endl; + return std::nullopt; + } + auto end = wline.find(L'"', begin+1); + while (inEscape(wline, end)) { + end = wline.find(L'"', end+1); + } + if (end == std::wstring::npos) { + errorstream << "String extends beyond po file line: " << line << std::endl; + return std::nullopt; + } + s.append(unescapeC(wline.substr(begin+1, end-begin-1))); + pos = end+1; + } + return std::pair(prefix, s); +} + +void Translations::loadPoTranslation(const std::string &basefilename, const std::string &data) +{ + std::istringstream is(data); + std::string line; + std::map last_entry; + std::wstring last_key; + std::wstring wbasefilename = utf8_to_wide(basefilename); + GettextPluralForm::Ptr plural; + bool skip = false; + bool skip_last = false; + + while (is.good()) { + std::getline(is, line); + // Trim last character if file was using a \r\n line ending + if (line.length () > 0 && line[line.length() - 1] == '\r') + line.resize(line.length() - 1); + + auto parsed = parsePoLine(line); + if (!parsed) + continue; + auto prefix = parsed->first; + auto s = parsed->second; + + if (prefix == L"#") { + if (s[0] == L',') { + // Skip fuzzy entries + if ((s + L' ').find(L" fuzzy ") != line.npos) { + if (last_entry.empty()) + skip_last = true; + else + skip = true; + } + } + continue; + } + + if (prefix.empty()) { + // Continuation of previous line + if (last_key == L"") { + errorstream << "Unable to parse po file: continuation of non-existant previous line" << std::endl; + continue; + } + + last_entry[last_key].append(s); + continue; + } + + if (prefix == L"msgctxt" || (prefix == L"msgid" && last_entry.find(L"msgid") != last_entry.end())) { + if (last_entry.find(L"msgid") != last_entry.end()) { + if (!skip_last) { + if (last_entry[L"msgid"].empty()) { + if (last_entry.find(L"msgstr") == last_entry.end()) { + errorstream << "Header entry has no \"msgstr\" field" << std::endl; + } else if (plural) { + errorstream << "Attempt to override existing po header entry" << std::endl; + } else { + for (auto &line: str_split(last_entry[L"msgstr"], L'\n')) { + if (str_starts_with(line, L"Plural-Forms:")) { + plural = GettextPluralForm::parseHeaderLine(line); + if (!(plural && *plural)) { + errorstream << "Invalid Plural-Forms line: " << wide_to_utf8(line) << std::endl; + } + } + } + } + } else { + loadPoEntry(wbasefilename, plural, last_entry); + } + } + last_entry.clear(); + skip_last = skip; + } else if (!last_entry.empty()) { + errorstream << "Unable to parse po file: previous entry has no \"msgid\" field but is not empty" << std::endl; + last_entry.clear(); + skip_last = skip; + } + } else { + // prevent malpositioned fuzzy flag from influencing the following entry + skip = false; + } + if (last_entry.find(prefix) != last_entry.end()) { + errorstream << "Unable to parse po file: Key \"" << wide_to_utf8(prefix) << "\" was already present in previous entry" << std::endl; + continue; + } + last_key = prefix; + last_entry[prefix] = s; + } + + if (last_entry.find(L"msgid") != last_entry.end()) { + if (!skip_last && !last_entry[L"msgid"].empty()) + loadPoEntry(wbasefilename, plural, last_entry); + } else if (!last_entry.empty()) { + errorstream << "Unable to parse po file: Last entry has no \"msgid\" field" << std::endl; + } +} + +void Translations::loadMoEntry(const std::wstring &basefilename, const GettextPluralForm::Ptr &plural_form, const std::string &original, const std::string &translated) +{ + std::wstring textdomain = L""; + size_t found; + std::string noriginal = original; + found = original.find('\x04'); // EOT character + if (found != std::string::npos) { + textdomain = utf8_to_wide(original.substr(0, found)); + noriginal = original.substr(found + 1); + } else { + textdomain = basefilename; + } + + found = noriginal.find('\0'); + if (found != std::string::npos) { + std::vector translations = str_split(utf8_to_wide(translated), L'\0'); + addPluralTranslation(textdomain, plural_form, utf8_to_wide(noriginal.substr(0, found)), translations); + addPluralTranslation(textdomain, plural_form, utf8_to_wide(noriginal.substr(found + 1)), translations); + } else { + addTranslation(textdomain, utf8_to_wide(noriginal), utf8_to_wide(translated)); + } +} + +inline u32 readVarEndian(bool is_be, std::string_view data, size_t pos = 0) +{ + if (pos + 4 > data.size()) + return 0; + if (is_be) { + return + ((u32)(unsigned char)data[pos+0] << 24) | ((u32)(unsigned char)data[pos+1] << 16) | + ((u32)(unsigned char)data[pos+2] << 8) | ((u32)(unsigned char)data[pos+3] << 0); + } else { + return + ((u32)(unsigned char)data[pos+0] << 0) | ((u32)(unsigned char)data[pos+1] << 8) | + ((u32)(unsigned char)data[pos+2] << 16) | ((u32)(unsigned char)data[pos+3] << 24); + } +} + +void Translations::loadMoTranslation(const std::string &basefilename, const std::string &data) +{ + size_t length = data.length(); + std::wstring wbasefilename = utf8_to_wide(basefilename); + GettextPluralForm::Ptr plural_form; + + if (length < 20) { + errorstream << "Ignoring too short mo file" << std::endl; + return; + } + + u32 magic = readVarEndian(false, data); + bool is_be; + if (magic == 0x950412de) { + is_be = false; + } else if (magic == 0xde120495) { + is_be = true; + } else { + errorstream << "Bad magic number for mo file: 0x" << hex_encode(data.substr(0, 4)) << std::endl; + return; + } + + u32 revision = readVarEndian(is_be, data, 4); + if (revision != 0) { + errorstream << "Unknown revision " << revision << " for mo file" << std::endl; + return; + } + + u32 nstring = readVarEndian(is_be, data, 8); + u32 original_offset = readVarEndian(is_be, data, 12); + u32 translated_offset = readVarEndian(is_be, data, 16); + + if (length < original_offset + 8 * (u64)nstring || length < translated_offset + 8 * (u64)nstring) { + errorstream << "Ignoring truncated mo file" << std::endl; + return; + } + + for (u32 i = 0; i < nstring; i++) { + u32 original_len = readVarEndian(is_be, data, original_offset + 8 * i); + u32 original_off = readVarEndian(is_be, data, original_offset + 8 * i + 4); + u32 translated_len = readVarEndian(is_be, data, translated_offset + 8 * i); + u32 translated_off = readVarEndian(is_be, data, translated_offset + 8 * i + 4); + + if (length < original_off + (u64)original_len || length < translated_off + (u64)translated_len) { + errorstream << "Ignoring translation out of mo file" << std::endl; + continue; + } + + if (data[original_off+original_len] != '\0' || data[translated_off+translated_len] != '\0') { + errorstream << "String in mo entry does not have a trailing NUL" << std::endl; + continue; + } + + auto original = data.substr(original_off, original_len); + auto translated = data.substr(translated_off, translated_len); + + if (original.empty()) { + if (plural_form) { + errorstream << "Attempt to override existing mo header entry" << std::endl; + } else { + for (auto &line: str_split(translated, '\n')) { + if (str_starts_with(line, "Plural-Forms:")) { + plural_form = GettextPluralForm::parseHeaderLine(utf8_to_wide(line)); + if (!(plural_form && *plural_form)) { + errorstream << "Invalid Plural-Forms line: " << line << std::endl; + } + } + } + } + } else { + loadMoEntry(wbasefilename, plural_form, original, translated); + } + } + + return; +} + +void Translations::loadTranslation(const std::string &filename, const std::string &data) +{ + const char *trExtension[] = { ".tr", NULL }; + const char *poExtension[] = { ".po", NULL }; + const char *moExtension[] = { ".mo", NULL }; + if (!removeStringEnd(filename, trExtension).empty()) { + loadTrTranslation(data); + } else if (!removeStringEnd(filename, poExtension).empty()) { + std::string basefilename = str_split(filename, '.')[0]; + loadPoTranslation(basefilename, data); + } else if (!removeStringEnd(filename, moExtension).empty()) { + std::string basefilename = str_split(filename, '.')[0]; + loadMoTranslation(basefilename, data); + } else { + errorstream << "loadTranslation called with invalid filename: \"" << filename << "\"" << std::endl; } } diff --git a/src/translation.h b/src/translation.h index d7ed15505..972cdafef 100644 --- a/src/translation.h +++ b/src/translation.h @@ -19,8 +19,12 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once +#include "gettext_plural_form.h" #include +#include +#include #include +#include class Translations; #ifndef SERVER @@ -30,11 +34,39 @@ extern Translations *g_client_translations; class Translations { public: - void loadTranslation(const std::string &data); + void loadTranslation(const std::string &filename, const std::string &data); void clear(); - const std::wstring &getTranslation(const std::wstring &textdomain, - const std::wstring &s) const; + const std::wstring &getTranslation( + const std::wstring &textdomain, const std::wstring &s) const; + const std::wstring &getPluralTranslation(const std::wstring &textdomain, + const std::wstring &s, unsigned long int number) const; + static const std::string_view getFileLanguage(const std::string &filename); + static inline bool isTranslationFile(const std::string &filename) + { + return getFileLanguage(filename) != ""; + } + // for testing + inline size_t size() + { + return m_translations.size() + m_plural_translations.size()/2; + } private: std::unordered_map m_translations; + std::unordered_map>> m_plural_translations; + + void addTranslation(const std::wstring &textdomain, const std::wstring &original, + const std::wstring &translated); + void addPluralTranslation(const std::wstring &textdomain, + const GettextPluralForm::Ptr &plural, + const std::wstring &original, + std::vector &translated); + std::wstring unescapeC(const std::wstring &str); + std::optional> parsePoLine(const std::string &line); + bool inEscape(const std::wstring &str, size_t pos); + void loadPoEntry(const std::wstring &basefilename, const GettextPluralForm::Ptr &plural_form, const std::map &entry); + void loadMoEntry(const std::wstring &basefilename, const GettextPluralForm::Ptr &plural_form, const std::string &original, const std::string &translated); + void loadTrTranslation(const std::string &data); + void loadPoTranslation(const std::string &basefilename, const std::string &data); + void loadMoTranslation(const std::string &basefilename, const std::string &data); }; diff --git a/src/unittest/CMakeLists.txt b/src/unittest/CMakeLists.txt index ec52ee6bf..46e4f9a18 100644 --- a/src/unittest/CMakeLists.txt +++ b/src/unittest/CMakeLists.txt @@ -13,6 +13,7 @@ set (UNITTEST_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/test_filesys.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_inventory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_irrptr.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test_logging.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_lua.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_map.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_mapblock.cpp @@ -36,6 +37,7 @@ set (UNITTEST_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/test_socket.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_servermodmanager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_threading.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test_translations.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_utilities.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_voxelarea.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_voxelalgorithms.cpp @@ -49,7 +51,7 @@ set (UNITTEST_CLIENT_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/test_content_mapblock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_eventmanager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_gameui.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test_irr_gltf_mesh_loader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_mesh_compare.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_keycode.cpp PARENT_SCOPE) - diff --git a/src/unittest/mock_serveractiveobject.h b/src/unittest/mock_serveractiveobject.h index 78e3df8af..995a27e55 100644 --- a/src/unittest/mock_serveractiveobject.h +++ b/src/unittest/mock_serveractiveobject.h @@ -22,7 +22,7 @@ with this program; if not, write to the Free Software Foundation, Inc., class MockServerActiveObject : public ServerActiveObject { public: - MockServerActiveObject(ServerEnvironment *env = nullptr, const v3f &p = v3f()) : + MockServerActiveObject(ServerEnvironment *env = nullptr, v3f p = v3f()) : ServerActiveObject(env, p) {} virtual ActiveObjectType getType() const { return ACTIVEOBJECT_TYPE_TEST; } diff --git a/src/unittest/test.cpp b/src/unittest/test.cpp index 33f8dcbb5..a3b9250a0 100644 --- a/src/unittest/test.cpp +++ b/src/unittest/test.cpp @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "nodedef.h" #include "itemdef.h" #include "dummygamedef.h" +#include "log_internal.h" #include "modchannels.h" #include "util/numeric.h" #include "porting.h" diff --git a/src/unittest/test_connection.cpp b/src/unittest/test_connection.cpp index 4adbc9039..b7d751dc3 100644 --- a/src/unittest/test_connection.cpp +++ b/src/unittest/test_connection.cpp @@ -23,7 +23,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "porting.h" #include "settings.h" #include "util/serialize.h" -#include "network/connection_internal.h" +#include "network/peerhandler.h" +#include "network/mtp/internal.h" #include "network/networkpacket.h" #include "network/socket.h" @@ -59,7 +60,7 @@ struct Handler : public con::PeerHandler { Handler(const char *a_name) : name(a_name) {} - void peerAdded(con::Peer *peer) + void peerAdded(con::IPeer *peer) { infostream << "Handler(" << name << ")::peerAdded(): " "id=" << peer->id << std::endl; @@ -67,7 +68,7 @@ struct Handler : public con::PeerHandler count++; } - void deletingPeer(con::Peer *peer, bool timeout) + void deletingPeer(con::IPeer *peer, bool timeout) { infostream << "Handler(" << name << ")::deletingPeer(): " "id=" << peer->id << ", timeout=" << timeout << std::endl; @@ -159,14 +160,15 @@ void TestConnection::testHelpers() void TestConnection::testConnectSendReceive() { + + constexpr u32 timeout_ms = 100; + /* Test some real connections NOTE: This mostly tests the legacy interface. */ - u32 proto_id = 0xad26846a; - Handler hand_server("server"); Handler hand_client("client"); @@ -187,11 +189,11 @@ void TestConnection::testConnectSendReceive() } infostream << "** Creating server Connection" << std::endl; - con::Connection server(proto_id, 512, 5.0, false, &hand_server); + con::Connection server(512, 5.0f, false, &hand_server); server.Serve(address); infostream << "** Creating client Connection" << std::endl; - con::Connection client(proto_id, 512, 5.0, false, &hand_client); + con::Connection client(512, 5.0f, false, &hand_client); UASSERT(hand_server.count == 0); UASSERT(hand_client.count == 0); @@ -211,13 +213,11 @@ void TestConnection::testConnectSendReceive() // Client should not have added client yet UASSERT(hand_client.count == 0); - try { - NetworkPacket pkt; - infostream << "** running client.Receive()" << std::endl; - client.Receive(&pkt); + NetworkPacket pkt; + infostream << "** running client.Receive()" << std::endl; + if (client.ReceiveTimeoutMs(&pkt, timeout_ms)) { infostream << "** Client received: peer_id=" << pkt.getPeerId() << ", size=" << pkt.getSize() << std::endl; - } catch (con::NoIncomingDataException &e) { } // Client should have added server now @@ -228,14 +228,14 @@ void TestConnection::testConnectSendReceive() sleep_ms(100); - try { - NetworkPacket pkt; - infostream << "** running server.Receive()" << std::endl; - server.Receive(&pkt); + NetworkPacket pkt1; + infostream << "** running server.Receive()" << std::endl; + if (server.ReceiveTimeoutMs(&pkt, timeout_ms)) { infostream << "** Server received: peer_id=" << pkt.getPeerId() - << ", size=" << pkt.getSize() - << std::endl; - } catch (con::NoIncomingDataException &e) { + << ", size=" << pkt.getSize() + << std::endl; + } + else { // No actual data received, but the client has // probably been connected } @@ -250,27 +250,23 @@ void TestConnection::testConnectSendReceive() //sleep_ms(50); while (client.Connected() == false) { - try { - NetworkPacket pkt; - infostream << "** running client.Receive()" << std::endl; - client.Receive(&pkt); + NetworkPacket pkt; + infostream << "** running client.Receive()" << std::endl; + if (client.TryReceive(&pkt)) { infostream << "** Client received: peer_id=" << pkt.getPeerId() << ", size=" << pkt.getSize() << std::endl; - } catch (con::NoIncomingDataException &e) { } sleep_ms(50); } sleep_ms(50); - try { - NetworkPacket pkt; - infostream << "** running server.Receive()" << std::endl; - server.Receive(&pkt); + NetworkPacket pkt2; + infostream << "** running server.Receive()" << std::endl; + if (server.ReceiveTimeoutMs(&pkt, timeout_ms)) { infostream << "** Server received: peer_id=" << pkt.getPeerId() - << ", size=" << pkt.getSize() - << std::endl; - } catch (con::NoIncomingDataException &e) { + << ", size=" << pkt.getSize() + << std::endl; } /* @@ -289,7 +285,7 @@ void TestConnection::testConnectSendReceive() NetworkPacket recvpacket; infostream << "** running server.Receive()" << std::endl; - server.Receive(&recvpacket); + UASSERT(server.ReceiveTimeoutMs(&recvpacket, timeout_ms)); infostream << "** Server received: peer_id=" << pkt.getPeerId() << ", size=" << pkt.getSize() << ", data=" << (const char*)pkt.getU8Ptr(0) @@ -339,14 +335,12 @@ void TestConnection::testConnectSendReceive() for (;;) { if (porting::getTimeMs() - timems0 > 5000 || received) break; - try { - NetworkPacket pkt; - client.Receive(&pkt); + NetworkPacket pkt; + if (client.ReceiveTimeoutMs(&pkt, timeout_ms)) { size = pkt.getSize(); peer_id = pkt.getPeerId(); recvdata = pkt.oldForgePacket(); received = true; - } catch (con::NoIncomingDataException &e) { } sleep_ms(10); } diff --git a/src/unittest/test_filesys.cpp b/src/unittest/test_filesys.cpp index fd25d2de9..e24e79374 100644 --- a/src/unittest/test_filesys.cpp +++ b/src/unittest/test_filesys.cpp @@ -42,6 +42,7 @@ public: void testSafeWriteToFile(); void testCopyFileContents(); void testNonExist(); + void testRecursiveDelete(); }; static TestFileSys g_test_instance; @@ -56,6 +57,7 @@ void TestFileSys::runTests(IGameDef *gamedef) TEST(testSafeWriteToFile); TEST(testCopyFileContents); TEST(testNonExist); + TEST(testRecursiveDelete); } //////////////////////////////////////////////////////////////////////////////// @@ -338,3 +340,32 @@ void TestFileSys::testNonExist() auto ifs = open_ifstream(path.c_str(), false); UASSERT(!ifs.good()); } + +void TestFileSys::testRecursiveDelete() +{ + std::string dirs[2]; + dirs[0] = getTestTempDirectory() + DIR_DELIM "a"; + dirs[1] = dirs[0] + DIR_DELIM "b"; + + std::string files[2] = { + dirs[0] + DIR_DELIM "file1", + dirs[1] + DIR_DELIM "file2" + }; + + for (auto &it : dirs) + fs::CreateDir(it); + for (auto &it : files) + open_ofstream(it.c_str(), false).close(); + + for (auto &it : dirs) + UASSERT(fs::IsDir(it)); + for (auto &it : files) + UASSERT(fs::IsFile(it)); + + UASSERT(fs::RecursiveDelete(dirs[0])); + + for (auto &it : dirs) + UASSERT(!fs::IsDir(it)); + for (auto &it : files) + UASSERT(!fs::IsFile(it)); +} diff --git a/src/unittest/test_irr_gltf_mesh_loader.cpp b/src/unittest/test_irr_gltf_mesh_loader.cpp new file mode 100644 index 000000000..674f3c0dd --- /dev/null +++ b/src/unittest/test_irr_gltf_mesh_loader.cpp @@ -0,0 +1,462 @@ +// Minetest +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "content/subgames.h" +#include "filesys.h" + +#include "irr_v3d.h" +#include "irr_v2d.h" +#include "irr_ptr.h" + +#include "ISkinnedMesh.h" +#include + +#include "catch.h" + +TEST_CASE("gltf") { + +const auto gamespec = findSubgame("devtest"); + +if (!gamespec.isValid()) + SKIP(); + +irr::SIrrlichtCreationParameters p; +p.DriverType = video::EDT_NULL; +auto *driver = irr::createDeviceEx(p); +REQUIRE(driver); + +auto *smgr = driver->getSceneManager(); +const auto loadMesh = [&] (const io::path& filepath) { + irr_ptr file(driver->getFileSystem()->createAndOpenFile(filepath)); + REQUIRE(file); + return smgr->getMesh(file.get()); +}; + +const static auto model_stem = gamespec.gamemods_path + + DIR_DELIM + "gltf" + DIR_DELIM + "models" + DIR_DELIM + "gltf_"; + +SECTION("error cases") { + const static auto invalid_model_path = gamespec.gamemods_path + DIR_DELIM + "gltf" + DIR_DELIM + "invalid" + DIR_DELIM; + + SECTION("empty gltf file") { + CHECK(!loadMesh(invalid_model_path + "empty.gltf")); + } + + SECTION("null file pointer") { + CHECK(!smgr->getMesh(nullptr)); + } + + SECTION("invalid JSON") { + CHECK(!loadMesh(invalid_model_path + "json_missing_brace.gltf")); + } + + // This is an example of something that should be validated by tiniergltf. + SECTION("invalid bufferview bounds") + { + CHECK(!loadMesh(invalid_model_path + "invalid_bufferview_bounds.gltf")); + } +} + +SECTION("minimal triangle") { + const auto path = GENERATE( + model_stem + "minimal_triangle.gltf", + model_stem + "triangle_with_vertex_stride.gltf", + // Test non-indexed geometry. + model_stem + "triangle_without_indices.gltf"); + INFO(path); + const auto mesh = loadMesh(path); + REQUIRE(mesh); + REQUIRE(mesh->getMeshBufferCount() == 1); + + SECTION("vertex coordinates are correct") { + REQUIRE(mesh->getMeshBuffer(0)->getVertexCount() == 3); + auto vertices = static_cast( + mesh->getMeshBuffer(0)->getVertices()); + CHECK(vertices[0].Pos == v3f {0.0f, 0.0f, 0.0f}); + CHECK(vertices[1].Pos == v3f {1.0f, 0.0f, 0.0f}); + CHECK(vertices[2].Pos == v3f {0.0f, 1.0f, 0.0f}); + } + + SECTION("vertex indices are correct") { + REQUIRE(mesh->getMeshBuffer(0)->getIndexCount() == 3); + auto indices = static_cast( + mesh->getMeshBuffer(0)->getIndices()); + CHECK(indices[0] == 2); + CHECK(indices[1] == 1); + CHECK(indices[2] == 0); + } +} + +SECTION("blender cube") { + const auto path = GENERATE( + model_stem + "blender_cube.gltf", + model_stem + "blender_cube.glb"); + const auto mesh = loadMesh(path); + REQUIRE(mesh); + REQUIRE(mesh->getMeshBufferCount() == 1); + SECTION("vertex coordinates are correct") { + REQUIRE(mesh->getMeshBuffer(0)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(0)->getVertices()); + CHECK(vertices[0].Pos == v3f{-10.0f, -10.0f, -10.0f}); + CHECK(vertices[3].Pos == v3f{-10.0f, 10.0f, -10.0f}); + CHECK(vertices[6].Pos == v3f{-10.0f, -10.0f, 10.0f}); + CHECK(vertices[9].Pos == v3f{-10.0f, 10.0f, 10.0f}); + CHECK(vertices[12].Pos == v3f{10.0f, -10.0f, -10.0f}); + CHECK(vertices[15].Pos == v3f{10.0f, 10.0f, -10.0f}); + CHECK(vertices[18].Pos == v3f{10.0f, -10.0f, 10.0f}); + CHECK(vertices[21].Pos == v3f{10.0f, 10.0f, 10.0f}); + } + + SECTION("vertex indices are correct") { + REQUIRE(mesh->getMeshBuffer(0)->getIndexCount() == 36); + auto indices = static_cast( + mesh->getMeshBuffer(0)->getIndices()); + CHECK(indices[0] == 16); + CHECK(indices[1] == 5); + CHECK(indices[2] == 22); + CHECK(indices[35] == 0); + } + + SECTION("vertex normals are correct") { + REQUIRE(mesh->getMeshBuffer(0)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(0)->getVertices()); + CHECK(vertices[0].Normal == v3f{-1.0f, 0.0f, 0.0f}); + CHECK(vertices[1].Normal == v3f{0.0f, -1.0f, 0.0f}); + CHECK(vertices[2].Normal == v3f{0.0f, 0.0f, -1.0f}); + CHECK(vertices[3].Normal == v3f{-1.0f, 0.0f, 0.0f}); + CHECK(vertices[6].Normal == v3f{-1.0f, 0.0f, 0.0f}); + CHECK(vertices[23].Normal == v3f{1.0f, 0.0f, 0.0f}); + + } + + SECTION("texture coords are correct") { + REQUIRE(mesh->getMeshBuffer(0)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(0)->getVertices()); + CHECK(vertices[0].TCoords == v2f{0.375f, 1.0f}); + CHECK(vertices[1].TCoords == v2f{0.125f, 0.25f}); + CHECK(vertices[2].TCoords == v2f{0.375f, 0.0f}); + CHECK(vertices[3].TCoords == v2f{0.6250f, 1.0f}); + CHECK(vertices[6].TCoords == v2f{0.375f, 0.75f}); + } +} + +SECTION("blender cube scaled") { + const auto mesh = loadMesh(model_stem + "blender_cube_scaled.gltf"); + REQUIRE(mesh); + REQUIRE(mesh->getMeshBufferCount() == 1); + + SECTION("Scaling is correct") { + REQUIRE(mesh->getMeshBuffer(0)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(0)->getVertices()); + + CHECK(vertices[0].Pos == v3f{-150.0f, -1.0f, -21.5f}); + CHECK(vertices[3].Pos == v3f{-150.0f, 1.0f, -21.5f}); + CHECK(vertices[6].Pos == v3f{-150.0f, -1.0f, 21.5f}); + CHECK(vertices[9].Pos == v3f{-150.0f, 1.0f, 21.5f}); + CHECK(vertices[12].Pos == v3f{150.0f, -1.0f, -21.5f}); + CHECK(vertices[15].Pos == v3f{150.0f, 1.0f, -21.5f}); + CHECK(vertices[18].Pos == v3f{150.0f, -1.0f, 21.5f}); + CHECK(vertices[21].Pos == v3f{150.0f, 1.0f, 21.5f}); + } +} + +SECTION("blender cube matrix transform") { + const auto mesh = loadMesh(model_stem + "blender_cube_matrix_transform.gltf"); + REQUIRE(mesh); + REQUIRE(mesh->getMeshBufferCount() == 1); + + SECTION("Transformation is correct") { + REQUIRE(mesh->getMeshBuffer(0)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(0)->getVertices()); + const auto checkVertex = [&](const std::size_t i, v3f vec) { + // The transform scales by (1, 2, 3) and translates by (4, 5, 6). + CHECK(vertices[i].Pos == vec * v3f{1, 2, 3} + // The -6 is due to the coordinate system conversion. + + v3f{4, 5, -6}); + }; + checkVertex(0, v3f{-1, -1, -1}); + checkVertex(3, v3f{-1, 1, -1}); + checkVertex(6, v3f{-1, -1, 1}); + checkVertex(9, v3f{-1, 1, 1}); + checkVertex(12, v3f{1, -1, -1}); + checkVertex(15, v3f{1, 1, -1}); + checkVertex(18, v3f{1, -1, 1}); + checkVertex(21, v3f{1, 1, 1}); + } +} + +SECTION("snow man") { + const auto mesh = loadMesh(model_stem + "snow_man.gltf"); + REQUIRE(mesh); + REQUIRE(mesh->getMeshBufferCount() == 3); + + SECTION("vertex coordinates are correct for all buffers") { + REQUIRE(mesh->getMeshBuffer(0)->getVertexCount() == 24); + { + auto vertices = static_cast( + mesh->getMeshBuffer(0)->getVertices()); + CHECK(vertices[0].Pos == v3f{3.0f, 24.0f, -3.0f}); + CHECK(vertices[3].Pos == v3f{3.0f, 18.0f, 3.0f}); + CHECK(vertices[6].Pos == v3f{-3.0f, 18.0f, -3.0f}); + CHECK(vertices[9].Pos == v3f{3.0f, 24.0f, 3.0f}); + CHECK(vertices[12].Pos == v3f{3.0f, 18.0f, -3.0f}); + CHECK(vertices[15].Pos == v3f{-3.0f, 18.0f, 3.0f}); + CHECK(vertices[18].Pos == v3f{3.0f, 18.0f, -3.0f}); + CHECK(vertices[21].Pos == v3f{3.0f, 18.0f, 3.0f}); + } + { + REQUIRE(mesh->getMeshBuffer(1)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(1)->getVertices()); + CHECK(vertices[2].Pos == v3f{5.0f, 10.0f, 5.0f}); + CHECK(vertices[3].Pos == v3f{5.0f, 0.0f, 5.0f}); + CHECK(vertices[7].Pos == v3f{-5.0f, 0.0f, 5.0f}); + CHECK(vertices[8].Pos == v3f{5.0f, 10.0f, -5.0f}); + CHECK(vertices[14].Pos == v3f{5.0f, 0.0f, 5.0f}); + CHECK(vertices[16].Pos == v3f{5.0f, 10.0f, -5.0f}); + CHECK(vertices[22].Pos == v3f{-5.0f, 10.0f, 5.0f}); + CHECK(vertices[23].Pos == v3f{-5.0f, 0.0f, 5.0f}); + } + { + REQUIRE(mesh->getMeshBuffer(2)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(2)->getVertices()); + CHECK(vertices[1].Pos == v3f{4.0f, 10.0f, -4.0f}); + CHECK(vertices[2].Pos == v3f{4.0f, 18.0f, 4.0f}); + CHECK(vertices[3].Pos == v3f{4.0f, 10.0f, 4.0f}); + CHECK(vertices[10].Pos == v3f{-4.0f, 18.0f, -4.0f}); + CHECK(vertices[11].Pos == v3f{-4.0f, 18.0f, 4.0f}); + CHECK(vertices[12].Pos == v3f{4.0f, 10.0f, -4.0f}); + CHECK(vertices[17].Pos == v3f{-4.0f, 18.0f, -4.0f}); + CHECK(vertices[18].Pos == v3f{4.0f, 10.0f, -4.0f}); + } + } + + SECTION("vertex indices are correct for all buffers") { + { + REQUIRE(mesh->getMeshBuffer(0)->getIndexCount() == 36); + auto indices = static_cast( + mesh->getMeshBuffer(0)->getIndices()); + CHECK(indices[0] == 23); + CHECK(indices[1] == 21); + CHECK(indices[2] == 22); + CHECK(indices[35] == 2); + } + { + REQUIRE(mesh->getMeshBuffer(1)->getIndexCount() == 36); + auto indices = static_cast( + mesh->getMeshBuffer(1)->getIndices()); + CHECK(indices[10] == 16); + CHECK(indices[11] == 18); + CHECK(indices[15] == 13); + CHECK(indices[27] == 5); + } + { + REQUIRE(mesh->getMeshBuffer(2)->getIndexCount() == 36); + auto indices = static_cast( + mesh->getMeshBuffer(2)->getIndices()); + CHECK(indices[26] == 6); + CHECK(indices[27] == 5); + CHECK(indices[29] == 6); + CHECK(indices[32] == 2); + } + } + + + SECTION("vertex normals are correct for all buffers") { + { + REQUIRE(mesh->getMeshBuffer(0)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(0)->getVertices()); + CHECK(vertices[0].Normal == v3f{1.0f, 0.0f, -0.0f}); + CHECK(vertices[1].Normal == v3f{1.0f, 0.0f, -0.0f}); + CHECK(vertices[2].Normal == v3f{1.0f, 0.0f, -0.0f}); + CHECK(vertices[3].Normal == v3f{1.0f, 0.0f, -0.0f}); + CHECK(vertices[6].Normal == v3f{-1.0f, 0.0f, -0.0f}); + CHECK(vertices[23].Normal == v3f{0.0f, 0.0f, 1.0f}); + } + { + REQUIRE(mesh->getMeshBuffer(1)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(1)->getVertices()); + CHECK(vertices[0].Normal == v3f{1.0f, 0.0f, -0.0f}); + CHECK(vertices[1].Normal == v3f{1.0f, 0.0f, -0.0f}); + CHECK(vertices[3].Normal == v3f{1.0f, 0.0f, -0.0f}); + CHECK(vertices[6].Normal == v3f{-1.0f, 0.0f, -0.0f}); + CHECK(vertices[7].Normal == v3f{-1.0f, 0.0f, -0.0f}); + CHECK(vertices[22].Normal == v3f{0.0f, 0.0f, 1.0f}); + } + { + REQUIRE(mesh->getMeshBuffer(2)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(2)->getVertices()); + CHECK(vertices[3].Normal == v3f{1.0f, 0.0f, -0.0f}); + CHECK(vertices[4].Normal == v3f{-1.0f, 0.0f, -0.0f}); + CHECK(vertices[5].Normal == v3f{-1.0f, 0.0f, -0.0f}); + CHECK(vertices[10].Normal == v3f{0.0f, 1.0f, -0.0f}); + CHECK(vertices[11].Normal == v3f{0.0f, 1.0f, -0.0f}); + CHECK(vertices[19].Normal == v3f{0.0f, 0.0f, -1.0f}); + } + } + + + SECTION("texture coords are correct for all buffers") { + { + REQUIRE(mesh->getMeshBuffer(0)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(0)->getVertices()); + CHECK(vertices[0].TCoords == v2f{0.583333313f, 0.791666686f}); + CHECK(vertices[1].TCoords == v2f{0.583333313f, 0.666666686f}); + CHECK(vertices[2].TCoords == v2f{0.708333313f, 0.791666686f}); + CHECK(vertices[5].TCoords == v2f{0.375f, 0.416666657f}); + CHECK(vertices[6].TCoords == v2f{0.5f, 0.291666657f}); + CHECK(vertices[19].TCoords == v2f{0.708333313f, 0.75f}); + } + { + REQUIRE(mesh->getMeshBuffer(1)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(1)->getVertices()); + + CHECK(vertices[1].TCoords == v2f{0.0f, 0.791666686f}); + CHECK(vertices[4].TCoords == v2f{0.208333328f, 0.791666686f}); + CHECK(vertices[5].TCoords == v2f{0.0f, 0.791666686f}); + CHECK(vertices[6].TCoords == v2f{0.208333328f, 0.583333313f}); + CHECK(vertices[12].TCoords == v2f{0.416666657f, 0.791666686f}); + CHECK(vertices[15].TCoords == v2f{0.208333328f, 0.583333313f}); + } + { + REQUIRE(mesh->getMeshBuffer(2)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(2)->getVertices()); + CHECK(vertices[10].TCoords == v2f{0.375f, 0.416666657f}); + CHECK(vertices[11].TCoords == v2f{0.375f, 0.583333313f}); + CHECK(vertices[12].TCoords == v2f{0.708333313f, 0.625f}); + CHECK(vertices[17].TCoords == v2f{0.541666687f, 0.458333343f}); + CHECK(vertices[20].TCoords == v2f{0.208333328f, 0.416666657f}); + CHECK(vertices[22].TCoords == v2f{0.375f, 0.416666657f}); + } + } +} + +// https://github.com/KhronosGroup/glTF-Sample-Models/tree/main/2.0/SimpleSparseAccessor +SECTION("simple sparse accessor") +{ + const auto mesh = loadMesh(model_stem + "simple_sparse_accessor.gltf"); + REQUIRE(mesh); + const auto *vertices = reinterpret_cast( + mesh->getMeshBuffer(0)->getVertices()); + const std::array expectedPositions = { + // Lower + v3f(0, 0, 0), + v3f(1, 0, 0), + v3f(2, 0, 0), + v3f(3, 0, 0), + v3f(4, 0, 0), + v3f(5, 0, 0), + v3f(6, 0, 0), + // Upper + v3f(0, 1, 0), + v3f(1, 2, 0), // overridden + v3f(2, 1, 0), + v3f(3, 3, 0), // overridden + v3f(4, 1, 0), + v3f(5, 4, 0), // overridden + v3f(6, 1, 0), + }; + for (std::size_t i = 0; i < expectedPositions.size(); ++i) + CHECK(vertices[i].Pos == expectedPositions[i]); +} + +// https://github.com/KhronosGroup/glTF-Sample-Models/tree/main/2.0/SimpleSkin +SECTION("simple skin") +{ + using ISkinnedMesh = irr::scene::ISkinnedMesh; + const auto mesh = loadMesh(model_stem + "simple_skin.gltf"); + REQUIRE(mesh != nullptr); + auto csm = dynamic_cast(mesh); + const auto joints = csm->getAllJoints(); + REQUIRE(joints.size() == 3); + + const auto findJoint = [&](const std::function &predicate) { + for (std::size_t i = 0; i < joints.size(); ++i) { + if (predicate(joints[i])) { + return joints[i]; + } + } + throw std::runtime_error("joint not found"); + }; + + // Check the node hierarchy + const auto parent = findJoint([](auto joint) { + return !joint->Children.empty(); + }); + REQUIRE(parent->Children.size() == 1); + const auto child = parent->Children[0]; + REQUIRE(child != parent); + + SECTION("transformations are correct") + { + CHECK(parent->Animatedposition == v3f(0, 0, 0)); + CHECK(parent->Animatedrotation == irr::core::quaternion()); + CHECK(parent->Animatedscale == v3f(1, 1, 1)); + CHECK(parent->GlobalInversedMatrix == irr::core::matrix4()); + const v3f childTranslation(0, 1, 0); + CHECK(child->Animatedposition == childTranslation); + CHECK(child->Animatedrotation == irr::core::quaternion()); + CHECK(child->Animatedscale == v3f(1, 1, 1)); + irr::core::matrix4 inverseBindMatrix; + inverseBindMatrix.setInverseTranslation(childTranslation); + CHECK(child->GlobalInversedMatrix == inverseBindMatrix); + } + + SECTION("weights are correct") + { + const auto weights = [&](const ISkinnedMesh::SJoint *joint) { + std::unordered_map weights; + for (std::size_t i = 0; i < joint->Weights.size(); ++i) { + const auto weight = joint->Weights[i]; + REQUIRE(weight.buffer_id == 0); + weights[weight.vertex_id] = weight.strength; + } + return weights; + }; + const auto parentWeights = weights(parent); + const auto childWeights = weights(child); + + const auto checkWeights = [&](irr::u32 index, irr::f32 parentWeight, irr::f32 childWeight) { + const auto getWeight = [](auto weights, auto index) { + const auto it = weights.find(index); + return it == weights.end() ? 0.0f : it->second; + }; + CHECK(getWeight(parentWeights, index) == parentWeight); + CHECK(getWeight(childWeights, index) == childWeight); + }; + checkWeights(0, 1.00, 0.00); + checkWeights(1, 1.00, 0.00); + checkWeights(2, 0.75, 0.25); + checkWeights(3, 0.75, 0.25); + checkWeights(4, 0.50, 0.50); + checkWeights(5, 0.50, 0.50); + checkWeights(6, 0.25, 0.75); + checkWeights(7, 0.25, 0.75); + checkWeights(8, 0.00, 1.00); + checkWeights(9, 0.00, 1.00); + } + + SECTION("there should be a third node not involved in skinning") + { + const auto other = findJoint([&](auto joint) { + return joint != child && joint != parent; + }); + CHECK(other->Weights.empty()); + } +} + +driver->closeDevice(); +driver->drop(); +} diff --git a/src/unittest/test_irrptr.cpp b/src/unittest/test_irrptr.cpp index befeefc73..61d1e302c 100644 --- a/src/unittest/test_irrptr.cpp +++ b/src/unittest/test_irrptr.cpp @@ -21,6 +21,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "exceptions.h" #include "irr_ptr.h" +#include "IReferenceCounted.h" class TestIrrPtr : public TestBase { diff --git a/src/unittest/test_logging.cpp b/src/unittest/test_logging.cpp new file mode 100644 index 000000000..dcfe5ec83 --- /dev/null +++ b/src/unittest/test_logging.cpp @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "test.h" +#include "log_internal.h" + +using std::ostream; + +class TestLogging : public TestBase +{ +public: + TestLogging() { TestManager::registerTestModule(this); } + const char *getName() { return "TestLogging"; } + + void runTests(IGameDef *gamedef); + + void testNullChecks(); + void testBitCheck(); +}; + +static TestLogging g_test_instance; + +void TestLogging::runTests(IGameDef *gamedef) +{ + TEST(testNullChecks); + TEST(testBitCheck); +} + +//////////////////////////////////////////////////////////////////////////////// + +void TestLogging::testNullChecks() +{ + CaptureLogOutput capture(g_logger); + + infostream << "Test char*: " << (char*)0 << std::endl; + infostream << "Test signed char*: " << (signed char*)0 << std::endl; + infostream << "Test unsigned char*: " << (unsigned char*)0 << std::endl; + + infostream << "Test const char*: " << (const char*)0 << std::endl; + infostream << "Test const signed char*: " << (const signed char*)0 << std::endl; + infostream << "Test const unsigned char*: " << (const unsigned char*)0 << std::endl; + + + auto logs = capture.take(); + UASSERTEQ(size_t, logs.size(), 6); + UASSERTEQ(std::string, logs[0].text, "Test char*: (null)"); + UASSERTEQ(std::string, logs[1].text, "Test signed char*: (null)"); + UASSERTEQ(std::string, logs[2].text, "Test unsigned char*: (null)"); + UASSERTEQ(std::string, logs[3].text, "Test const char*: (null)"); + UASSERTEQ(std::string, logs[4].text, "Test const signed char*: (null)"); + UASSERTEQ(std::string, logs[5].text, "Test const unsigned char*: (null)"); +} + +namespace { + class ForceEofBit {}; + class ForceFailBit {}; + class ForceBadBit {}; + + ostream& operator<<(ostream& os, ForceEofBit) + { + os.setstate(std::ios::eofbit); + return os; + } + + ostream& operator<<(ostream& os, ForceFailBit) + { + os.setstate(std::ios::failbit); + return os; + } + + ostream& operator<<(ostream& os, ForceBadBit) + { + os.setstate(std::ios::badbit); + return os; + } +} + +void TestLogging::testBitCheck() +{ + CaptureLogOutput capture(g_logger); + + infostream << "EOF is " << ForceEofBit{} << std::endl; + infostream << "Fail is " << ForceFailBit{} << std::endl; + infostream << "Bad is " << ForceBadBit{} << std::endl; + + auto logs = capture.take(); + UASSERTEQ(size_t, logs.size(), 3); + UASSERTEQ(std::string, logs[0].text, "EOF is (ostream:eofbit)"); + UASSERTEQ(std::string, logs[1].text, "Fail is (ostream:failbit)"); + UASSERTEQ(std::string, logs[2].text, "Bad is (ostream:badbit)"); +} diff --git a/src/unittest/test_moveaction.cpp b/src/unittest/test_moveaction.cpp index 966cea8c7..b3e49341e 100644 --- a/src/unittest/test_moveaction.cpp +++ b/src/unittest/test_moveaction.cpp @@ -68,6 +68,8 @@ void TestMoveAction::runTests(IGameDef *gamedef) auto null_map = std::unique_ptr(); ServerEnvironment server_env(std::move(null_map), &server, &mb); MockServerActiveObject obj(&server_env); + obj.setId(1); + server.getScriptIface()->addObjectReference(&obj); TEST(testMove, &obj, gamedef); TEST(testMoveFillStack, &obj, gamedef); @@ -82,6 +84,8 @@ void TestMoveAction::runTests(IGameDef *gamedef) TEST(testCallbacks, &obj, &server); TEST(testCallbacksSwap, &obj, &server); + + server.getScriptIface()->removeObjectReference(&obj); } static ItemStack parse_itemstack(const char *s) diff --git a/src/unittest/test_nodedef.cpp b/src/unittest/test_nodedef.cpp index acf669783..85dadd17d 100644 --- a/src/unittest/test_nodedef.cpp +++ b/src/unittest/test_nodedef.cpp @@ -17,38 +17,22 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "test.h" - -#include - #include "gamedef.h" #include "nodedef.h" #include "network/networkprotocol.h" -class TestNodeDef : public TestBase -{ -public: - TestNodeDef() { TestManager::registerTestModule(this); } - const char *getName() { return "TestNodeDef"; } +#include - void runTests(IGameDef *gamedef); +#include +#include - void testContentFeaturesSerialization(); -}; -static TestNodeDef g_test_instance; - -void TestNodeDef::runTests(IGameDef *gamedef) -{ - TEST(testContentFeaturesSerialization); -} - -//////////////////////////////////////////////////////////////////////////////// - -void TestNodeDef::testContentFeaturesSerialization() +TEST_CASE("Given a node definition, " + "when we serialize and then deserialize it, " + "then the deserialized one should be equal to the original.", + "[nodedef]") { ContentFeatures f; - f.name = "default:stone"; for (TileDef &tiledef : f.tiledef) tiledef.name = "default_stone.png"; @@ -56,12 +40,10 @@ void TestNodeDef::testContentFeaturesSerialization() std::ostringstream os(std::ios::binary); f.serialize(os, LATEST_PROTOCOL_VERSION); - // verbosestream<<"Test ContentFeatures size: "<size() == 3); + CHECK((*form)(0) == 0); + CHECK((*form)(1) == 0); + CHECK((*form)(2) == 1); + } + + SECTION("PO file parser") + { + Translations translations; + translations.loadTranslation(TEST_PO_NAME, read_translation_file(TEST_PO_NAME)); + + CHECK(translations.size() == 5); + CHECK(translations.getTranslation(TEXTDOMAIN_PO, L"foo") == L"bar"); + CHECK(translations.getTranslation(TEXTDOMAIN_PO, L"Untranslated") == L"Untranslated"); + CHECK(translations.getTranslation(TEXTDOMAIN_PO, L"Fuzzy") == L"Fuzzy"); + CHECK(translations.getTranslation(TEXTDOMAIN_PO, L"Multi\\line\nstring") == L"Multi\\\"li\\ne\nresult"); + CHECK(translations.getTranslation(TEXTDOMAIN_PO, L"Wrong order") == L"Wrong order"); + CHECK(translations.getPluralTranslation(TEXTDOMAIN_PO, L"Plural form", 1) == L"Singular result"); + CHECK(translations.getPluralTranslation(TEXTDOMAIN_PO, L"Singular form", 0) == L"Plural result"); + CHECK(translations.getPluralTranslation(TEXTDOMAIN_PO, L"Partial translation", 1) == L"Partially translated"); + CHECK(translations.getPluralTranslation(TEXTDOMAIN_PO, L"Partial translations", 2) == L"Partial translations"); + CHECK(translations.getTranslation(CONTEXT, L"With context") == L"Has context"); + } + + SECTION("MO file parser") + { + Translations translations; + translations.loadTranslation(TEST_MO_NAME, read_translation_file(TEST_MO_NAME)); + + CHECK(translations.size() == 2); + CHECK(translations.getTranslation(CONTEXT, L"With context") == L"Has context"); + CHECK(translations.getPluralTranslation(CONTEXT, L"Plural form", 1) == L"Singular result"); + CHECK(translations.getPluralTranslation(CONTEXT, L"Singular form", 0) == L"Plural result"); + } +} diff --git a/src/unittest/test_utilities.cpp b/src/unittest/test_utilities.cpp index 996b418e3..a05421c9b 100644 --- a/src/unittest/test_utilities.cpp +++ b/src/unittest/test_utilities.cpp @@ -61,6 +61,7 @@ public: void testSanitizeDirName(); void testIsBlockInSight(); void testColorizeURL(); + void testSanitizeUntrusted(); }; static TestUtilities g_test_instance; @@ -95,6 +96,7 @@ void TestUtilities::runTests(IGameDef *gamedef) TEST(testSanitizeDirName); TEST(testIsBlockInSight); TEST(testColorizeURL); + TEST(testSanitizeUntrusted); } //////////////////////////////////////////////////////////////////////////////// @@ -743,3 +745,28 @@ void TestUtilities::testColorizeURL() warningstream << "Test skipped." << std::endl; #endif } + +void TestUtilities::testSanitizeUntrusted() +{ + std::string_view t1{u8"Anästhesieausrüstung"}; + UASSERTEQ(auto, sanitize_untrusted(t1), t1); + + std::string_view t2{"stop\x00here", 9}; + UASSERTEQ(auto, sanitize_untrusted(t2), "stop"); + + UASSERTEQ(auto, sanitize_untrusted("\x01\x08\x13\x1dhello\r\n\tworld"), "hello\n\tworld"); + + std::string_view t3{"some \x1b(T@whatever)text\x1b" "E here"}; + UASSERTEQ(auto, sanitize_untrusted(t3), t3); + auto t3_sanitized = sanitize_untrusted(t3, false); + UASSERT(str_starts_with(t3_sanitized, "some ") && str_ends_with(t3_sanitized, " here")); + UASSERT(t3_sanitized.find('\x1b') == std::string::npos); + + UASSERTEQ(auto, sanitize_untrusted("\x1b[31m"), "[31m"); + + // edge cases + for (bool keep : {true, false}) { + UASSERTEQ(auto, sanitize_untrusted("\x1b", keep), ""); + UASSERTEQ(auto, sanitize_untrusted("\x1b(", keep), "("); + } +} diff --git a/src/unittest/test_voxelarea.cpp b/src/unittest/test_voxelarea.cpp index 386fe499c..1139e72fa 100644 --- a/src/unittest/test_voxelarea.cpp +++ b/src/unittest/test_voxelarea.cpp @@ -38,6 +38,7 @@ public: void test_equal(); void test_plus(); void test_minor(); + void test_diff(); void test_intersect(); void test_index_xyz_all_pos(); void test_index_xyz_x_neg(); @@ -75,6 +76,7 @@ void TestVoxelArea::runTests(IGameDef *gamedef) TEST(test_equal); TEST(test_plus); TEST(test_minor); + TEST(test_diff); TEST(test_intersect); TEST(test_index_xyz_all_pos); TEST(test_index_xyz_x_neg); @@ -100,21 +102,21 @@ void TestVoxelArea::runTests(IGameDef *gamedef) void TestVoxelArea::test_addarea() { - VoxelArea v1(v3s16(-1447, 8854, -875), v3s16(-147, -9547, 669)); - VoxelArea v2(v3s16(-887, 4445, -5478), v3s16(447, -8779, 4778)); + VoxelArea v1(v3s16(-1447, -9547, -875), v3s16(-147, 8854, 669)); + VoxelArea v2(v3s16(-887, -8779, -5478), v3s16(447, 4445, 4778)); v1.addArea(v2); - UASSERT(v1.MinEdge == v3s16(-1447, 4445, -5478)); - UASSERT(v1.MaxEdge == v3s16(447, -8779, 4778)); + UASSERT(v1.MinEdge == v3s16(-1447, -9547, -5478)); + UASSERT(v1.MaxEdge == v3s16(447, 8854, 4778)); } void TestVoxelArea::test_pad() { - VoxelArea v1(v3s16(-1447, 8854, -875), v3s16(-147, -9547, 669)); + VoxelArea v1(v3s16(-1447, -9547, -875), v3s16(-147, 8854, 669)); v1.pad(v3s16(100, 200, 300)); - UASSERT(v1.MinEdge == v3s16(-1547, 8654, -1175)); - UASSERT(v1.MaxEdge == v3s16(-47, -9347, 969)); + UASSERT(v1.MinEdge == v3s16(-1547, -9747, -1175)); + UASSERT(v1.MaxEdge == v3s16(-47, 9054, 969)); } void TestVoxelArea::test_extent() @@ -124,6 +126,9 @@ void TestVoxelArea::test_extent() VoxelArea v2(v3s16(32493, -32507, 32752), v3s16(32508, -32492, 32767)); UASSERT(v2.getExtent() == v3s16(16, 16, 16)); + + UASSERT(VoxelArea({2,3,4}, {1,2,3}).hasEmptyExtent()); + UASSERT(VoxelArea({2,3,4}, {2,2,3}).hasEmptyExtent() == false); } void TestVoxelArea::test_volume() @@ -133,6 +138,9 @@ void TestVoxelArea::test_volume() VoxelArea v2(v3s16(32493, -32507, 32752), v3s16(32508, -32492, 32767)); UASSERTEQ(s32, v2.getVolume(), 4096); + + UASSERTEQ(s32, VoxelArea({2,3,4}, {1,2,3}).getVolume(), 0); + UASSERTEQ(s32, VoxelArea({2,3,4}, {2,2,3}).getVolume(), 0); } void TestVoxelArea::test_contains_voxelarea() @@ -185,8 +193,7 @@ void TestVoxelArea::test_equal() VoxelArea v1(v3s16(-1337, -9547, -789), v3s16(-147, 750, 669)); UASSERTEQ(bool, v1 == VoxelArea(v3s16(-1337, -9547, -789), v3s16(-147, 750, 669)), true); - UASSERTEQ(bool, v1 == VoxelArea(v3s16(0, 0, 0), v3s16(-147, 750, 669)), false); - UASSERTEQ(bool, v1 == VoxelArea(v3s16(0, 0, 0), v3s16(-147, 750, 669)), false); + UASSERTEQ(bool, v1 == VoxelArea(v3s16(-147, 0, 0), v3s16(0, 750, 669)), false); UASSERTEQ(bool, v1 == VoxelArea(v3s16(0, 0, 0), v3s16(0, 0, 0)), false); } @@ -212,6 +219,30 @@ void TestVoxelArea::test_minor() VoxelArea(v3s16(-10, -10, -45), v3s16(100, 100, 65))); } +void TestVoxelArea::test_diff() +{ + const VoxelArea v1({-10, -10, -10}, {100, 100, 100}); + std::vector res; + + v1.diff(VoxelArea({-10, -10, -10}, {99, 100, 100}), res); + UASSERTEQ(auto, res.size(), 1U); + UASSERT(res[0] == VoxelArea({100, -10, -10}, {100, 100, 100})); + res.clear(); + + v1.diff(VoxelArea({-10, -10, -10}, {100, 50, 80}), res); + UASSERTEQ(auto, res.size(), 2U); + UASSERT(res[0] == VoxelArea({-10, -10, 81}, {100, 100, 100})); + UASSERT(res[1] == VoxelArea({-10, 51, -10}, {100, 100, 80})); + res.clear(); + + // edge cases + v1.diff(v1, res); + UASSERT(res.empty()); + v1.diff(VoxelArea(), res); + UASSERTEQ(auto, res.size(), 1U); + UASSERT(res[0] == v1); +} + void TestVoxelArea::test_intersect() { VoxelArea v1({-10, -10, -10}, {10, 10, 10}); @@ -231,8 +262,8 @@ void TestVoxelArea::test_index_xyz_all_pos() VoxelArea v1; UASSERTEQ(s32, v1.index(156, 25, 236), 155); - VoxelArea v2(v3s16(756, 8854, -875), v3s16(-147, -9547, 669)); - UASSERTEQ(s32, v2.index(156, 25, 236), 1267138774); + VoxelArea v2(v3s16(-147, -9547, -875), v3s16(756, 8854, 669)); + UASSERTEQ(s32, v2.index(156, 25, 236), 1310722495); } void TestVoxelArea::test_index_xyz_x_neg() @@ -240,8 +271,8 @@ void TestVoxelArea::test_index_xyz_x_neg() VoxelArea v1; UASSERTEQ(s32, v1.index(-147, 25, 366), -148); - VoxelArea v2(v3s16(756, 8854, -875), v3s16(-147, -9547, 669)); - UASSERTEQ(s32, v2.index(-147, 25, 366), -870244825); + VoxelArea v2(v3s16(-147, -9547, -875), v3s16(756, 8854, 669)); + UASSERTEQ(s32, v2.index(-147, 25, 366), -821642064); } void TestVoxelArea::test_index_xyz_y_neg() @@ -249,8 +280,8 @@ void TestVoxelArea::test_index_xyz_y_neg() VoxelArea v1; UASSERTEQ(s32, v1.index(247, -269, 100), 246); - VoxelArea v2(v3s16(756, 8854, -875), v3s16(-147, -9547, 669)); - UASSERTEQ(s32, v2.index(247, -269, 100), -989760747); + VoxelArea v2(v3s16(-147, -9547, -875), v3s16(756, 8854, 669)); + UASSERTEQ(s32, v2.index(247, -269, 100), -951958678); } void TestVoxelArea::test_index_xyz_z_neg() @@ -258,8 +289,8 @@ void TestVoxelArea::test_index_xyz_z_neg() VoxelArea v1; UASSERTEQ(s32, v1.index(244, 336, -887), 243); - VoxelArea v2(v3s16(756, 8854, -875), v3s16(-147, -9547, 669)); - UASSERTEQ(s32, v2.index(244, 336, -887), -191478876); + VoxelArea v2(v3s16(-147, -9547, -875), v3s16(756, 8854, 669)); + UASSERTEQ(s32, v2.index(244, 336, -887), -190690273); } void TestVoxelArea::test_index_xyz_xy_neg() @@ -267,8 +298,8 @@ void TestVoxelArea::test_index_xyz_xy_neg() VoxelArea v1; UASSERTEQ(s32, v1.index(-365, -47, 6978), -366); - VoxelArea v2(v3s16(756, 8854, -875), v3s16(-147, -9547, 669)); - UASSERTEQ(s32, v2.index(-365, -47, 6978), 1493679101); + VoxelArea v2(v3s16(-147, -9547, -875), v3s16(756, 8854, 669)); + UASSERTEQ(s32, v2.index(-365, -47, 6978), 1797427926); } void TestVoxelArea::test_index_xyz_yz_neg() @@ -276,8 +307,8 @@ void TestVoxelArea::test_index_xyz_yz_neg() VoxelArea v1; UASSERTEQ(s32, v1.index(66, -58, -789), 65); - VoxelArea v2(v3s16(756, 8854, -875), v3s16(-147, -9547, 669)); - UASSERTEQ(s32, v2.index(66, -58, -789), 1435362734); + VoxelArea v2(v3s16(-147, -9547, -875), v3s16(756, 8854, 669)); + UASSERTEQ(s32, v2.index(66, -58, -789), 1439223357); } void TestVoxelArea::test_index_xyz_xz_neg() @@ -285,8 +316,8 @@ void TestVoxelArea::test_index_xyz_xz_neg() VoxelArea v1; UASSERTEQ(s32, v1.index(-36, 589, -992), -37); - VoxelArea v2(v3s16(756, 8854, -875), v3s16(-147, -9547, 669)); - UASSERTEQ(s32, v2.index(-36, 589, -992), -1934371362); + VoxelArea v2(v3s16(-147, -9547, -875), v3s16(756, 8854, 669)); + UASSERTEQ(s32, v2.index(-36, 589, -992), -1937179681); } void TestVoxelArea::test_index_xyz_all_neg() @@ -294,8 +325,8 @@ void TestVoxelArea::test_index_xyz_all_neg() VoxelArea v1; UASSERTEQ(s32, v1.index(-88, -99, -1474), -89); - VoxelArea v2(v3s16(756, 8854, -875), v3s16(-147, -9547, 669)); - UASSERTEQ(s32, v2.index(-88, -99, -1474), -1343473846); + VoxelArea v2(v3s16(-147, -9547, -875), v3s16(756, 8854, 669)); + UASSERTEQ(s32, v2.index(-88, -99, -1474), -1366133749); } void TestVoxelArea::test_index_v3s16_all_pos() @@ -303,8 +334,8 @@ void TestVoxelArea::test_index_v3s16_all_pos() VoxelArea v1; UASSERTEQ(s32, v1.index(v3s16(156, 25, 236)), 155); - VoxelArea v2(v3s16(756, 8854, -875), v3s16(-147, -9547, 669)); - UASSERTEQ(s32, v2.index(v3s16(156, 25, 236)), 1267138774); + VoxelArea v2(v3s16(-147, -9547, -875), v3s16(756, 8854, 669)); + UASSERTEQ(s32, v2.index(v3s16(156, 25, 236)), 1310722495); } void TestVoxelArea::test_index_v3s16_x_neg() @@ -312,8 +343,8 @@ void TestVoxelArea::test_index_v3s16_x_neg() VoxelArea v1; UASSERTEQ(s32, v1.index(v3s16(-147, 25, 366)), -148); - VoxelArea v2(v3s16(756, 8854, -875), v3s16(-147, -9547, 669)); - UASSERTEQ(s32, v2.index(v3s16(-147, 25, 366)), -870244825); + VoxelArea v2(v3s16(-147, -9547, -875), v3s16(756, 8854, 669)); + UASSERTEQ(s32, v2.index(v3s16(-147, 25, 366)), -821642064); } void TestVoxelArea::test_index_v3s16_y_neg() @@ -321,8 +352,8 @@ void TestVoxelArea::test_index_v3s16_y_neg() VoxelArea v1; UASSERTEQ(s32, v1.index(v3s16(247, -269, 100)), 246); - VoxelArea v2(v3s16(756, 8854, -875), v3s16(-147, -9547, 669)); - UASSERTEQ(s32, v2.index(v3s16(247, -269, 100)), -989760747); + VoxelArea v2(v3s16(-147, -9547, -875), v3s16(756, 8854, 669)); + UASSERTEQ(s32, v2.index(v3s16(247, -269, 100)), -951958678); } void TestVoxelArea::test_index_v3s16_z_neg() @@ -330,8 +361,8 @@ void TestVoxelArea::test_index_v3s16_z_neg() VoxelArea v1; UASSERTEQ(s32, v1.index(v3s16(244, 336, -887)), 243); - VoxelArea v2(v3s16(756, 8854, -875), v3s16(-147, -9547, 669)); - UASSERTEQ(s32, v2.index(v3s16(244, 336, -887)), -191478876); + VoxelArea v2(v3s16(-147, -9547, -875), v3s16(756, 8854, 669)); + UASSERTEQ(s32, v2.index(v3s16(244, 336, -887)), -190690273); } void TestVoxelArea::test_index_v3s16_xy_neg() @@ -339,8 +370,8 @@ void TestVoxelArea::test_index_v3s16_xy_neg() VoxelArea v1; UASSERTEQ(s32, v1.index(v3s16(-365, -47, 6978)), -366); - VoxelArea v2(v3s16(756, 8854, -875), v3s16(-147, -9547, 669)); - UASSERTEQ(s32, v2.index(v3s16(-365, -47, 6978)), 1493679101); + VoxelArea v2(v3s16(-147, -9547, -875), v3s16(756, 8854, 669)); + UASSERTEQ(s32, v2.index(v3s16(-365, -47, 6978)), 1797427926); } void TestVoxelArea::test_index_v3s16_yz_neg() @@ -348,8 +379,8 @@ void TestVoxelArea::test_index_v3s16_yz_neg() VoxelArea v1; UASSERTEQ(s32, v1.index(v3s16(66, -58, -789)), 65); - VoxelArea v2(v3s16(756, 8854, -875), v3s16(-147, -9547, 669)); - UASSERTEQ(s32, v2.index(v3s16(66, -58, -789)), 1435362734); + VoxelArea v2(v3s16(-147, -9547, -875), v3s16(756, 8854, 669)); + UASSERTEQ(s32, v2.index(v3s16(66, -58, -789)), 1439223357); } void TestVoxelArea::test_index_v3s16_xz_neg() @@ -357,8 +388,8 @@ void TestVoxelArea::test_index_v3s16_xz_neg() VoxelArea v1; UASSERTEQ(s32, v1.index(v3s16(-36, 589, -992)), -37); - VoxelArea v2(v3s16(756, 8854, -875), v3s16(-147, -9547, 669)); - UASSERTEQ(s32, v2.index(v3s16(-36, 589, -992)), -1934371362); + VoxelArea v2(v3s16(-147, -9547, -875), v3s16(756, 8854, 669)); + UASSERTEQ(s32, v2.index(v3s16(-36, 589, -992)), -1937179681); } void TestVoxelArea::test_index_v3s16_all_neg() @@ -366,8 +397,8 @@ void TestVoxelArea::test_index_v3s16_all_neg() VoxelArea v1; UASSERTEQ(s32, v1.index(v3s16(-88, -99, -1474)), -89); - VoxelArea v2(v3s16(756, 8854, -875), v3s16(-147, -9547, 669)); - UASSERTEQ(s32, v2.index(v3s16(-88, -99, -1474)), -1343473846); + VoxelArea v2(v3s16(-147, -9547, -875), v3s16(756, 8854, 669)); + UASSERTEQ(s32, v2.index(v3s16(-88, -99, -1474)), -1366133749); } void TestVoxelArea::test_add_x() diff --git a/src/unittest/test_voxelmanipulator.cpp b/src/unittest/test_voxelmanipulator.cpp index acc2707e7..6ea0ca9af 100644 --- a/src/unittest/test_voxelmanipulator.cpp +++ b/src/unittest/test_voxelmanipulator.cpp @@ -87,7 +87,7 @@ void TestVoxelManipulator::testVoxelManipulator(const NodeDefManager *nodedef) v.print(infostream, nodedef); infostream << "*** Setting (-1,0,-1)=2 ***" << std::endl; - v.setNodeNoRef(v3s16(-1,0,-1), MapNode(t_CONTENT_GRASS)); + v.setNode(v3s16(-1,0,-1), MapNode(t_CONTENT_GRASS)); v.print(infostream, nodedef); UASSERT(v.getNode(v3s16(-1,0,-1)).getContent() == t_CONTENT_GRASS); diff --git a/src/util/basic_macros.h b/src/util/basic_macros.h index 7c3746417..f0fd4eb48 100644 --- a/src/util/basic_macros.h +++ b/src/util/basic_macros.h @@ -28,6 +28,12 @@ with this program; if not, write to the Free Software Foundation, Inc., // Requires #define CONTAINS(c, v) (std::find((c).begin(), (c).end(), (v)) != (c).end()) +// Requires +#define SORT_AND_UNIQUE(c) do { \ + std::sort((c).begin(), (c).end()); \ + (c).erase(std::unique((c).begin(), (c).end()), (c).end()); \ + } while (0) + // To disable copy constructors and assignment operations for some class // 'Foobar', add the macro DISABLE_CLASS_COPY(Foobar) in the class definition. // Note this also disables copying for any classes derived from 'Foobar' as well diff --git a/src/util/colorize.cpp b/src/util/colorize.cpp index 873ec06fc..0814c2d34 100644 --- a/src/util/colorize.cpp +++ b/src/util/colorize.cpp @@ -23,6 +23,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #include "log.h" #include "string.h" #include +#include std::string colorize_url(const std::string &url) { diff --git a/src/util/enriched_string.cpp b/src/util/enriched_string.cpp index 04c5ef806..09baeac29 100644 --- a/src/util/enriched_string.cpp +++ b/src/util/enriched_string.cpp @@ -29,7 +29,7 @@ EnrichedString::EnrichedString() clear(); } -EnrichedString::EnrichedString(const std::wstring &string, +EnrichedString::EnrichedString(std::wstring_view string, const std::vector &colors) { clear(); @@ -37,18 +37,12 @@ EnrichedString::EnrichedString(const std::wstring &string, m_colors = colors; } -EnrichedString::EnrichedString(const std::wstring &s, const SColor &color) +EnrichedString::EnrichedString(std::wstring_view s, const SColor &color) { clear(); addAtEnd(translate_string(s), color); } -EnrichedString::EnrichedString(const wchar_t *str, const SColor &color) -{ - clear(); - addAtEnd(translate_string(std::wstring(str)), color); -} - void EnrichedString::clear() { m_string.clear(); @@ -59,19 +53,20 @@ void EnrichedString::clear() m_background = irr::video::SColor(0, 0, 0, 0); } -EnrichedString &EnrichedString::operator=(const wchar_t *str) +EnrichedString &EnrichedString::operator=(std::wstring_view str) { clear(); - addAtEnd(translate_string(std::wstring(str)), m_default_color); + addAtEnd(translate_string(str), m_default_color); return *this; } -void EnrichedString::addAtEnd(const std::wstring &s, SColor initial_color) +void EnrichedString::addAtEnd(std::wstring_view s, SColor initial_color) { SColor color(initial_color); bool use_default = (m_default_length == m_string.size() && color == m_default_color); + m_string.reserve(m_string.size() + s.size()); m_colors.reserve(m_colors.size() + s.size()); size_t i = 0; @@ -142,7 +137,7 @@ void EnrichedString::addCharNoColor(wchar_t c) if (m_colors.empty()) { m_colors.emplace_back(m_default_color); } else { - m_colors.push_back(m_colors[m_colors.size() - 1]); + m_colors.push_back(m_colors.back()); } } @@ -203,11 +198,6 @@ EnrichedString EnrichedString::substr(size_t pos, size_t len) const return str; } -const wchar_t *EnrichedString::c_str() const -{ - return m_string.c_str(); -} - const std::vector &EnrichedString::getColors() const { return m_colors; diff --git a/src/util/enriched_string.h b/src/util/enriched_string.h index 3d19eaed5..18fd967bc 100644 --- a/src/util/enriched_string.h +++ b/src/util/enriched_string.h @@ -20,6 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once #include +#include #include #include @@ -28,17 +29,15 @@ using namespace irr; class EnrichedString { public: EnrichedString(); - EnrichedString(const std::wstring &s, + EnrichedString(std::wstring_view s, const video::SColor &color = video::SColor(255, 255, 255, 255)); - EnrichedString(const wchar_t *str, - const video::SColor &color = video::SColor(255, 255, 255, 255)); - EnrichedString(const std::wstring &string, + EnrichedString(std::wstring_view string, const std::vector &colors); - EnrichedString &operator=(const wchar_t *str); + EnrichedString &operator=(std::wstring_view s); void clear(); - void addAtEnd(const std::wstring &s, video::SColor color); + void addAtEnd(std::wstring_view s, video::SColor color); // Adds the character source[i] at the end. // An EnrichedString should always be able to be copied @@ -51,9 +50,18 @@ public: EnrichedString getNextLine(size_t *pos) const; EnrichedString substr(size_t pos = 0, size_t len = std::string::npos) const; + EnrichedString operator+(const EnrichedString &other) const; void operator+=(const EnrichedString &other); - const wchar_t *c_str() const; + void operator+=(std::wstring_view other) + { + *this += EnrichedString(other); + } + + const wchar_t *c_str() const + { + return getString().c_str(); + } const std::vector &getColors() const; const std::wstring &getString() const; @@ -106,6 +114,5 @@ private: video::SColor m_default_color; video::SColor m_background; // This variable defines the length of the default-colored text. - // Change this to a std::vector if an "end coloring" tag is wanted. size_t m_default_length = 0; }; diff --git a/src/util/numeric.h b/src/util/numeric.h index 73b4fb5cd..758b55968 100644 --- a/src/util/numeric.h +++ b/src/util/numeric.h @@ -28,6 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "SColor.h" #include #include +#include #define rangelim(d, min, max) ((d) < (min) ? (min) : ((d) > (max) ? (max) : (d))) #define myfloor(x) ((x) < 0.0 ? (int)(x) - 1 : (int)(x)) @@ -179,7 +180,9 @@ struct MeshGrid { /// @brief Returns true if p is an origin of a cell in the grid. bool isMeshPos(v3s16 &p) const { - return ((p.X + p.Y + p.Z) % cell_size) == 0; + return p.X % cell_size == 0 + && p.Y % cell_size == 0 + && p.Z % cell_size == 0; } /// @brief Returns index of the given offset in a grid cell diff --git a/src/util/pointer.h b/src/util/pointer.h index 528897a1c..b7fad4fe1 100644 --- a/src/util/pointer.h +++ b/src/util/pointer.h @@ -273,9 +273,7 @@ public: void grab() noexcept { ++m_refcount; } void drop() noexcept { if (--m_refcount == 0) delete this; } - // Preserve own reference count. - IntrusiveReferenceCounted(const IntrusiveReferenceCounted &) {} - IntrusiveReferenceCounted &operator=(const IntrusiveReferenceCounted &) { return *this; } + DISABLE_CLASS_COPY(IntrusiveReferenceCounted) private: u32 m_refcount = 1; }; diff --git a/src/util/srp.cpp b/src/util/srp.cpp index 56b2aa763..b1dfa76a4 100644 --- a/src/util/srp.cpp +++ b/src/util/srp.cpp @@ -51,6 +51,7 @@ #endif #include "my_sha256.h" +#include "porting.h" #include "srp.h" //#define CSRP_USE_SHA1 @@ -70,26 +71,6 @@ printf("\n"); }*/ -static int g_initialized = 0; - -#define RAND_BUFF_MAX 128 -static unsigned int g_rand_idx; -static unsigned char g_rand_buff[RAND_BUFF_MAX]; - -void *(*srp_alloc)(size_t) = &malloc; -void *(*srp_realloc)(void *, size_t) = &realloc; -void (*srp_free)(void *) = &free; - -void srp_set_memory_functions( - void *(*new_srp_alloc)(size_t), - void *(*new_srp_realloc)(void *, size_t), - void (*new_srp_free)(void *)) -{ - srp_alloc = new_srp_alloc; - srp_realloc = new_srp_realloc; - srp_free = new_srp_free; -} - typedef struct { mpz_t N; mpz_t g; @@ -189,13 +170,13 @@ static void delete_ng(NGConstant *ng) if (ng) { mpz_clear(ng->N); mpz_clear(ng->g); - srp_free(ng); + free(ng); } } static NGConstant *new_ng(SRP_NGType ng_type, const char *n_hex, const char *g_hex) { - NGConstant *ng = (NGConstant *)srp_alloc(sizeof(NGConstant)); + NGConstant *ng = (NGConstant *)malloc(sizeof(NGConstant)); if (!ng) return 0; @@ -402,17 +383,17 @@ static SRP_Result H_nn( size_t len_n1 = mpz_num_bytes(n1); size_t len_n2 = mpz_num_bytes(n2); size_t nbytes = len_N + len_N; - unsigned char *bin = (unsigned char *)srp_alloc(nbytes); + unsigned char *bin = (unsigned char *)malloc(nbytes); if (!bin) return SRP_ERR; if (len_n1 > len_N || len_n2 > len_N) { - srp_free(bin); + free(bin); return SRP_ERR; } memset(bin, 0, nbytes); mpz_to_bin(n1, bin + (len_N - len_n1)); mpz_to_bin(n2, bin + (len_N + len_N - len_n2)); hash(alg, bin, nbytes, buff); - srp_free(bin); + free(bin); mpz_from_bin(buff, hash_length(alg), result); return SRP_OK; } @@ -422,12 +403,12 @@ static SRP_Result H_ns(mpz_t result, SRP_HashAlgorithm alg, const unsigned char { unsigned char buff[CSRP_MAX_HASH]; size_t nbytes = len_n + len_bytes; - unsigned char *bin = (unsigned char *)srp_alloc(nbytes); + unsigned char *bin = (unsigned char *)malloc(nbytes); if (!bin) return SRP_ERR; memcpy(bin, n, len_n); memcpy(bin + len_n, bytes, len_bytes); hash(alg, bin, nbytes, buff); - srp_free(bin); + free(bin); mpz_from_bin(buff, hash_length(alg), result); return SRP_OK; } @@ -454,22 +435,22 @@ static int calculate_x(mpz_t result, SRP_HashAlgorithm alg, const unsigned char static SRP_Result update_hash_n(SRP_HashAlgorithm alg, HashCTX *ctx, const mpz_t n) { size_t len = mpz_num_bytes(n); - unsigned char *n_bytes = (unsigned char *)srp_alloc(len); + unsigned char *n_bytes = (unsigned char *)malloc(len); if (!n_bytes) return SRP_ERR; mpz_to_bin(n, n_bytes); hash_update(alg, ctx, n_bytes, len); - srp_free(n_bytes); + free(n_bytes); return SRP_OK; } static SRP_Result hash_num(SRP_HashAlgorithm alg, const mpz_t n, unsigned char *dest) { int nbytes = mpz_num_bytes(n); - unsigned char *bin = (unsigned char *)srp_alloc(nbytes); + unsigned char *bin = (unsigned char *)malloc(nbytes); if (!bin) return SRP_ERR; mpz_to_bin(n, bin); hash(alg, bin, nbytes, dest); - srp_free(bin); + free(bin); return SRP_OK; } @@ -521,60 +502,23 @@ static SRP_Result calculate_H_AMK(SRP_HashAlgorithm alg, unsigned char *dest, return SRP_OK; } -static SRP_Result fill_buff() -{ - g_rand_idx = 0; - -#ifdef WIN32 - HCRYPTPROV wctx; -#else - FILE *fp = 0; -#endif - -#ifdef WIN32 - - if (!CryptAcquireContext(&wctx, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) - return SRP_ERR; - if (!CryptGenRandom(wctx, sizeof(g_rand_buff), (BYTE *)g_rand_buff)) return SRP_ERR; - if (!CryptReleaseContext(wctx, 0)) return SRP_ERR; - -#else - fp = fopen("/dev/urandom", "r"); - - if (!fp) return SRP_ERR; - - if (fread(g_rand_buff, sizeof(g_rand_buff), 1, fp) != 1) { fclose(fp); return SRP_ERR; } - if (fclose(fp)) return SRP_ERR; -#endif - return SRP_OK; -} - static SRP_Result mpz_fill_random(mpz_t num) { - // was call: BN_rand(num, 256, -1, 0); - if (RAND_BUFF_MAX - g_rand_idx < 32) - if (fill_buff() != SRP_OK) return SRP_ERR; - mpz_from_bin((const unsigned char *)(&g_rand_buff[g_rand_idx]), 32, num); - g_rand_idx += 32; + unsigned char random_buf[32]; + if (!porting::secure_rand_fill_buf(random_buf, sizeof(random_buf))) + return SRP_ERR; + mpz_from_bin(random_buf, sizeof(random_buf), num); return SRP_OK; } -static SRP_Result init_random() -{ - if (g_initialized) return SRP_OK; - SRP_Result ret = fill_buff(); - g_initialized = (ret == SRP_OK); - return ret; -} - #define srp_dbg_num(num, text) ; /*void srp_dbg_num(mpz_t num, char * prevtext) { int len_num = mpz_num_bytes(num); - char *bytes_num = (char*) srp_alloc(len_num); + char *bytes_num = (char*) malloc(len_num); mpz_to_bin(num, (unsigned char *) bytes_num); srp_dbg_data(bytes_num, len_num, prevtext); - srp_free(bytes_num); + free(bytes_num); }*/ @@ -600,18 +544,13 @@ SRP_Result srp_create_salted_verification_key( SRP_HashAlgorithm alg, if (!ng) goto error_and_exit; - if (init_random() != SRP_OK) /* Only happens once */ - goto error_and_exit; - if (*bytes_s == NULL) { size_t size_to_fill = 16; *len_s = size_to_fill; - if (RAND_BUFF_MAX - g_rand_idx < size_to_fill) - if (fill_buff() != SRP_OK) goto error_and_exit; - *bytes_s = (unsigned char *)srp_alloc(size_to_fill); + *bytes_s = (unsigned char *)malloc(size_to_fill); if (!*bytes_s) goto error_and_exit; - memcpy(*bytes_s, &g_rand_buff[g_rand_idx], size_to_fill); - g_rand_idx += size_to_fill; + if (!porting::secure_rand_fill_buf(*bytes_s, size_to_fill)) + goto error_and_exit; } if (!calculate_x( @@ -624,7 +563,7 @@ SRP_Result srp_create_salted_verification_key( SRP_HashAlgorithm alg, *len_v = mpz_num_bytes(v); - *bytes_v = (unsigned char *)srp_alloc(*len_v); + *bytes_v = (unsigned char *)malloc(*len_v); if (!*bytes_v) goto error_and_exit; @@ -673,22 +612,16 @@ struct SRPVerifier *srp_verifier_new(SRP_HashAlgorithm alg, if (!ng) goto cleanup_and_exit; - ver = (struct SRPVerifier *)srp_alloc(sizeof(struct SRPVerifier)); + ver = (struct SRPVerifier *)malloc(sizeof(struct SRPVerifier)); if (!ver) goto cleanup_and_exit; - if (init_random() != SRP_OK) { /* Only happens once */ - srp_free(ver); - ver = 0; - goto cleanup_and_exit; - } - - ver->username = (char *)srp_alloc(ulen); + ver->username = (char *)malloc(ulen); ver->hash_alg = alg; ver->ng = ng; if (!ver->username) { - srp_free(ver); + free(ver); ver = 0; goto cleanup_and_exit; } @@ -733,7 +666,7 @@ struct SRPVerifier *srp_verifier_new(SRP_HashAlgorithm alg, } *len_B = mpz_num_bytes(B); - *bytes_B = (unsigned char *)srp_alloc(*len_B); + *bytes_B = (unsigned char *)malloc(*len_B); if (!*bytes_B) { *len_B = 0; @@ -744,7 +677,7 @@ struct SRPVerifier *srp_verifier_new(SRP_HashAlgorithm alg, ver->bytes_B = *bytes_B; } else { - srp_free(ver); + free(ver); ver = 0; } @@ -761,8 +694,8 @@ cleanup_and_exit: mpz_clear(tmp3); return ver; ver_cleanup_and_exit: - srp_free(ver->username); - srp_free(ver); + free(ver->username); + free(ver); ver = 0; goto cleanup_and_exit; } @@ -771,10 +704,10 @@ void srp_verifier_delete(struct SRPVerifier *ver) { if (ver) { delete_ng(ver->ng); - srp_free(ver->username); - srp_free(ver->bytes_B); + free(ver->username); + free(ver->bytes_B); memset(ver, 0, sizeof(*ver)); - srp_free(ver); + free(ver); } } @@ -818,15 +751,12 @@ struct SRPUser *srp_user_new(SRP_HashAlgorithm alg, SRP_NGType ng_type, const unsigned char *bytes_password, size_t len_password, const char *n_hex, const char *g_hex) { - struct SRPUser *usr = (struct SRPUser *)srp_alloc(sizeof(struct SRPUser)); + struct SRPUser *usr = (struct SRPUser *)malloc(sizeof(struct SRPUser)); size_t ulen = strlen(username) + 1; size_t uvlen = strlen(username_for_verifier) + 1; if (!usr) goto err_exit; - if (init_random() != SRP_OK) /* Only happens once */ - goto err_exit; - usr->hash_alg = alg; usr->ng = new_ng(ng_type, n_hex, g_hex); @@ -836,9 +766,9 @@ struct SRPUser *srp_user_new(SRP_HashAlgorithm alg, SRP_NGType ng_type, if (!usr->ng) goto err_exit; - usr->username = (char *)srp_alloc(ulen); - usr->username_verifier = (char *)srp_alloc(uvlen); - usr->password = (unsigned char *)srp_alloc(len_password); + usr->username = (char *)malloc(ulen); + usr->username_verifier = (char *)malloc(uvlen); + usr->password = (unsigned char *)malloc(len_password); usr->password_len = len_password; if (!usr->username || !usr->password || !usr->username_verifier) goto err_exit; @@ -859,13 +789,13 @@ err_exit: mpz_clear(usr->A); mpz_clear(usr->S); delete_ng(usr->ng); - srp_free(usr->username); - srp_free(usr->username_verifier); + free(usr->username); + free(usr->username_verifier); if (usr->password) { memset(usr->password, 0, usr->password_len); - srp_free(usr->password); + free(usr->password); } - srp_free(usr); + free(usr); } return 0; @@ -882,14 +812,14 @@ void srp_user_delete(struct SRPUser *usr) memset(usr->password, 0, usr->password_len); - srp_free(usr->username); - srp_free(usr->username_verifier); - srp_free(usr->password); + free(usr->username); + free(usr->username_verifier); + free(usr->password); - if (usr->bytes_A) srp_free(usr->bytes_A); + if (usr->bytes_A) free(usr->bytes_A); memset(usr, 0, sizeof(*usr)); - srp_free(usr); + free(usr); } } @@ -928,7 +858,7 @@ SRP_Result srp_user_start_authentication(struct SRPUser *usr, char **username, mpz_powm(usr->A, usr->ng->g, usr->a, usr->ng->N); *len_A = mpz_num_bytes(usr->A); - *bytes_A = (unsigned char *)srp_alloc(*len_A); + *bytes_A = (unsigned char *)malloc(*len_A); if (!*bytes_A) goto error_and_exit; diff --git a/src/util/srp.h b/src/util/srp.h index ac66dc936..fc4d2dc89 100644 --- a/src/util/srp.h +++ b/src/util/srp.h @@ -79,15 +79,6 @@ typedef enum { SRP_OK, } SRP_Result; -/* Sets the memory functions used by srp. - * Note: this doesn't set the memory functions used by gmp, - * but it is supported to have different functions for srp and gmp. - * Don't call this after you have already allocated srp structures. - */ -void srp_set_memory_functions( - void *(*new_srp_alloc) (size_t), - void *(*new_srp_realloc) (void *, size_t), - void (*new_srp_free) (void *)); /* Out: bytes_v, len_v * diff --git a/src/util/stream.h b/src/util/stream.h index 2e61b46d2..dbbe22592 100644 --- a/src/util/stream.h +++ b/src/util/stream.h @@ -20,51 +20,58 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once #include -#include +#include #include -template > +template > class StringStreamBuffer : public std::streambuf { public: StringStreamBuffer(Emitter emitter) : m_emitter(emitter) { buffer_index = 0; } - int overflow(int c) { - push_back(c); - return c; + int overflow(int c) override { + if (c != traits_type::eof()) + push_back(c); + return 0; } void push_back(char c) { - if (c == '\n' || c == '\r') { - if (buffer_index) - m_emitter(std::string(buffer, buffer_index)); - buffer_index = 0; + // emit only complete lines, or if the buffer is full + if (c == '\n') { + sync(); } else { buffer[buffer_index++] = c; if (buffer_index >= BufferLength) { - m_emitter(std::string(buffer, buffer_index)); - buffer_index = 0; + sync(); } } } - std::streamsize xsputn(const char *s, std::streamsize n) { - for (int i = 0; i < n; ++i) + std::streamsize xsputn(const char *s, std::streamsize n) override { + for (std::streamsize i = 0; i < n; ++i) push_back(s[i]); return n; } + + int sync() override { + if (buffer_index) + m_emitter(std::string_view(buffer, buffer_index)); + buffer_index = 0; + return 0; + } + private: Emitter m_emitter; + unsigned int buffer_index; char buffer[BufferLength]; - int buffer_index; }; class DummyStreamBuffer : public std::streambuf { - int overflow(int c) { - return c; + int overflow(int c) override { + return 0; } - std::streamsize xsputn(const char *s, std::streamsize n) { + std::streamsize xsputn(const char *s, std::streamsize n) override { return n; } }; diff --git a/src/util/string.cpp b/src/util/string.cpp index 0c896e6ec..b05d993a5 100644 --- a/src/util/string.cpp +++ b/src/util/string.cpp @@ -154,6 +154,16 @@ std::string wide_to_utf8(std::wstring_view input) return out; } +void wide_add_codepoint(std::wstring &result, char32_t codepoint) +{ + if ((0xD800 <= codepoint && codepoint <= 0xDFFF) || (0x10FFFF < codepoint)) { + // Invalid codepoint, replace with unicode replacement character + result.push_back(0xFFFD); + return; + } + result.push_back(codepoint); +} + #else // _WIN32 std::wstring utf8_to_wide(std::string_view input) @@ -180,6 +190,29 @@ std::string wide_to_utf8(std::wstring_view input) return out; } +void wide_add_codepoint(std::wstring &result, char32_t codepoint) +{ + if (codepoint < 0x10000) { + if (0xD800 <= codepoint && codepoint <= 0xDFFF) { + // Invalid codepoint, part of a surrogate pair + // Replace with unicode replacement character + result.push_back(0xFFFD); + return; + } + result.push_back((wchar_t) codepoint); + return; + } + codepoint -= 0x10000; + if (codepoint >= 0x100000) { + // original codepoint was above 0x10FFFF, so invalid + // replace with unicode replacement character + result.push_back(0xFFFD); + return; + } + result.push_back((wchar_t) ((codepoint >> 10) | 0xD800)); + result.push_back((wchar_t) ((codepoint & 0x3FF) | 0xDC00)); +} + #endif // _WIN32 @@ -668,25 +701,35 @@ std::string wrap_rows(std::string_view from, unsigned row_len, bool has_color_co * We get the argument "White", translated, and create a template string with "@1" instead of it. * We finally get the template "@1 Wool" that was used in the beginning, which we translate * before filling it again. + * + * The \x1bT marking the beginning of a translated string allows two '@'-separated arguments: + * - The first one is the textdomain/context in which the string is to be translated. Most often, + * this is the name of the mod which asked for the translation. + * - The second argument, if present, should be an integer; it is used to decide which plural form + * to use, for languages containing several plural forms. */ -static void translate_all(const std::wstring &s, size_t &i, +static void translate_all(std::wstring_view s, size_t &i, Translations *translations, std::wstring &res); -static void translate_string(const std::wstring &s, Translations *translations, - const std::wstring &textdomain, size_t &i, std::wstring &res) +static void translate_string(std::wstring_view s, Translations *translations, + const std::wstring &textdomain, size_t &i, std::wstring &res, + bool use_plural, unsigned long int number) { - std::wostringstream output; std::vector args; int arg_number = 1; + + // Re-assemble the template. + std::wstring output; + output.reserve(s.length()); while (i < s.length()) { // Not an escape sequence: just add the character. if (s[i] != '\x1b') { - output.put(s[i]); + output += s[i]; // The character is a literal '@'; add it twice // so that it is not mistaken for an argument. if (s[i] == L'@') - output.put(L'@'); + output += L'@'; ++i; continue; } @@ -733,12 +776,12 @@ static void translate_string(const std::wstring &s, Translations *translations, args.push_back(arg); continue; } - output.put(L'@'); - output << arg_number; + output += L'@'; + output += std::to_wstring(arg_number); ++arg_number; std::wstring arg; translate_all(s, i, translations, arg); - args.push_back(arg); + args.push_back(std::move(arg)); } else { // This is an escape sequence *inside* the template string to translate itself. // This should not happen, show an error message. @@ -747,21 +790,27 @@ static void translate_string(const std::wstring &s, Translations *translations, } } - std::wstring toutput; // Translate the template. - if (translations != nullptr) - toutput = translations->getTranslation( - textdomain, output.str()); - else - toutput = output.str(); + std::wstring toutput; + if (translations != nullptr) { + if (use_plural) + toutput = translations->getPluralTranslation( + textdomain, output, number); + else + toutput = translations->getTranslation( + textdomain, output); + } else { + toutput = output; + } // Put back the arguments in the translated template. - std::wostringstream result; size_t j = 0; + res.clear(); + res.reserve(toutput.length()); while (j < toutput.length()) { // Normal character, add it to output and continue. if (toutput[j] != L'@' || j == toutput.length() - 1) { - result.put(toutput[j]); + res += toutput[j]; ++j; continue; } @@ -769,7 +818,7 @@ static void translate_string(const std::wstring &s, Translations *translations, ++j; // Literal escape for '@'. if (toutput[j] == L'@') { - result.put(L'@'); + res += L'@'; ++j; continue; } @@ -778,16 +827,15 @@ static void translate_string(const std::wstring &s, Translations *translations, int arg_index = toutput[j] - L'1'; ++j; if (0 <= arg_index && (size_t)arg_index < args.size()) { - result << args[arg_index]; + res += args[arg_index]; } else { // This is not allowed: show an error message errorstream << "Ignoring out-of-bounds argument escape sequence in translation" << std::endl; } } - res = result.str(); } -static void translate_all(const std::wstring &s, size_t &i, +static void translate_all(std::wstring_view s, size_t &i, Translations *translations, std::wstring &res) { res.clear(); @@ -836,10 +884,37 @@ static void translate_all(const std::wstring &s, size_t &i, } else if (parts[0] == L"T") { // Beginning of translated string. std::wstring textdomain; + bool use_plural = false; + unsigned long int number = 0; if (parts.size() > 1) textdomain = parts[1]; + if (parts.size() > 2 && parts[2] != L"") { + // parts[2] should contain a number used for selecting + // the plural form. + // However, we can't blindly cast it to an unsigned long int, + // as it might be too large for that. + // + // We follow the advice of gettext and reduce integers larger than 1000000 + // to something in the range [1000000, 2000000), with the same last 6 digits. + // + // https://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html + constexpr unsigned long int max = 1000000; + + use_plural = true; + number = 0; + for (char c : parts[2]) { + if (L'0' <= c && c <= L'9') { + number = (10 * number + (unsigned long int)(c - L'0')); + if (number >= 2 * max) number = (number % max) + max; + } else { + // Invalid number + use_plural = false; + break; + } + } + } std::wstring translated; - translate_string(s, translations, textdomain, i, translated); + translate_string(s, translations, textdomain, i, translated, use_plural, number); res.append(translated); } else { // Another escape sequence, such as colors. Preserve it. @@ -849,7 +924,7 @@ static void translate_all(const std::wstring &s, size_t &i, } // Translate string server side -std::wstring translate_string(const std::wstring &s, Translations *translations) +std::wstring translate_string(std::wstring_view s, Translations *translations) { size_t i = 0; std::wstring res; @@ -858,7 +933,7 @@ std::wstring translate_string(const std::wstring &s, Translations *translations) } // Translate string client side -std::wstring translate_string(const std::wstring &s) +std::wstring translate_string(std::wstring_view s) { #ifdef SERVER return translate_string(s, nullptr); @@ -950,6 +1025,53 @@ std::string sanitizeDirName(std::string_view str, std::string_view optional_pref return wide_to_utf8(safe_name); } +template +void remove_indexed(std::string &s, F pred) +{ + size_t j = 0; + for (size_t i = 0; i < s.length();) { + if (pred(s, i++)) + j++; + if (i != j) + s[j] = s[i]; + } + s.resize(j); +} + +std::string sanitize_untrusted(std::string_view str, bool keep_escapes) +{ + // truncate on NULL + std::string s{str.substr(0, str.find('\0'))}; + + // remove control characters except tab, feed and escape + s.erase(std::remove_if(s.begin(), s.end(), [] (unsigned char c) { + return c < 9 || (c >= 13 && c < 27) || (c >= 28 && c < 32); + }), s.end()); + + if (!keep_escapes) { + s.erase(std::remove(s.begin(), s.end(), '\x1b'), s.end()); + return s; + } + // Note: Minetest escapes generally just look like \x1b# or \x1b(###) + // where # is a single character and ### any number of characters. + // Here we additionally assume that the first character in the sequence + // is [A-Za-z], to enable us to filter foreign types of escapes that might + // be unsafe e.g. ANSI escapes in a terminal. + const auto &check = [] (char c) { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); + }; + remove_indexed(s, [&check] (const std::string &s, size_t i) { + if (s[i] != '\x1b') + return true; + if (i+1 >= s.length()) + return false; + if (s[i+1] == '(') + return i+2 < s.length() && check(s[i+2]); // long-form escape + else + return check(s[i+1]); // short-form escape + }); + return s; +} void safe_print_string(std::ostream &os, std::string_view str) { diff --git a/src/util/string.h b/src/util/string.h index bddbc62ce..50e208966 100644 --- a/src/util/string.h +++ b/src/util/string.h @@ -32,6 +32,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include +#include #include class Translations; @@ -87,6 +88,8 @@ struct FlagDesc { std::wstring utf8_to_wide(std::string_view input); std::string wide_to_utf8(std::wstring_view input); +void wide_add_codepoint(std::wstring &result, char32_t codepoint); + std::string urlencode(std::string_view str); std::string urldecode(std::string_view str); @@ -325,19 +328,30 @@ inline std::string lowercase(std::string_view str) } +inline bool my_isspace(const char c) +{ + return std::isspace(c); +} + +inline bool my_isspace(const wchar_t c) +{ + return std::iswspace(c); +} + /** * @param str * @return A view of \p str with leading and trailing whitespace removed. */ -inline std::string_view trim(std::string_view str) +template +inline std::basic_string_view trim(const std::basic_string_view &str) { size_t front = 0; size_t back = str.size(); - while (front < back && std::isspace(str[front])) + while (front < back && my_isspace(str[front])) ++front; - while (back > front && std::isspace(str[back - 1])) + while (back > front && my_isspace(str[back - 1])) --back; return str.substr(front, back - front); @@ -351,16 +365,24 @@ inline std::string_view trim(std::string_view str) * @param str * @return A copy of \p str with leading and trailing whitespace removed. */ -inline std::string trim(std::string &&str) +template +inline std::basic_string trim(std::basic_string &&str) { - std::string ret(trim(std::string_view(str))); + std::basic_string ret(trim(std::basic_string_view(str))); return ret; } -// The above declaration causes ambiguity with char pointers so we have to fix that: -inline std::string_view trim(const char *str) +template +inline std::basic_string_view trim(const std::basic_string &str) { - return trim(std::string_view(str)); + return trim(std::basic_string_view(str)); +} + +// The above declaration causes ambiguity with char pointers so we have to fix that: +template +inline std::basic_string_view trim(const T *str) +{ + return trim(std::basic_string_view(str)); } @@ -631,11 +653,11 @@ std::vector > split(const std::basic_string &s, T delim) return tokens; } -std::wstring translate_string(const std::wstring &s, Translations *translations); +std::wstring translate_string(std::wstring_view s, Translations *translations); -std::wstring translate_string(const std::wstring &s); +std::wstring translate_string(std::wstring_view s); -inline std::wstring unescape_translate(const std::wstring &s) { +inline std::wstring unescape_translate(std::wstring_view s) { return unescape_enriched(translate_string(s)); } @@ -761,6 +783,16 @@ inline irr::core::stringw utf8_to_stringw(std::string_view input) */ std::string sanitizeDirName(std::string_view str, std::string_view optional_prefix); +/** + * Sanitize an untrusted string (e.g. from the network). This will get strip + * control characters and (optionally) any MT-style escape sequences too. + * Note that they won't be removed cleanly but rather just broken, unlike with + * unescape_enriched. + * Line breaks and UTF-8 is permitted. + */ +[[nodiscard]] +std::string sanitize_untrusted(std::string_view str, bool keep_escapes = true); + /** * Prints a sanitized version of a string without control characters. * '\t' and '\n' are allowed, as are UTF-8 control characters (e.g. RTL). diff --git a/src/util/timetaker.cpp b/src/util/timetaker.cpp index a18d813ba..47d8ab83a 100644 --- a/src/util/timetaker.cpp +++ b/src/util/timetaker.cpp @@ -35,7 +35,7 @@ u64 TimeTaker::stop(bool quiet) if (m_result != nullptr) { (*m_result) += dtime; } else { - if (!quiet) { + if (!quiet && !m_name.empty()) { infostream << m_name << " took " << dtime << TimePrecision_units[m_precision] << std::endl; } diff --git a/src/util/tracy_wrapper.h b/src/util/tracy_wrapper.h new file mode 100644 index 000000000..0c61ba837 --- /dev/null +++ b/src/util/tracy_wrapper.h @@ -0,0 +1,200 @@ +/* +Minetest +Copyright (C) 2024 DS + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +/* + * Wrapper for , so that we can use Tracy's macros without + * having it as mandatory dependency. + * + * For annotations that you don't intend to upstream, you can also include + * directly (which also works in irr/). + */ + +#pragma once + +#include "config.h" +#include "util/basic_macros.h" + +#if BUILD_WITH_TRACY + +#include // IWYU pragma: export + +#else + +// Copied from Tracy.hpp + +#define TracyNoop + +#define ZoneNamed(x,y) +#define ZoneNamedN(x,y,z) +#define ZoneNamedC(x,y,z) +#define ZoneNamedNC(x,y,z,w) + +#define ZoneTransient(x,y) +#define ZoneTransientN(x,y,z) + +#define ZoneScoped +#define ZoneScopedN(x) +#define ZoneScopedC(x) +#define ZoneScopedNC(x,y) + +#define ZoneText(x,y) +#define ZoneTextV(x,y,z) +#define ZoneTextF(x,...) +#define ZoneTextVF(x,y,...) +#define ZoneName(x,y) +#define ZoneNameV(x,y,z) +#define ZoneNameF(x,...) +#define ZoneNameVF(x,y,...) +#define ZoneColor(x) +#define ZoneColorV(x,y) +#define ZoneValue(x) +#define ZoneValueV(x,y) +#define ZoneIsActive false +#define ZoneIsActiveV(x) false + +#define FrameMark +#define FrameMarkNamed(x) +#define FrameMarkStart(x) +#define FrameMarkEnd(x) + +#define FrameImage(x,y,z,w,a) + +#define TracyLockable( type, varname ) type varname +#define TracyLockableN( type, varname, desc ) type varname +#define TracySharedLockable( type, varname ) type varname +#define TracySharedLockableN( type, varname, desc ) type varname +#define LockableBase( type ) type +#define SharedLockableBase( type ) type +#define LockMark(x) (void)x +#define LockableName(x,y,z) + +#define TracyPlot(x,y) +#define TracyPlotConfig(x,y,z,w,a) + +#define TracyMessage(x,y) +#define TracyMessageL(x) +#define TracyMessageC(x,y,z) +#define TracyMessageLC(x,y) +#define TracyAppInfo(x,y) + +#define TracyAlloc(x,y) +#define TracyFree(x) +#define TracySecureAlloc(x,y) +#define TracySecureFree(x) + +#define TracyAllocN(x,y,z) +#define TracyFreeN(x,y) +#define TracySecureAllocN(x,y,z) +#define TracySecureFreeN(x,y) + +#define ZoneNamedS(x,y,z) +#define ZoneNamedNS(x,y,z,w) +#define ZoneNamedCS(x,y,z,w) +#define ZoneNamedNCS(x,y,z,w,a) + +#define ZoneTransientS(x,y,z) +#define ZoneTransientNS(x,y,z,w) + +#define ZoneScopedS(x) +#define ZoneScopedNS(x,y) +#define ZoneScopedCS(x,y) +#define ZoneScopedNCS(x,y,z) + +#define TracyAllocS(x,y,z) +#define TracyFreeS(x,y) +#define TracySecureAllocS(x,y,z) +#define TracySecureFreeS(x,y) + +#define TracyAllocNS(x,y,z,w) +#define TracyFreeNS(x,y,z) +#define TracySecureAllocNS(x,y,z,w) +#define TracySecureFreeNS(x,y,z) + +#define TracyMessageS(x,y,z) +#define TracyMessageLS(x,y) +#define TracyMessageCS(x,y,z,w) +#define TracyMessageLCS(x,y,z) + +#define TracySourceCallbackRegister(x,y) +#define TracyParameterRegister(x,y) +#define TracyParameterSetup(x,y,z,w) +#define TracyIsConnected false +#define TracyIsStarted false +#define TracySetProgramName(x) + +#define TracyFiberEnter(x) +#define TracyFiberEnterHint(x,y) +#define TracyFiberLeave + +#endif + + +// Helper for making sure frames end in all possible control flow path +class FrameMarker +{ + const char *m_name; + bool m_started = false; + +public: + FrameMarker(const char *name) : m_name(name) {} + + ~FrameMarker() { end(); } + + DISABLE_CLASS_COPY(FrameMarker) + + FrameMarker(FrameMarker &&other) noexcept : + m_name(other.m_name), m_started(other.m_started) + { + other.m_started = false; + } + + FrameMarker &operator=(FrameMarker &&other) noexcept + { + if (&other != this) { + end(); + m_name = other.m_name; + m_started = other.m_started; + other.m_started = false; + } + return *this; + } + + FrameMarker &&started() && + { + if (!m_started) { + FrameMarkStart(m_name); + m_started = true; + } + return std::move(*this); + } + + void start() + { + // no move happens, because we drop the reference + (void)std::move(*this).started(); + } + + void end() + { + if (m_started) { + m_started = false; + FrameMarkEnd(m_name); + } + } +}; diff --git a/src/voxel.cpp b/src/voxel.cpp index 0f87cf282..9fb310962 100644 --- a/src/voxel.cpp +++ b/src/voxel.cpp @@ -29,10 +29,8 @@ with this program; if not, write to the Free Software Foundation, Inc., /* Debug stuff */ -u64 addarea_time = 0; u64 emerge_time = 0; u64 emerge_load_time = 0; -u64 clearflag_time = 0; VoxelManipulator::~VoxelManipulator() { @@ -53,7 +51,7 @@ void VoxelManipulator::clear() } void VoxelManipulator::print(std::ostream &o, const NodeDefManager *ndef, - VoxelPrintMode mode) + VoxelPrintMode mode) const { const v3s16 &em = m_area.getExtent(); v3s16 of = m_area.MinEdge; @@ -140,8 +138,6 @@ void VoxelManipulator::addArea(const VoxelArea &area) if(m_area.contains(area)) return; - TimeTaker timer("addArea", &addarea_time); - // Calculate new area VoxelArea new_area; // New area is the requested area if m_area has zero volume @@ -158,15 +154,6 @@ void VoxelManipulator::addArea(const VoxelArea &area) s32 new_size = new_area.getVolume(); - /*dstream<<"adding area "; - area.print(dstream); - dstream<<", old area "; - m_area.print(dstream); - dstream<<", new area "; - new_area.print(dstream); - dstream<<", new_size="< &result) + template + void diff(const VoxelArea &a, C &result) const { - /* - This can result in a maximum of 6 areas - */ - // If a is an empty area, return the current area as a whole - if(a.getExtent() == v3s16(0,0,0)) + if(a.hasEmptyExtent()) { VoxelArea b = *this; - if(b.getVolume() != 0) + if (b.getVolume() != 0) result.push_back(b); return; } assert(contains(a)); // pre-condition + const auto &take = [&result] (v3s16 min, v3s16 max) { + VoxelArea b(min, max); + if (b.getVolume() != 0) + result.push_back(b); + }; + // Take back area, XY inclusive { v3s16 min(MinEdge.X, MinEdge.Y, a.MaxEdge.Z+1); v3s16 max(MaxEdge.X, MaxEdge.Y, MaxEdge.Z); - VoxelArea b(min, max); - if(b.getVolume() != 0) - result.push_back(b); + take(min, max); } // Take front area, XY inclusive { v3s16 min(MinEdge.X, MinEdge.Y, MinEdge.Z); v3s16 max(MaxEdge.X, MaxEdge.Y, a.MinEdge.Z-1); - VoxelArea b(min, max); - if(b.getVolume() != 0) - result.push_back(b); + take(min, max); } // Take top area, X inclusive { v3s16 min(MinEdge.X, a.MaxEdge.Y+1, a.MinEdge.Z); v3s16 max(MaxEdge.X, MaxEdge.Y, a.MaxEdge.Z); - VoxelArea b(min, max); - if(b.getVolume() != 0) - result.push_back(b); + take(min, max); } // Take bottom area, X inclusive { v3s16 min(MinEdge.X, MinEdge.Y, a.MinEdge.Z); v3s16 max(MaxEdge.X, a.MinEdge.Y-1, a.MaxEdge.Z); - VoxelArea b(min, max); - if(b.getVolume() != 0) - result.push_back(b); + take(min, max); } // Take left area, non-inclusive { v3s16 min(MinEdge.X, a.MinEdge.Y, a.MinEdge.Z); v3s16 max(a.MinEdge.X-1, a.MaxEdge.Y, a.MaxEdge.Z); - VoxelArea b(min, max); - if(b.getVolume() != 0) - result.push_back(b); + take(min, max); } // Take right area, non-inclusive { v3s16 min(a.MaxEdge.X+1, a.MinEdge.Y, a.MinEdge.Z); v3s16 max(MaxEdge.X, a.MaxEdge.Y, a.MaxEdge.Z); - VoxelArea b(min, max); - if(b.getVolume() != 0) - result.push_back(b); + take(min, max); } - } /* @@ -344,30 +334,34 @@ public: << "=" << getVolume(); } - // Edges are inclusive + /// Minimum edge of the area (inclusive) + /// @warning read-only! v3s16 MinEdge = v3s16(1,1,1); + /// Maximum edge of the area (inclusive) + /// @warning read-only! v3s16 MaxEdge; + private: void cacheExtent() { m_cache_extent = MaxEdge - MinEdge + v3s16(1,1,1); + // If positions were sorted correctly this must always hold. + // Note that this still permits empty areas (where MinEdge = MaxEdge + 1). + assert(m_cache_extent.X >= 0); + assert(m_cache_extent.Y >= 0); + assert(m_cache_extent.Z >= 0); } v3s16 m_cache_extent = v3s16(0,0,0); }; -// unused -#define VOXELFLAG_UNUSED (1 << 0) -// no data about that node -#define VOXELFLAG_NO_DATA (1 << 1) -// Algorithm-dependent -#define VOXELFLAG_CHECKED1 (1 << 2) -// Algorithm-dependent -#define VOXELFLAG_CHECKED2 (1 << 3) -// Algorithm-dependent -#define VOXELFLAG_CHECKED3 (1 << 4) -// Algorithm-dependent -#define VOXELFLAG_CHECKED4 (1 << 5) +enum : u8 { + VOXELFLAG_NO_DATA = 1 << 0, // no data about that node + VOXELFLAG_CHECKED1 = 1 << 1, // Algorithm-dependent + VOXELFLAG_CHECKED2 = 1 << 2, // Algorithm-dependent + VOXELFLAG_CHECKED3 = 1 << 3, // Algorithm-dependent + VOXELFLAG_CHECKED4 = 1 << 4, // Algorithm-dependent +}; enum VoxelPrintMode { @@ -392,36 +386,36 @@ public: VoxelArea voxel_area(p); addArea(voxel_area); - if (m_flags[m_area.index(p)] & VOXELFLAG_NO_DATA) { - /*dstream<<"EXCEPT: VoxelManipulator::getNode(): " - <<"p=("< Thread Local Storage (TLS) is totally broken -name=llvm-mingw-20231128-ucrt-ubuntu-20.04-x86_64.tar.xz -wget "https://github.com/mstorsjo/llvm-mingw/releases/download/20231128/$name" -O "$name" +date=20240619 +name=llvm-mingw-${date}-ucrt-ubuntu-20.04-x86_64.tar.xz +wget "https://github.com/mstorsjo/llvm-mingw/releases/download/$date/$name" -O "$name" sha256sum -w -c <(grep -F "$name" "$topdir/sha256sums.txt") tar -xaf "$name" -C "$1" --strip-components=1 rm -f "$name" diff --git a/util/buildbot/sha256sums.txt b/util/buildbot/sha256sums.txt index 0587e6ea1..310ab6a6e 100644 --- a/util/buildbot/sha256sums.txt +++ b/util/buildbot/sha256sums.txt @@ -1,7 +1,7 @@ -753dc38c591e078eae6a0a6b25f69826211256f444f3691a170670d8a12988f9 curl-8.5.0-win32.zip -aa86abc3eb054d74d5fe15996f281cf84230a61b4ab7b3a702ab7dbb71e1203f curl-8.5.0-win64.zip -3e9d7bbca953b96dfd65acc28baaa87e8881aab29809ba03b9c9aefe3d071189 freetype-2.13.2-win32.zip -acf901e93aedbcfa92eb3aab1def252676af845b1747ca5c3e7c5866576168cc freetype-2.13.2-win64.zip +627d4111ee655a68e806251974ba9d0337efac19cb07d499689c44c328a23775 curl-8.9.1-win32.zip +ed906726531388441d7f93fc0a1c9d567d476fbc8cfbae19dc5a0f7288949abe curl-8.9.1-win64.zip +7a94b9e69d4872489228ad7cca6a16117a433f809d9b20fa3e44e1616a33c5d7 freetype-2.13.3-win32.zip +f7d882319790f72ebc8eff00526388432bd26bff3a56c4ef5cce0a829bbbef0d freetype-2.13.3-win64.zip 41b10766de2773f0f0851fde16b363024685e0397f4bb2e5cd2a7be196960a01 gettext-0.20.2-win32.zip 1ceed167ff16fea944f76ab6ea2969160c71a67419259b17c9c523e7a01eb883 gettext-0.20.2-win64.zip 53dfd31285f470fcf0dca88217c5cf9c557729af6d103afae5936e72ddc38d3c libjpeg-3.0.1-win32.zip @@ -10,20 +10,20 @@ f54e9a577e2db47ed28f4a01e74181d2c607627c551d30f48263e01b59e84f67 libleveldb-1.2 2f039848a4e6c05a2347fe5a7fa63c430dd08d1bc88235645a863c859e14f5f8 libleveldb-1.23-win64.zip 0df94afb8efa361cceb132ecf9491720afbc45ba844a7b1c94607295829b53ca libogg-1.3.5-win32.zip 5c4acb4c99429a04b5e69650719b2eb17616bf52837d2372a0f859952eebce48 libogg-1.3.5-win64.zip -6baf4e819bfb3573760524b5dc9a04b5e479090d6d2046b86cf39a3107c0071f libpng-1.6.40-win32.zip -c02e029f01fce44baea7f4aecfd2564bd8a03507c0c6af8b03339ae0452c8b7d libpng-1.6.40-win64.zip +fb61536bfce414fdecb30dfbdc8b26e87969ee30b420f5fb8542f7573a1c1d12 libpng-1.6.43-win32.zip +ccd0b8ecbaa07028067a99dd4314ec7799445f80a28ddc86fa3f6bf25700177b libpng-1.6.43-win64.zip 456ece10a2be4247b27fbe88f88ddd54aae604736a6b76ba9a922b602fe40f40 libvorbis-1.3.7-win32.zip 57f4db02da00556895bb63fafa6e46b5f7dac87c25fde27af4315f56a1aa7a86 libvorbis-1.3.7-win64.zip -0f21ff3be90311092fe32e0e30878ef3ae9d9437b8d9ac25ef279e0d84e9bb8e llvm-mingw-20231128-ucrt-ubuntu-20.04-x86_64.tar.xz -da6ad10632cf172992158e9ea0977a87914b5d5de93a972c3430b6a412237556 luajit-20240125-win32.zip -2b1dabe83d478b398cf9226d96de7fa62c973365c4aea70d27ba5782fb49d2d0 luajit-20240125-win64.zip +27d33157cc252c29ad6f777a96a0d94176fea1b534ff09b5071485def143b90e llvm-mingw-20240619-ucrt-ubuntu-20.04-x86_64.tar.xz +5380bbb0bfd4482b5774e4f7c0ff58cc0857477b88a88a178316a464d3459cf1 luajit-20240905-win32.zip +5805c75c61bf948f790e6f845adc94a4946e43ab8a78c5b5b441550f8a665d2c luajit-20240905-win64.zip e2443451fe5c2066eb564c64b8a1762738a88b7fd749c8b5907fed45c785497b openal-soft-1.23.1-win32.zip cb041445a118469caefbad2647470cb8571c8337bce2adc07634011ab5625417 openal-soft-1.23.1-win64.zip -574e0847e622ff09ab23e2b22b77685a2ab6ee43de3e2932f3e8a14a4d7b9338 sdl2-2.30.3-win32.zip -6127afdfc7b6a4ade8caf9a7267748ffea974f729866dd5be96c7a69d2f0fee7 sdl2-2.30.3-win64.zip -326701086a0ed66e09a9f3ec4d971654c13b6bd79cfdd079c947ecdcd6409525 sqlite3-3.44.2-win32.zip -b2d474e3625f8f426b6cc5c0ecac831a1de46f7d1027bf4a9f6267b0b0411d42 sqlite3-3.44.2-win64.zip +af09a54f1f5d75ef6e1bf63662489ca57d44b6b522446638afe35e59b8456a3c sdl2-2.30.7-win32.zip +613abc34a84ed2c3b050314b340ba7e675879e8ed7848e6a28cd9c50262a33b0 sdl2-2.30.7-win64.zip +9685857ae0b418068ad4324e3711121bda97488d19235a0e68a6060162e014d7 sqlite3-3.46.1-win32.zip +7e2990619b1fd1d5ed654d1df77ea809d4332c2e914ea8bba53b2cf5acdf10ff sqlite3-3.46.1-win64.zip 8af10515d57dbfee5d2106cd66cafa2adeb4270d4c6047ccbf7e8b5d2d50681c zlib-1.3.1-win32.zip ad43f5d23052590c65633530743e5d622cc76b33c109072e6fd7b487aff56bca zlib-1.3.1-win64.zip -3564dabbe17ec4ecae1fb9a78fe48d9f7c71e2b1166456f6ee27e52fd9c84357 zstd-1.5.5-win32.zip -e61b1f327ce2d836d1f8ca00c40ac77d3ab5309135851c98229bbdf82b060ae5 zstd-1.5.5-win64.zip +e1bd36f6da039ee8c1694509f379a5023c05d6c90905a2cbb424f0395167570a zstd-1.5.6-win32.zip +f65b75b04b00f6bda859a7c60667f735c664a893bf7796b38393c16cc40a1a82 zstd-1.5.6-win64.zip diff --git a/util/bump_version.sh b/util/bump_version.sh index 45452c295..5e920dc70 100755 --- a/util/bump_version.sh +++ b/util/bump_version.sh @@ -16,20 +16,18 @@ prompt_for() { } # Reads current versions -# out: VERSION_MAJOR VERSION_MINOR VERSION_PATCH VERSION_IS_DEV CURRENT_VERSION ANDROID_VERSION_CODE +# out: VERSION_MAJOR VERSION_MINOR VERSION_PATCH VERSION_IS_DEV CURRENT_VERSION read_versions() { VERSION_MAJOR=$(grep -oE '^set\(VERSION_MAJOR [0-9]+\)$' CMakeLists.txt | tr -dC 0-9) VERSION_MINOR=$(grep -oE '^set\(VERSION_MINOR [0-9]+\)$' CMakeLists.txt | tr -dC 0-9) VERSION_PATCH=$(grep -oE '^set\(VERSION_PATCH [0-9]+\)$' CMakeLists.txt | tr -dC 0-9) VERSION_IS_DEV=$(grep -oE '^set\(DEVELOPMENT_BUILD [A-Z]+\)$' CMakeLists.txt) - ANDROID_VERSION_CODE=$(grep -oE '\("versionCode", [0-9]+\)' android/build.gradle | tr -dC 0-9) # Make sure they all exist [ -n "$VERSION_MAJOR" ] [ -n "$VERSION_MINOR" ] [ -n "$VERSION_PATCH" ] [ -n "$VERSION_IS_DEV" ] - [ -n "$ANDROID_VERSION_CODE" ] if echo "$VERSION_IS_DEV" | grep -q ' TRUE'; then VERSION_IS_DEV=1 @@ -39,26 +37,18 @@ read_versions() { CURRENT_VERSION="$VERSION_MAJOR.$VERSION_MINOR.$VERSION_PATCH" echo "Current Minetest version: $CURRENT_VERSION" - echo "Current Android version code: $ANDROID_VERSION_CODE" } # Retrieves protocol version from header # in: $1 read_proto_ver() { local ref=$1 - git show "$ref":src/network/networkprotocol.h | grep -oE 'LATEST_PROTOCOL_VERSION [0-9]+' | tr -dC 0-9 -} - -## Prompts for new android version code -# in: ANDROID_VERSION_CODE -# out: NEW_ANDROID_VERSION_CODE -bump_android_ver() { - # +1 for ARM and +1 for ARM64 APKs - NEW_ANDROID_VERSION_CODE=$(expr $ANDROID_VERSION_CODE + 2) - NEW_ANDROID_VERSION_CODE=$(prompt_for "Set android version code" '[0-9]+' $NEW_ANDROID_VERSION_CODE) - - echo - echo "New android version code: $NEW_ANDROID_VERSION_CODE" + local output=$(git show "$ref":src/network/networkprotocol.cpp 2>/dev/null) + if [ -z "$output" ]; then + # Fallback to previous file (for tags < 5.10.0) + output=$(git show "$ref":src/network/networkprotocol.h) + fi + grep -oE 'LATEST_PROTOCOL_VERSION\s+=?\s*[0-9]+' <<<"$output" | tr -dC 0-9 } ## Prompts for new version @@ -108,14 +98,6 @@ set_dev_build() { git add -f CMakeLists.txt android/build.gradle } -## Writes new android version code -# in: NEW_ANDROID_VERSION_CODE -write_android_version() { - sed -i -re "s/\"versionCode\", [0-9]+/\"versionCode\", $NEW_ANDROID_VERSION_CODE/" android/build.gradle - - git add -f android/build.gradle -} - ## Writes new version to the right files # in: NEXT_VERSION NEXT_VERSION_{MAJOR,MINOR,PATCH} write_new_version() { @@ -198,8 +180,6 @@ if [ "$DO_PATCH_REL" -eq 0 ]; then exit 1 fi - bump_android_ver - write_android_version set_dev_build 0 perform_release "$CURRENT_VERSION" @@ -212,8 +192,6 @@ if [ "$DO_PATCH_REL" -eq 0 ]; then else # On a patch release the version moves from 5.7.0 -> 5.7.1 (new tag) - bump_android_ver - write_android_version bump_version write_new_version diff --git a/util/helper_mod/error.lua b/util/helper_mod/error.lua new file mode 100644 index 000000000..2a4b3e355 --- /dev/null +++ b/util/helper_mod/error.lua @@ -0,0 +1 @@ +error("intentional") diff --git a/util/helper_mod/init.lua b/util/helper_mod/init.lua index 4da832ed7..b2ed3b29d 100644 --- a/util/helper_mod/init.lua +++ b/util/helper_mod/init.lua @@ -48,4 +48,24 @@ elseif mode == "mapgen" then end core.after(0, next_, 1) +elseif mode == "error" then + + local n = tonumber(core.settings:get("error_type")) + local error_lua = core.get_modpath(core.get_current_modname()) .. "/error.lua" + if n == 1 then + print("=> error during startup <=") + error("intentional") + elseif n == 2 then + print("=> error on first step <=") + core.after(0, error, "intentional") + elseif n == 3 then + print("=> error in async script <=") + core.register_async_dofile(error_lua) + elseif n == 4 then + print("=> error in mapgen script <=") + core.register_mapgen_script(error_lua) + else + assert(false) + end + end diff --git a/util/test_error_cases.sh b/util/test_error_cases.sh new file mode 100755 index 000000000..801e097a3 --- /dev/null +++ b/util/test_error_cases.sh @@ -0,0 +1,46 @@ +#!/bin/bash +dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +gameid=${gameid:-devtest} +minetest=$dir/../bin/minetest +testspath=$dir/../tests +conf_server=$testspath/server.conf +worldpath=$testspath/world + +[ -e "$minetest" ] || { echo "executable $minetest missing"; exit 1; } + +write_config () { + printf '%s\n' >"$conf_server" \ + helper_mode=error mg_name=singlenode "$@" +} + +run () { + timeout 10 "$@" + r=$? + echo "Exit status: $r" + [ $r -eq 124 ] && echo "(timed out)" + if [ $r -ne 1 ]; then + echo "-> Test failed" + exit 1 + fi +} + +rm -rf "$worldpath" +mkdir -p "$worldpath/worldmods" + +ln -s "$dir/helper_mod" "$worldpath/worldmods/" + +args=(--server --config "$conf_server" --world "$worldpath" --gameid $gameid) + +# make sure we can tell apart sanitizer and minetest errors +export ASAN_OPTIONS="exitcode=42" +export MSAN_OPTIONS="exitcode=42" + +# see helper_mod/init.lua for the different types +for n in $(seq 1 4); do + write_config error_type=$n + run "$minetest" "${args[@]}" + echo "---------------" +done + +echo "All done." +exit 0 diff --git a/util/test_multiplayer.sh b/util/test_multiplayer.sh index 624669ac1..b12908423 100755 --- a/util/test_multiplayer.sh +++ b/util/test_multiplayer.sh @@ -33,7 +33,8 @@ printf '%s\n' >"$testspath/client1.conf" \ printf '%s\n' >"$testspath/server.conf" \ max_block_send_distance=1 active_block_range=1 \ - devtest_unittests_autostart=true helper_mode=devtest + devtest_unittests_autostart=true helper_mode=devtest \ + "${serverconf:-}" ln -s "$dir/helper_mod" "$worldpath/worldmods/"