From bb6782ca58a8a23709cfe0b0e9083ef8f50377b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20=C3=85str=C3=B6m?= Date: Sat, 30 Mar 2024 11:05:58 +0100 Subject: [PATCH 01/34] Add repeat_dig_time setting (#14295) --- builtin/settingtypes.txt | 6 +++++- src/client/game.cpp | 22 +++++++++++++++------- src/defaultsettings.cpp | 1 + 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/builtin/settingtypes.txt b/builtin/settingtypes.txt index 57420ec15..517a83012 100644 --- a/builtin/settingtypes.txt +++ b/builtin/settingtypes.txt @@ -114,7 +114,11 @@ 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 +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. +repeat_dig_time (Dig repetition interval) float 0.15 0.15 2.0 # Automatically jump up single-node obstacles. autojump (Automatic jumping) bool false diff --git a/src/client/game.cpp b/src/client/game.cpp index 849e7702a..67529681b 100644 --- a/src/client/game.cpp +++ b/src/client/game.cpp @@ -939,6 +939,7 @@ private: f32 m_cache_mouse_sensitivity; f32 m_cache_joystick_frustum_sensitivity; f32 m_repeat_place_time; + f32 m_repeat_dig_time; f32 m_cache_cam_smoothing; bool m_invert_mouse; @@ -985,6 +986,8 @@ Game::Game() : &settingChangedCallback, this); g_settings->registerChangedCallback("repeat_place_time", &settingChangedCallback, this); + g_settings->registerChangedCallback("repeat_dig_time", + &settingChangedCallback, this); g_settings->registerChangedCallback("noclip", &settingChangedCallback, this); g_settings->registerChangedCallback("free_move", @@ -1051,6 +1054,8 @@ Game::~Game() &settingChangedCallback, this); g_settings->deregisterChangedCallback("repeat_place_time", &settingChangedCallback, this); + g_settings->deregisterChangedCallback("repeat_dig_time", + &settingChangedCallback, this); g_settings->deregisterChangedCallback("noclip", &settingChangedCallback, this); g_settings->deregisterChangedCallback("free_move", @@ -3912,12 +3917,14 @@ void Game::handleDigging(const PointedThing &pointed, const v3s16 &nodepos, runData.nodig_delay_timer = runData.dig_time_complete / (float)crack_animation_length; - // We don't want a corresponding delay to very time consuming nodes - // and nodes without digging time (e.g. torches) get a fixed delay. - if (runData.nodig_delay_timer > 0.3) - runData.nodig_delay_timer = 0.3; - else if (runData.dig_instantly) - runData.nodig_delay_timer = 0.15; + // Don't add a corresponding delay to very time consuming nodes. + runData.nodig_delay_timer = std::min(runData.nodig_delay_timer, 0.3f); + + // Ensure that the delay between breaking nodes + // (dig_time_complete + nodig_delay_timer) is at least the + // value of the repeat_dig_time setting. + runData.nodig_delay_timer = std::max(runData.nodig_delay_timer, + m_repeat_dig_time - runData.dig_time_complete); if (client->modsLoaded() && client->getScript()->on_dignode(nodepos, n)) { @@ -4315,7 +4322,8 @@ void Game::readSettings() m_cache_enable_fog = g_settings->getBool("enable_fog"); m_cache_mouse_sensitivity = g_settings->getFloat("mouse_sensitivity", 0.001f, 10.0f); m_cache_joystick_frustum_sensitivity = std::max(g_settings->getFloat("joystick_frustum_sensitivity"), 0.001f); - m_repeat_place_time = g_settings->getFloat("repeat_place_time", 0.16f, 2.0); + m_repeat_place_time = g_settings->getFloat("repeat_place_time", 0.15f, 2.0f); + m_repeat_dig_time = g_settings->getFloat("repeat_dig_time", 0.15f, 2.0f); m_cache_enable_noclip = g_settings->getBool("noclip"); m_cache_enable_free_move = g_settings->getBool("free_move"); diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp index d4942a4f5..e018e591b 100644 --- a/src/defaultsettings.cpp +++ b/src/defaultsettings.cpp @@ -301,6 +301,7 @@ void set_default_settings() settings->setDefault("invert_hotbar_mouse_wheel", "false"); settings->setDefault("mouse_sensitivity", "0.2"); settings->setDefault("repeat_place_time", "0.25"); + settings->setDefault("repeat_dig_time", "0.15"); settings->setDefault("safe_dig_and_place", "false"); settings->setDefault("random_input", "false"); settings->setDefault("aux1_descends", "false"); From 6c6e48f00607233ae6cb3cf37fa4544d727aacf2 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sat, 30 Mar 2024 11:06:28 +0100 Subject: [PATCH 02/34] Move values the mainmenu caches to dedicated files (#14433) --- builtin/mainmenu/common.lua | 35 ++++++++-- builtin/mainmenu/content/update_detector.lua | 21 ++++-- builtin/mainmenu/dlg_reinstall_mtg.lua | 23 ++++-- builtin/mainmenu/dlg_version_info.lua | 23 +++--- builtin/mainmenu/init.lua | 2 - builtin/mainmenu/misc.lua | 6 -- builtin/mainmenu/serverlistmgr.lua | 70 +++++++++++-------- builtin/mainmenu/tests/serverlistmgr_spec.lua | 3 +- builtin/settingtypes.txt | 15 +--- doc/menu_lua_api.md | 4 -- src/defaultsettings.cpp | 5 +- src/gui/guiEngine.cpp | 2 + src/script/lua_api/l_mainmenu.cpp | 40 ----------- src/script/lua_api/l_mainmenu.h | 4 -- src/script/scripting_mainmenu.cpp | 18 +++-- src/script/scripting_mainmenu.h | 3 + 16 files changed, 142 insertions(+), 132 deletions(-) delete mode 100644 builtin/mainmenu/misc.lua diff --git a/builtin/mainmenu/common.lua b/builtin/mainmenu/common.lua index 471360581..16c5c9d95 100644 --- a/builtin/mainmenu/common.lua +++ b/builtin/mainmenu/common.lua @@ -18,6 +18,26 @@ -- Global menu data menudata = {} +-- located in user cache path, for remembering this like e.g. last update check +cache_settings = Settings(core.get_cache_path() .. DIR_DELIM .. "common.conf") + +--- Checks if the given key contains a timestamp less than a certain age. +--- Pair this with a call to `cache_settings:set(key, tostring(os.time()))` +--- after successfully refreshing the cache. +--- @param key Name of entry in cache_settings +--- @param max_age Age to check against, in seconds +--- @return true if the max age is not reached +function check_cache_age(key, max_age) + local time_now = os.time() + local time_checked = tonumber(cache_settings:get(key)) or 0 + return time_now - time_checked < max_age +end + +function core.on_before_close() + -- called before the menu is closed, either exit or to join a game + cache_settings:write() +end + -- Local cached values local min_supp_proto, max_supp_proto @@ -27,6 +47,16 @@ function common_update_cached_supp_proto() end common_update_cached_supp_proto() +-- Other global functions + +function core.sound_stop(handle, ...) + return handle:stop(...) +end + +function os.tmpname() + error('do not use') -- instead: core.get_temp_path() +end + -- Menu helper functions local function render_client_count(n) @@ -140,11 +170,6 @@ function render_serverlist_row(spec) return table.concat(details, ",") end ---------------------------------------------------------------------------------- -os.tmpname = function() - error('do not use') -- instead use core.get_temp_path() -end --------------------------------------------------------------------------------- function menu_render_worldlist() local retval = {} diff --git a/builtin/mainmenu/content/update_detector.lua b/builtin/mainmenu/content/update_detector.lua index 558a0fabb..f70220224 100644 --- a/builtin/mainmenu/content/update_detector.lua +++ b/builtin/mainmenu/content/update_detector.lua @@ -26,13 +26,23 @@ if not core.get_http_api then end +assert(core.create_dir(core.get_cache_path() .. DIR_DELIM .. "cdb")) +local cache_file_path = core.get_cache_path() .. DIR_DELIM .. "cdb" .. DIR_DELIM .. "updates.json" local has_fetched = false local latest_releases do - local tmp = core.get_once("cdb_latest_releases") - if tmp then - latest_releases = core.deserialize(tmp, true) - has_fetched = latest_releases ~= nil + if check_cache_age("cdb_updates_last_checked", 3 * 3600) then + local f = io.open(cache_file_path, "r") + local data = "" + if f then + data = f:read("*a") + f:close() + end + data = data ~= "" and core.parse_json(data) or nil + if type(data) == "table" then + latest_releases = data + has_fetched = true + end end end @@ -97,7 +107,8 @@ local function fetch() return end latest_releases = lowercase_keys(releases) - core.set_once("cdb_latest_releases", core.serialize(latest_releases)) + core.safe_file_write(cache_file_path, core.write_json(latest_releases)) + cache_settings:set("cdb_updates_last_checked", tostring(os.time())) if update_detector.get_count() > 0 then local maintab = ui.find_by_name("maintab") diff --git a/builtin/mainmenu/dlg_reinstall_mtg.lua b/builtin/mainmenu/dlg_reinstall_mtg.lua index 77652e968..c86c75d2b 100644 --- a/builtin/mainmenu/dlg_reinstall_mtg.lua +++ b/builtin/mainmenu/dlg_reinstall_mtg.lua @@ -15,15 +15,30 @@ --with this program; if not, write to the Free Software Foundation, Inc., --51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +---- IMPORTANT ---- +-- This whole file can be removed after a while. +-- It was only directly useful for upgrades from 5.7.0 to 5.8.0, but +-- maybe some odd fellow directly upgrades from 5.6.1 to 5.9.0 in the future... +-- see in case it's not obvious +---- ---- + +local SETTING_NAME = "no_mtg_notification" + function check_reinstall_mtg() - if core.settings:get_bool("no_mtg_notification") then + -- used to be in minetest.conf + if core.settings:get_bool(SETTING_NAME) then + cache_settings:set_bool(SETTING_NAME, true) + core.settings:remove(SETTING_NAME) + end + + if cache_settings:get_bool(SETTING_NAME) then return end local games = core.get_games() for _, game in ipairs(games) do if game.id == "minetest" then - core.settings:set_bool("no_mtg_notification", true) + cache_settings:set_bool(SETTING_NAME, true) return end end @@ -37,7 +52,7 @@ function check_reinstall_mtg() end end if not mtg_world_found then - core.settings:set_bool("no_mtg_notification", true) + cache_settings:set_bool(SETTING_NAME, true) return end @@ -87,7 +102,7 @@ local function buttonhandler(this, fields) end if fields.dismiss then - core.settings:set_bool("no_mtg_notification", true) + cache_settings:set_bool("no_mtg_notification", true) this:delete() return true end diff --git a/builtin/mainmenu/dlg_version_info.lua b/builtin/mainmenu/dlg_version_info.lua index 98085ee4a..483cd30bd 100644 --- a/builtin/mainmenu/dlg_version_info.lua +++ b/builtin/mainmenu/dlg_version_info.lua @@ -51,12 +51,13 @@ end local function version_info_buttonhandler(this, fields) if fields.version_check_remind then -- Erase last known, user will be reminded again at next check - core.settings:set("update_last_known", "") + cache_settings:set("update_last_known", "") this:delete() return true end if fields.version_check_never then - core.settings:set("update_last_checked", "disabled") + -- clear checked URL + core.settings:set("update_information_url", "") this:delete() return true end @@ -116,7 +117,7 @@ local function on_version_info_received(json) return end - local known_update = tonumber(core.settings:get("update_last_known")) or 0 + local known_update = tonumber(cache_settings:get("update_last_known")) or 0 -- Format: MMNNPPP (Major, Minor, Patch) local new_number = type(json.latest) == "table" and json.latest.version_code @@ -135,7 +136,7 @@ local function on_version_info_received(json) return end - core.settings:set("update_last_known", tostring(new_number)) + cache_settings:set("update_last_known", tostring(new_number)) -- Show version info dialog (once) maintab:hide() @@ -149,20 +150,20 @@ end function check_new_version() local url = core.settings:get("update_information_url") - if core.settings:get("update_last_checked") == "disabled" or - url == "" then + if url == "" then -- Never show any updates return end - local time_now = os.time() - local time_checked = tonumber(core.settings:get("update_last_checked")) or 0 - if time_now - time_checked < 2 * 24 * 3600 then - -- Check interval of 2 entire days + -- every 2 days + if check_cache_age("update_last_checked", 2 * 24 * 3600) then return end + cache_settings:set("update_last_checked", tostring(os.time())) - core.settings:set("update_last_checked", tostring(time_now)) + -- Clean old leftovers (this can be removed after 5.9.0 or so) + core.settings:remove("update_last_checked") + core.settings:remove("update_last_known") core.handle_async(function(params) local http = core.get_http_api() diff --git a/builtin/mainmenu/init.lua b/builtin/mainmenu/init.lua index 388ab458d..41885e298 100644 --- a/builtin/mainmenu/init.lua +++ b/builtin/mainmenu/init.lua @@ -28,8 +28,6 @@ local basepath = core.get_builtin_path() defaulttexturedir = core.get_texturepath_share() .. DIR_DELIM .. "base" .. DIR_DELIM .. "pack" .. DIR_DELIM -dofile(menupath .. DIR_DELIM .. "misc.lua") - dofile(basepath .. "common" .. DIR_DELIM .. "filterlist.lua") dofile(basepath .. "fstk" .. DIR_DELIM .. "buttonbar.lua") dofile(basepath .. "fstk" .. DIR_DELIM .. "dialog.lua") diff --git a/builtin/mainmenu/misc.lua b/builtin/mainmenu/misc.lua deleted file mode 100644 index 0677e96a3..000000000 --- a/builtin/mainmenu/misc.lua +++ /dev/null @@ -1,6 +0,0 @@ - --- old non-method sound function - -function core.sound_stop(handle, ...) - return handle:stop(...) -end diff --git a/builtin/mainmenu/serverlistmgr.lua b/builtin/mainmenu/serverlistmgr.lua index ab686ded0..997768c15 100644 --- a/builtin/mainmenu/serverlistmgr.lua +++ b/builtin/mainmenu/serverlistmgr.lua @@ -17,7 +17,7 @@ serverlistmgr = { -- continent code we detected for ourselves - my_continent = core.get_once("continent"), + my_continent = nil, -- list of locally favorites servers favorites = nil, @@ -26,6 +26,15 @@ serverlistmgr = { servers = nil, } +do + if check_cache_age("geoip_last_checked", 3600) then + local tmp = cache_settings:get("geoip") or "" + if tmp:match("^[A-Z][A-Z]$") then + serverlistmgr.my_continent = tmp + end + end +end + -------------------------------------------------------------------------------- -- Efficient data structure for normalizing arbitrary scores attached to objects -- e.g. {{"a", 3.14}, {"b", 3.14}, {"c", 20}, {"d", 0}} @@ -112,6 +121,22 @@ local public_downloading = false local geoip_downloading = false -------------------------------------------------------------------------------- +local function fetch_geoip() + local http = core.get_http_api() + local url = core.settings:get("serverlist_url") .. "/geoip" + + local response = http.fetch_sync({ url = url }) + if not response.succeeded then + return + end + + local retval = core.parse_json(response.data) + if type(retval) ~= "table" then + return + end + return type(retval.continent) == "string" and retval.continent +end + function serverlistmgr.sync() if not serverlistmgr.servers then serverlistmgr.servers = {{ @@ -129,37 +154,23 @@ function serverlistmgr.sync() return end - -- only fetched once per MT instance if not serverlistmgr.my_continent and not geoip_downloading then geoip_downloading = true - core.handle_async( - function(param) - local http = core.get_http_api() - local url = core.settings:get("serverlist_url") .. "/geoip" - - local response = http.fetch_sync({ url = url }) - if not response.succeeded then - return - end - - local retval = core.parse_json(response.data) - return retval and type(retval.continent) == "string" and retval.continent - end, - nil, - function(result) - geoip_downloading = false - if not result then - return - end - serverlistmgr.my_continent = result - core.set_once("continent", result) - -- reorder list if we already have it - if serverlistmgr.servers then - serverlistmgr.servers = order_server_list(serverlistmgr.servers) - core.event_handler("Refresh") - end + core.handle_async(fetch_geoip, nil, function(result) + geoip_downloading = false + if not result then + return end - ) + serverlistmgr.my_continent = result + cache_settings:set("geoip", result) + cache_settings:set("geoip_last_checked", tostring(os.time())) + + -- re-sort list if applicable + if serverlistmgr.servers then + serverlistmgr.servers = order_server_list(serverlistmgr.servers) + core.event_handler("Refresh") + end + end) end if public_downloading then @@ -167,6 +178,7 @@ function serverlistmgr.sync() end public_downloading = true + -- note: this isn't cached because it's way too dynamic core.handle_async( function(param) local http = core.get_http_api() diff --git a/builtin/mainmenu/tests/serverlistmgr_spec.lua b/builtin/mainmenu/tests/serverlistmgr_spec.lua index 25e208d10..21ce8a226 100644 --- a/builtin/mainmenu/tests/serverlistmgr_spec.lua +++ b/builtin/mainmenu/tests/serverlistmgr_spec.lua @@ -1,5 +1,6 @@ -_G.core = {get_once = function(_) end} +_G.core = {} _G.unpack = table.unpack +_G.check_cache_age = function() return false end _G.serverlistmgr = {} dofile("builtin/common/vector.lua") diff --git a/builtin/settingtypes.txt b/builtin/settingtypes.txt index 517a83012..b039780bc 100644 --- a/builtin/settingtypes.txt +++ b/builtin/settingtypes.txt @@ -776,6 +776,7 @@ serverlist_url (Serverlist URL) string servers.minetest.net enable_split_login_register (Enable split login/register) bool true # URL to JSON file which provides information about the newest Minetest release +# If this is empty the engine will never check for updates. update_information_url (Update information URL) string https://www.minetest.net/release_info.json [*Server] @@ -2291,20 +2292,6 @@ show_advanced (Show advanced settings) bool false # Changing this setting requires a restart. enable_sound (Sound) bool true -# Unix timestamp (integer) of when the client last checked for an update -# Set this value to "disabled" to never check for updates. -update_last_checked (Last update check) string - -# Version number which was last seen during an update check. -# -# Representation: MMMIIIPPP, where M=Major, I=Minor, P=Patch -# Ex: 5.5.0 is 005005000 -update_last_known (Last known version update) int 0 - -# If this is set to true, the user will never (again) be shown the -# "reinstall Minetest Game" notification. -no_mtg_notification (Don't show "reinstall Minetest Game" notification) bool false - # Key for moving the player forward. keymap_forward (Forward key) key KEY_KEY_W diff --git a/doc/menu_lua_api.md b/doc/menu_lua_api.md index 3e8bb3583..979ef868f 100644 --- a/doc/menu_lua_api.md +++ b/doc/menu_lua_api.md @@ -55,10 +55,6 @@ Functions * Android only. Shares file using the share popup * `core.get_version()` (possible in async calls) * returns current core version -* `core.set_once(key, value)`: - * save a string value that persists even if menu is closed -* `core.get_once(key)`: - * get a string value saved by above function, or `nil` diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp index e018e591b..bb50a00aa 100644 --- a/src/defaultsettings.cpp +++ b/src/defaultsettings.cpp @@ -356,11 +356,10 @@ void set_default_settings() settings->setDefault("contentdb_flag_blacklist", "nonfree, desktop_default"); #endif - settings->setDefault("update_information_url", "https://www.minetest.net/release_info.json"); #if ENABLE_UPDATE_CHECKER - settings->setDefault("update_last_checked", ""); + settings->setDefault("update_information_url", "https://www.minetest.net/release_info.json"); #else - settings->setDefault("update_last_checked", "disabled"); + settings->setDefault("update_information_url", ""); #endif // Server diff --git a/src/gui/guiEngine.cpp b/src/gui/guiEngine.cpp index ea74f6afa..4d35f38ab 100644 --- a/src/gui/guiEngine.cpp +++ b/src/gui/guiEngine.cpp @@ -369,6 +369,8 @@ void GUIEngine::run() #endif } + m_script->beforeClose(); + RenderingEngine::autosaveScreensizeAndCo(initial_screen_size, initial_window_maximized); } diff --git a/src/script/lua_api/l_mainmenu.cpp b/src/script/lua_api/l_mainmenu.cpp index c98f974d2..fdb9e3740 100644 --- a/src/script/lua_api/l_mainmenu.cpp +++ b/src/script/lua_api/l_mainmenu.cpp @@ -1080,44 +1080,6 @@ int ModApiMainMenu::l_do_async_callback(lua_State *L) return 1; } -/******************************************************************************/ -// this is intentionally a global and not part of MainMenuScripting or such -namespace { - std::unordered_map once_values; - std::mutex once_mutex; -} - -int ModApiMainMenu::l_set_once(lua_State *L) -{ - std::string key = readParam(L, 1); - if (lua_isnil(L, 2)) - return 0; - std::string value = readParam(L, 2); - - { - MutexAutoLock lock(once_mutex); - once_values[key] = value; - } - - return 0; -} - -int ModApiMainMenu::l_get_once(lua_State *L) -{ - std::string key = readParam(L, 1); - - { - MutexAutoLock lock(once_mutex); - auto it = once_values.find(key); - if (it == once_values.end()) - lua_pushnil(L); - else - lua_pushstring(L, it->second.c_str()); - } - - return 1; -} - /******************************************************************************/ void ModApiMainMenu::Initialize(lua_State *L, int top) { @@ -1170,8 +1132,6 @@ void ModApiMainMenu::Initialize(lua_State *L, int top) API_FCT(open_dir); API_FCT(share_file); API_FCT(do_async_callback); - API_FCT(set_once); - API_FCT(get_once); } /******************************************************************************/ diff --git a/src/script/lua_api/l_mainmenu.h b/src/script/lua_api/l_mainmenu.h index 780e3b5b8..5535d2170 100644 --- a/src/script/lua_api/l_mainmenu.h +++ b/src/script/lua_api/l_mainmenu.h @@ -166,10 +166,6 @@ private: static int l_share_file(lua_State *L); - static int l_set_once(lua_State *L); - - static int l_get_once(lua_State *L); - // async static int l_do_async_callback(lua_State *L); diff --git a/src/script/scripting_mainmenu.cpp b/src/script/scripting_mainmenu.cpp index d88082b7d..e5cb81cab 100644 --- a/src/script/scripting_mainmenu.cpp +++ b/src/script/scripting_mainmenu.cpp @@ -58,7 +58,6 @@ MainMenuScripting::MainMenuScripting(GUIEngine* guiengine): infostream << "SCRIPTAPI: Initialized main menu modules" << std::endl; } -/******************************************************************************/ void MainMenuScripting::initializeModApi(lua_State *L, int top) { registerLuaClasses(L, top); @@ -79,20 +78,31 @@ void MainMenuScripting::initializeModApi(lua_State *L, int top) asyncEngine.initialize(MAINMENU_NUM_ASYNC_THREADS); } -/******************************************************************************/ void MainMenuScripting::registerLuaClasses(lua_State *L, int top) { LuaSettings::Register(L); MainMenuSoundHandle::Register(L); } -/******************************************************************************/ +void MainMenuScripting::beforeClose() +{ + SCRIPTAPI_PRECHECKHEADER + + int error_handler = PUSH_ERROR_HANDLER(L); + + lua_getglobal(L, "core"); + lua_getfield(L, -1, "on_before_close"); + + PCALL_RES(lua_pcall(L, 0, 0, error_handler)); + + lua_pop(L, 2); // Pop core, error handler +} + void MainMenuScripting::step() { asyncEngine.step(getStack()); } -/******************************************************************************/ u32 MainMenuScripting::queueAsync(std::string &&serialized_func, std::string &&serialized_param) { diff --git a/src/script/scripting_mainmenu.h b/src/script/scripting_mainmenu.h index 3c329654a..d2e5716e2 100644 --- a/src/script/scripting_mainmenu.h +++ b/src/script/scripting_mainmenu.h @@ -37,6 +37,9 @@ public: // Global step handler to pass back async events void step(); + // Calls core.on_before_close() + void beforeClose(); + // Pass async events from engine to async threads u32 queueAsync(std::string &&serialized_func, std::string &&serialized_param); From 6ac053bbaadc30f1313bbbf1db25667e273d4ce8 Mon Sep 17 00:00:00 2001 From: cx384 Date: Sun, 17 Mar 2024 14:09:25 +0100 Subject: [PATCH 03/34] Extract image generation/cache from texturesource.cpp --- src/client/CMakeLists.txt | 1 + src/client/imagesource.cpp | 1975 ++++++++++++++++++++++++++++++++++ src/client/imagesource.h | 84 ++ src/client/texturesource.cpp | 1961 +-------------------------------- 4 files changed, 2069 insertions(+), 1952 deletions(-) create mode 100644 src/client/imagesource.cpp create mode 100644 src/client/imagesource.h diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index f2aa511b7..d451d0911 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -69,6 +69,7 @@ set(client_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/tile.cpp ${CMAKE_CURRENT_SOURCE_DIR}/texturepaths.cpp ${CMAKE_CURRENT_SOURCE_DIR}/texturesource.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/imagesource.cpp ${CMAKE_CURRENT_SOURCE_DIR}/wieldmesh.cpp ${CMAKE_CURRENT_SOURCE_DIR}/shadows/dynamicshadows.cpp ${CMAKE_CURRENT_SOURCE_DIR}/shadows/dynamicshadowsrender.cpp diff --git a/src/client/imagesource.cpp b/src/client/imagesource.cpp new file mode 100644 index 000000000..92da3ee27 --- /dev/null +++ b/src/client/imagesource.cpp @@ -0,0 +1,1975 @@ +/* +Minetest +Copyright (C) 2010-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 "imagesource.h" + +#include +#include "settings.h" +#include "mesh.h" +#include "util/strfnd.h" +#include "renderingengine.h" +#include "util/base64.h" +#include "irrlicht_changes/printing.h" +#include "imagefilters.h" +#include "texturepaths.h" +#include "util/numeric.h" + + +//////////////////////////////// +// SourceImageCache Functions // +//////////////////////////////// + +SourceImageCache::~SourceImageCache() { + for (auto &m_image : m_images) { + m_image.second->drop(); + } + m_images.clear(); +} + +void SourceImageCache::insert(const std::string &name, video::IImage *img, bool prefer_local) +{ + assert(img); // Pre-condition + // Remove old image + std::map::iterator n; + n = m_images.find(name); + if (n != m_images.end()){ + if (n->second) + n->second->drop(); + } + + video::IImage* toadd = img; + bool need_to_grab = true; + + // Try to use local texture instead if asked to + if (prefer_local) { + bool is_base_pack; + std::string path = getTexturePath(name, &is_base_pack); + // Ignore base pack + if (!path.empty() && !is_base_pack) { + video::IImage *img2 = RenderingEngine::get_video_driver()-> + createImageFromFile(path.c_str()); + if (img2){ + toadd = img2; + need_to_grab = false; + } + } + } + + if (need_to_grab) + toadd->grab(); + m_images[name] = toadd; +} +video::IImage* SourceImageCache::get(const std::string &name) +{ + std::map::iterator n; + n = m_images.find(name); + if (n != m_images.end()) + return n->second; + return NULL; +} + +// Primarily fetches from cache, secondarily tries to read from filesystem +video::IImage* SourceImageCache::getOrLoad(const std::string &name) +{ + std::map::iterator n; + n = m_images.find(name); + if (n != m_images.end()){ + n->second->grab(); // Grab for caller + return n->second; + } + video::IVideoDriver *driver = RenderingEngine::get_video_driver(); + std::string path = getTexturePath(name); + if (path.empty()) { + infostream<<"SourceImageCache::getOrLoad(): No path found for \"" + <createImageFromFile(path.c_str()); + + if (img){ + m_images[name] = img; + img->grab(); // Grab for caller + } + return img; +} + + +//////////////////////////// +// Image Helper Functions // +//////////////////////////// + + +/** Draw an image on top of another one with gamma-incorrect alpha compositing + * + * This exists because IImage::copyToWithAlpha() doesn't seem to always work. + * + * \tparam overlay If enabled, only modify pixels in dst which are fully opaque. + * Defaults to false. + * \param src Top image. This image must have the ECF_A8R8G8B8 color format. + * \param dst Bottom image. + * The top image is drawn onto this base image in-place. + * \param dst_pos An offset vector to move src before drawing it onto dst + * \param size Size limit of the copied area +*/ +template +static void blit_with_alpha(video::IImage *src, video::IImage *dst, + v2s32 dst_pos, v2u32 size); + +// Apply a color to an image. Uses an int (0-255) to calculate the ratio. +// If the ratio is 255 or -1 and keep_alpha is true, then it multiples the +// color alpha with the destination alpha. +// Otherwise, any pixels that are not fully transparent get the color alpha. +static void apply_colorize(video::IImage *dst, v2u32 dst_pos, v2u32 size, + const video::SColor color, int ratio, bool keep_alpha); + +// paint a texture using the given color +static void apply_multiplication(video::IImage *dst, v2u32 dst_pos, v2u32 size, + const video::SColor color); + +// Perform a Screen blend with the given color. The opposite effect of a +// Multiply blend. +static void apply_screen(video::IImage *dst, v2u32 dst_pos, v2u32 size, + const video::SColor color); + +// Adjust the hue, saturation, and lightness of destination. Like +// "Hue-Saturation" in GIMP. +// If colorize is true then the image will be converted to a grayscale +// image as though seen through a colored glass, like "Colorize" in GIMP. +static void apply_hue_saturation(video::IImage *dst, v2u32 dst_pos, v2u32 size, + s32 hue, s32 saturation, s32 lightness, bool colorize); + +// Apply an overlay blend to an images. +// Overlay blend combines Multiply and Screen blend modes.The parts of the top +// layer where the base layer is light become lighter, the parts where the base +// layer is dark become darker.Areas where the base layer are mid grey are +// unaffected.An overlay with the same picture looks like an S - curve. +static void apply_overlay(video::IImage *overlay, video::IImage *dst, + v2s32 overlay_pos, v2s32 dst_pos, v2u32 size, bool hardlight); + +// Adjust the brightness and contrast of the base image. Conceptually like +// "Brightness-Contrast" in GIMP but allowing brightness to be wound all the +// way up to white or down to black. +static void apply_brightness_contrast(video::IImage *dst, v2u32 dst_pos, v2u32 size, + s32 brightness, s32 contrast); + +// Apply a mask to an image +static void apply_mask(video::IImage *mask, video::IImage *dst, + v2s32 mask_pos, v2s32 dst_pos, v2u32 size); + +// Draw or overlay a crack +static void draw_crack(video::IImage *crack, video::IImage *dst, + bool use_overlay, s32 frame_count, s32 progression, + video::IVideoDriver *driver, u8 tiles = 1); + +// Brighten image +void brighten(video::IImage *image); +// Parse a transform name +u32 parseImageTransform(std::string_view s); +// Apply transform to image dimension +core::dimension2d imageTransformDimension(u32 transform, core::dimension2d dim); +// Apply transform to image data +void imageTransform(u32 transform, video::IImage *src, video::IImage *dst); + +inline static void applyShadeFactor(video::SColor &color, u32 factor) +{ + u32 f = core::clamp(factor, 0, 256); + color.setRed(color.getRed() * f / 256); + color.setGreen(color.getGreen() * f / 256); + color.setBlue(color.getBlue() * f / 256); +} + +static video::IImage *createInventoryCubeImage( + video::IImage *top, video::IImage *left, video::IImage *right) +{ + core::dimension2du size_top = top->getDimension(); + core::dimension2du size_left = left->getDimension(); + core::dimension2du size_right = right->getDimension(); + + u32 size = npot2(std::max({ + size_top.Width, size_top.Height, + size_left.Width, size_left.Height, + size_right.Width, size_right.Height, + })); + + // It must be divisible by 4, to let everything work correctly. + // But it is a power of 2, so being at least 4 is the same. + // And the resulting texture should't be too large as well. + size = core::clamp(size, 4, 64); + + // With such parameters, the cube fits exactly, touching each image line + // from `0` to `cube_size - 1`. (Note that division is exact here). + u32 cube_size = 9 * size; + u32 offset = size / 2; + + video::IVideoDriver *driver = RenderingEngine::get_video_driver(); + + auto lock_image = [size, driver] (video::IImage *&image) -> const u32 * { + image->grab(); + core::dimension2du dim = image->getDimension(); + video::ECOLOR_FORMAT format = image->getColorFormat(); + if (dim.Width != size || dim.Height != size || format != video::ECF_A8R8G8B8) { + video::IImage *scaled = driver->createImage(video::ECF_A8R8G8B8, {size, size}); + image->copyToScaling(scaled); + image->drop(); + image = scaled; + } + sanity_check(image->getPitch() == 4 * size); + return reinterpret_cast(image->getData()); + }; + auto free_image = [] (video::IImage *image) -> void { + image->drop(); + }; + + video::IImage *result = driver->createImage(video::ECF_A8R8G8B8, {cube_size, cube_size}); + sanity_check(result->getPitch() == 4 * cube_size); + result->fill(video::SColor(0x00000000u)); + u32 *target = reinterpret_cast(result->getData()); + + // Draws single cube face + // `shade_factor` is face brightness, in range [0.0, 1.0] + // (xu, xv, x1; yu, yv, y1) form coordinate transformation matrix + // `offsets` list pixels to be drawn for single source pixel + auto draw_image = [=] (video::IImage *image, float shade_factor, + s16 xu, s16 xv, s16 x1, + s16 yu, s16 yv, s16 y1, + std::initializer_list offsets) -> void { + u32 brightness = core::clamp(256 * shade_factor, 0, 256); + const u32 *source = lock_image(image); + for (u16 v = 0; v < size; v++) { + for (u16 u = 0; u < size; u++) { + video::SColor pixel(*source); + applyShadeFactor(pixel, brightness); + s16 x = xu * u + xv * v + x1; + s16 y = yu * u + yv * v + y1; + for (const auto &off : offsets) + target[(y + off.Y) * cube_size + (x + off.X) + offset] = pixel.color; + source++; + } + } + free_image(image); + }; + + draw_image(top, 1.000000f, + 4, -4, 4 * (size - 1), + 2, 2, 0, + { + {2, 0}, {3, 0}, {4, 0}, {5, 0}, + {0, 1}, {1, 1}, {2, 1}, {3, 1}, {4, 1}, {5, 1}, {6, 1}, {7, 1}, + {2, 2}, {3, 2}, {4, 2}, {5, 2}, + }); + + draw_image(left, 0.836660f, + 4, 0, 0, + 2, 5, 2 * size, + { + {0, 0}, {1, 0}, + {0, 1}, {1, 1}, {2, 1}, {3, 1}, + {0, 2}, {1, 2}, {2, 2}, {3, 2}, + {0, 3}, {1, 3}, {2, 3}, {3, 3}, + {0, 4}, {1, 4}, {2, 4}, {3, 4}, + {2, 5}, {3, 5}, + }); + + draw_image(right, 0.670820f, + 4, 0, 4 * size, + -2, 5, 4 * size - 2, + { + {2, 0}, {3, 0}, + {0, 1}, {1, 1}, {2, 1}, {3, 1}, + {0, 2}, {1, 2}, {2, 2}, {3, 2}, + {0, 3}, {1, 3}, {2, 3}, {3, 3}, + {0, 4}, {1, 4}, {2, 4}, {3, 4}, + {0, 5}, {1, 5}, + }); + + return result; +} + +static std::string unescape_string(const std::string &str, const char esc = '\\') +{ + std::string out; + size_t pos = 0, cpos; + out.reserve(str.size()); + while (1) { + cpos = str.find_first_of(esc, pos); + if (cpos == std::string::npos) { + out += str.substr(pos); + break; + } + out += str.substr(pos, cpos - pos) + str[cpos + 1]; + pos = cpos + 2; + } + return out; +} + +/* + Replaces the smaller of the two images with one upscaled to match the + dimensions of the other. + Ensure no other references to these images are being held, as one may + get dropped and switched with a new image. +*/ +void upscaleImagesToMatchLargest(video::IImage *& img1, + video::IImage *& img2) +{ + core::dimension2d dim1 = img1->getDimension(); + core::dimension2d dim2 = img2->getDimension(); + + if (dim1 == dim2) { + // image dimensions match, no scaling required + + } + else if (dim1.Width * dim1.Height < dim2.Width * dim2.Height) { + // Upscale img1 + video::IImage *scaled_image = RenderingEngine::get_video_driver()-> + createImage(video::ECF_A8R8G8B8, dim2); + img1->copyToScaling(scaled_image); + img1->drop(); + img1 = scaled_image; + + } else { + // Upscale img2 + video::IImage *scaled_image = RenderingEngine::get_video_driver()-> + createImage(video::ECF_A8R8G8B8, dim1); + img2->copyToScaling(scaled_image); + img2->drop(); + img2 = scaled_image; + } +} + +void blitBaseImage(video::IImage* &src, video::IImage* &dst) +{ + //infostream<<"Blitting "< dim_dst = dst->getDimension(); + // Position to copy the blitted to in the base image + core::position2d pos_to(0,0); + + blit_with_alpha(src, dst, pos_to, dim_dst); +} + + +namespace { + +/** Calculate the color of a single pixel drawn on top of another pixel without + * gamma correction + * + * The color mixing is a little more complicated than just + * video::SColor::getInterpolated because getInterpolated does not handle alpha + * correctly. + * For example, a pixel with alpha=64 drawn atop a pixel with alpha=128 should + * yield a pixel with alpha=160, while getInterpolated would yield alpha=96. + * + * \tparam overlay If enabled, only modify dst_col if it is fully opaque + * \param src_col Color of the top pixel + * \param dst_col Color of the bottom pixel. This color is modified in-place to + * store the result. +*/ +template +void blit_pixel(video::SColor src_col, video::SColor &dst_col) +{ + u8 dst_a = (u8)dst_col.getAlpha(); + if constexpr (overlay) { + if (dst_a != 255) + // The bottom pixel has transparency -> do nothing + return; + } + u8 src_a = (u8)src_col.getAlpha(); + if (src_a == 0) { + // A fully transparent pixel is on top -> do nothing + return; + } + if (src_a == 255 || dst_a == 0) { + // The top pixel is fully opaque or the bottom pixel is + // fully transparent -> replace the color + dst_col = src_col; + return; + } + struct Color { u8 r, g, b; }; + Color src{(u8)src_col.getRed(), (u8)src_col.getGreen(), + (u8)src_col.getBlue()}; + Color dst{(u8)dst_col.getRed(), (u8)dst_col.getGreen(), + (u8)dst_col.getBlue()}; + if (dst_a == 255) { + // A semi-transparent pixel is on top and an opaque one in + // the bottom -> lerp r, g, and b + dst.r = (dst.r * (255 - src_a) + src.r * src_a) / 255; + dst.g = (dst.g * (255 - src_a) + src.g * src_a) / 255; + dst.b = (dst.b * (255 - src_a) + src.b * src_a) / 255; + dst_col.set(255, dst.r, dst.g, dst.b); + return; + } + // A semi-transparent pixel is on top of a + // semi-transparent pixel -> general alpha compositing + u16 a_new_255 = src_a * 255 + (255 - src_a) * dst_a; + dst.r = (dst.r * (255 - src_a) * dst_a + src.r * src_a * 255) / a_new_255; + dst.g = (dst.g * (255 - src_a) * dst_a + src.g * src_a * 255) / a_new_255; + dst.b = (dst.b * (255 - src_a) * dst_a + src.b * src_a * 255) / a_new_255; + dst_a = (a_new_255 + 127) / 255; + dst_col.set(dst_a, dst.r, dst.g, dst.b); +} + +} // namespace +template +void blit_with_alpha(video::IImage *src, video::IImage *dst, v2s32 dst_pos, + v2u32 size) +{ + if (dst->getColorFormat() != video::ECF_A8R8G8B8) + throw BaseException("blit_with_alpha() supports only ECF_A8R8G8B8 " + "destination images."); + + core::dimension2d src_dim = src->getDimension(); + core::dimension2d dst_dim = dst->getDimension(); + bool drop_src = false; + if (src->getColorFormat() != video::ECF_A8R8G8B8) { + video::IVideoDriver *driver = RenderingEngine::get_video_driver(); + video::IImage *src_converted = driver->createImage(video::ECF_A8R8G8B8, + src_dim); + if (!src_converted) + throw BaseException("blit_with_alpha() failed to convert the " + "source image to ECF_A8R8G8B8."); + src->copyTo(src_converted); + src = src_converted; + drop_src = true; + } + video::SColor *pixels_src = + reinterpret_cast(src->getData()); + video::SColor *pixels_dst = + reinterpret_cast(dst->getData()); + // Limit y and x to the overlapping ranges + // s.t. the positions are all in bounds after offsetting. + u32 x_start = (u32)std::max(0, -dst_pos.X); + u32 y_start = (u32)std::max(0, -dst_pos.Y); + u32 x_end = (u32)std::min({size.X, src_dim.Width, + dst_dim.Width - (s64)dst_pos.X}); + u32 y_end = (u32)std::min({size.Y, src_dim.Height, + dst_dim.Height - (s64)dst_pos.Y}); + for (u32 y0 = y_start; y0 < y_end; ++y0) { + size_t i_src = y0 * src_dim.Width + x_start; + size_t i_dst = (dst_pos.Y + y0) * dst_dim.Width + dst_pos.X + x_start; + for (u32 x0 = x_start; x0 < x_end; ++x0) { + blit_pixel(pixels_src[i_src++], pixels_dst[i_dst++]); + } + } + if (drop_src) + src->drop(); +} + +/* + Apply color to destination, using a weighted interpolation blend +*/ +static void apply_colorize(video::IImage *dst, v2u32 dst_pos, v2u32 size, + const video::SColor color, int ratio, bool keep_alpha) +{ + u32 alpha = color.getAlpha(); + video::SColor dst_c; + if ((ratio == -1 && alpha == 255) || ratio == 255) { // full replacement of color + if (keep_alpha) { // replace the color with alpha = dest alpha * color alpha + dst_c = color; + for (u32 y = dst_pos.Y; y < dst_pos.Y + size.Y; y++) + for (u32 x = dst_pos.X; x < dst_pos.X + size.X; x++) { + u32 dst_alpha = dst->getPixel(x, y).getAlpha(); + if (dst_alpha > 0) { + dst_c.setAlpha(dst_alpha * alpha / 255); + dst->setPixel(x, y, dst_c); + } + } + } else { // replace the color including the alpha + for (u32 y = dst_pos.Y; y < dst_pos.Y + size.Y; y++) + for (u32 x = dst_pos.X; x < dst_pos.X + size.X; x++) + if (dst->getPixel(x, y).getAlpha() > 0) + dst->setPixel(x, y, color); + } + } else { // interpolate between the color and destination + float interp = (ratio == -1 ? color.getAlpha() / 255.0f : ratio / 255.0f); + for (u32 y = dst_pos.Y; y < dst_pos.Y + size.Y; y++) + for (u32 x = dst_pos.X; x < dst_pos.X + size.X; x++) { + dst_c = dst->getPixel(x, y); + if (dst_c.getAlpha() > 0) { + dst_c = color.getInterpolated(dst_c, interp); + dst->setPixel(x, y, dst_c); + } + } + } +} + +/* + Apply color to destination, using a Multiply blend mode +*/ +static void apply_multiplication(video::IImage *dst, v2u32 dst_pos, v2u32 size, + const video::SColor color) +{ + video::SColor dst_c; + + for (u32 y = dst_pos.Y; y < dst_pos.Y + size.Y; y++) + for (u32 x = dst_pos.X; x < dst_pos.X + size.X; x++) { + dst_c = dst->getPixel(x, y); + dst_c.set( + dst_c.getAlpha(), + (dst_c.getRed() * color.getRed()) / 255, + (dst_c.getGreen() * color.getGreen()) / 255, + (dst_c.getBlue() * color.getBlue()) / 255 + ); + dst->setPixel(x, y, dst_c); + } +} + +/* + Apply color to destination, using a Screen blend mode +*/ +static void apply_screen(video::IImage *dst, v2u32 dst_pos, v2u32 size, + const video::SColor color) +{ + video::SColor dst_c; + + for (u32 y = dst_pos.Y; y < dst_pos.Y + size.Y; y++) + for (u32 x = dst_pos.X; x < dst_pos.X + size.X; x++) { + dst_c = dst->getPixel(x, y); + dst_c.set( + dst_c.getAlpha(), + 255 - ((255 - dst_c.getRed()) * (255 - color.getRed())) / 255, + 255 - ((255 - dst_c.getGreen()) * (255 - color.getGreen())) / 255, + 255 - ((255 - dst_c.getBlue()) * (255 - color.getBlue())) / 255 + ); + dst->setPixel(x, y, dst_c); + } +} + +/* + Adjust the hue, saturation, and lightness of destination. Like + "Hue-Saturation" in GIMP, but with 0 as the mid-point. + Hue should be from -180 to +180, or from 0 to 360. + Saturation and Lightness are percentages. + Lightness is from -100 to +100. + Saturation goes down to -100 (fully desaturated) but can go above 100, + allowing for even muted colors to become saturated. + + If colorize is true then saturation is from 0 to 100, and destination will + be converted to a grayscale image as seen through a colored glass, like + "Colorize" in GIMP. +*/ +static void apply_hue_saturation(video::IImage *dst, v2u32 dst_pos, v2u32 size, + s32 hue, s32 saturation, s32 lightness, bool colorize) +{ + video::SColorf colorf; + video::SColorHSL hsl; + f32 norm_s = core::clamp(saturation, -100, 1000) / 100.0f; + f32 norm_l = core::clamp(lightness, -100, 100) / 100.0f; + + if (colorize) { + hsl.Saturation = core::clamp((f32)saturation, 0.0f, 100.0f); + } + + for (u32 y = dst_pos.Y; y < dst_pos.Y + size.Y; y++) + for (u32 x = dst_pos.X; x < dst_pos.X + size.X; x++) { + + if (colorize) { + f32 lum = dst->getPixel(x, y).getLuminance() / 255.0f; + + if (norm_l < 0) { + lum *= norm_l + 1.0f; + } else { + lum = lum * (1.0f - norm_l) + norm_l; + } + hsl.Hue = 0; + hsl.Luminance = lum * 100; + + } else { + // convert the RGB to HSL + colorf = video::SColorf(dst->getPixel(x, y)); + hsl.fromRGB(colorf); + + if (norm_l < 0) { + hsl.Luminance *= norm_l + 1.0f; + } else{ + hsl.Luminance = hsl.Luminance + norm_l * (100.0f - hsl.Luminance); + } + + // Adjusting saturation in the same manner as lightness resulted in + // muted colors being affected too much and bright colors not + // affected enough, so I'm borrowing a leaf out of gimp's book and + // using a different scaling approach for saturation. + // https://github.com/GNOME/gimp/blob/6cc1e035f1822bf5198e7e99a53f7fa6e281396a/app/operations/gimpoperationhuesaturation.c#L139-L145= + // This difference is why values over 100% are not necessary for + // lightness but are very useful with saturation. An alternative UI + // approach would be to have an upper saturation limit of 100, but + // multiply positive values by ~3 to make it a more useful positive + // range scale. + hsl.Saturation *= norm_s + 1.0f; + hsl.Saturation = core::clamp(hsl.Saturation, 0.0f, 100.0f); + } + + // Apply the specified HSL adjustments + hsl.Hue = fmod(hsl.Hue + hue, 360); + if (hsl.Hue < 0) + hsl.Hue += 360; + + // Convert back to RGB + hsl.toRGB(colorf); + dst->setPixel(x, y, colorf.toSColor()); + } +} + + +/* + Apply an Overlay blend to destination + If hardlight is true then swap the dst & blend images (a hardlight blend) +*/ +static void apply_overlay(video::IImage *blend, video::IImage *dst, + v2s32 blend_pos, v2s32 dst_pos, v2u32 size, bool hardlight) +{ + video::IImage *blend_layer = hardlight ? dst : blend; + video::IImage *base_layer = hardlight ? blend : dst; + v2s32 blend_layer_pos = hardlight ? dst_pos : blend_pos; + v2s32 base_layer_pos = hardlight ? blend_pos : dst_pos; + + for (u32 y = 0; y < size.Y; y++) + for (u32 x = 0; x < size.X; x++) { + s32 base_x = x + base_layer_pos.X; + s32 base_y = y + base_layer_pos.Y; + + video::SColor blend_c = + blend_layer->getPixel(x + blend_layer_pos.X, y + blend_layer_pos.Y); + video::SColor base_c = base_layer->getPixel(base_x, base_y); + double blend_r = blend_c.getRed() / 255.0; + double blend_g = blend_c.getGreen() / 255.0; + double blend_b = blend_c.getBlue() / 255.0; + double base_r = base_c.getRed() / 255.0; + double base_g = base_c.getGreen() / 255.0; + double base_b = base_c.getBlue() / 255.0; + + base_c.set( + base_c.getAlpha(), + // Do a Multiply blend if less that 0.5, otherwise do a Screen blend + (u32)((base_r < 0.5 ? 2 * base_r * blend_r : 1 - 2 * (1 - base_r) * (1 - blend_r)) * 255), + (u32)((base_g < 0.5 ? 2 * base_g * blend_g : 1 - 2 * (1 - base_g) * (1 - blend_g)) * 255), + (u32)((base_b < 0.5 ? 2 * base_b * blend_b : 1 - 2 * (1 - base_b) * (1 - blend_b)) * 255) + ); + dst->setPixel(base_x, base_y, base_c); + } +} + +/* + Adjust the brightness and contrast of the base image. + + Conceptually like GIMP's "Brightness-Contrast" feature but allows brightness to be + wound all the way up to white or down to black. +*/ +static void apply_brightness_contrast(video::IImage *dst, v2u32 dst_pos, v2u32 size, + s32 brightness, s32 contrast) +{ + video::SColor dst_c; + // Only allow normalized contrast to get as high as 127/128 to avoid infinite slope. + // (we could technically allow -128/128 here as that would just result in 0 slope) + double norm_c = core::clamp(contrast, -127, 127) / 128.0; + double norm_b = core::clamp(brightness, -127, 127) / 127.0; + + // Scale brightness so its range is -127.5 to 127.5, otherwise brightness + // adjustments will outputs values from 0.5 to 254.5 instead of 0 to 255. + double scaled_b = brightness * 127.5 / 127; + + // Calculate a contrast slope such that that no colors will get clamped due + // to the brightness setting. + // This allows the texture modifier to used as a brightness modifier without + // the user having to calculate a contrast to avoid clipping at that brightness. + double slope = 1 - fabs(norm_b); + + // Apply the user's contrast adjustment to the calculated slope, such that + // -127 will make it near-vertical and +127 will make it horizontal + double angle = atan(slope); + angle += norm_c <= 0 + ? norm_c * angle // allow contrast slope to be lowered to 0 + : norm_c * (M_PI_2 - angle); // allow contrast slope to be raised almost vert. + slope = tan(angle); + + double c = slope <= 1 + ? -slope * 127.5 + 127.5 + scaled_b // shift up/down when slope is horiz. + : -slope * (127.5 - scaled_b) + 127.5; // shift left/right when slope is vert. + + // add 0.5 to c so that when the final result is cast to int, it is effectively + // rounded rather than trunc'd. + c += 0.5; + + for (u32 y = dst_pos.Y; y < dst_pos.Y + size.Y; y++) + for (u32 x = dst_pos.X; x < dst_pos.X + size.X; x++) { + dst_c = dst->getPixel(x, y); + + dst_c.set( + dst_c.getAlpha(), + core::clamp((int)(slope * dst_c.getRed() + c), 0, 255), + core::clamp((int)(slope * dst_c.getGreen() + c), 0, 255), + core::clamp((int)(slope * dst_c.getBlue() + c), 0, 255) + ); + dst->setPixel(x, y, dst_c); + } +} + +/* + Apply mask to destination +*/ +static void apply_mask(video::IImage *mask, video::IImage *dst, + v2s32 mask_pos, v2s32 dst_pos, v2u32 size) +{ + for (u32 y0 = 0; y0 < size.Y; y0++) { + for (u32 x0 = 0; x0 < size.X; x0++) { + s32 mask_x = x0 + mask_pos.X; + s32 mask_y = y0 + mask_pos.Y; + s32 dst_x = x0 + dst_pos.X; + s32 dst_y = y0 + dst_pos.Y; + video::SColor mask_c = mask->getPixel(mask_x, mask_y); + video::SColor dst_c = dst->getPixel(dst_x, dst_y); + dst_c.color &= mask_c.color; + dst->setPixel(dst_x, dst_y, dst_c); + } + } +} + +video::IImage *create_crack_image(video::IImage *crack, s32 frame_index, + core::dimension2d size, u8 tiles, video::IVideoDriver *driver) +{ + core::dimension2d strip_size = crack->getDimension(); + + if (tiles == 0 || strip_size.getArea() == 0) + return nullptr; + + core::dimension2d frame_size(strip_size.Width, strip_size.Width); + core::dimension2d tile_size(size / tiles); + s32 frame_count = strip_size.Height / strip_size.Width; + if (frame_index >= frame_count) + frame_index = frame_count - 1; + core::rect frame(v2s32(0, frame_index * frame_size.Height), frame_size); + video::IImage *result = nullptr; + + // extract crack frame + video::IImage *crack_tile = driver->createImage(video::ECF_A8R8G8B8, tile_size); + if (!crack_tile) + return nullptr; + if (tile_size == frame_size) { + crack->copyTo(crack_tile, v2s32(0, 0), frame); + } else { + video::IImage *crack_frame = driver->createImage(video::ECF_A8R8G8B8, frame_size); + if (!crack_frame) + goto exit__has_tile; + crack->copyTo(crack_frame, v2s32(0, 0), frame); + crack_frame->copyToScaling(crack_tile); + crack_frame->drop(); + } + if (tiles == 1) + return crack_tile; + + // tile it + result = driver->createImage(video::ECF_A8R8G8B8, size); + if (!result) + goto exit__has_tile; + result->fill({}); + for (u8 i = 0; i < tiles; i++) + for (u8 j = 0; j < tiles; j++) + crack_tile->copyTo(result, v2s32(i * tile_size.Width, j * tile_size.Height)); + +exit__has_tile: + crack_tile->drop(); + return result; +} + +static void draw_crack(video::IImage *crack, video::IImage *dst, + bool use_overlay, s32 frame_count, s32 progression, + video::IVideoDriver *driver, u8 tiles) +{ + // Dimension of destination image + core::dimension2d dim_dst = dst->getDimension(); + // Limit frame_count + if (frame_count > (s32) dim_dst.Height) + frame_count = dim_dst.Height; + if (frame_count < 1) + frame_count = 1; + // Dimension of the scaled crack stage, + // which is the same as the dimension of a single destination frame + core::dimension2d frame_size( + dim_dst.Width, + dim_dst.Height / frame_count + ); + video::IImage *crack_scaled = create_crack_image(crack, progression, + frame_size, tiles, driver); + if (!crack_scaled) + return; + + auto blit = use_overlay ? blit_with_alpha : blit_with_alpha; + for (s32 i = 0; i < frame_count; ++i) { + v2s32 dst_pos(0, frame_size.Height * i); + blit(crack_scaled, dst, dst_pos, frame_size); + } + + crack_scaled->drop(); +} + +void brighten(video::IImage *image) +{ + if (image == NULL) + return; + + core::dimension2d dim = image->getDimension(); + + for (u32 y=0; ygetPixel(x,y); + c.setRed(0.5 * 255 + 0.5 * (float)c.getRed()); + c.setGreen(0.5 * 255 + 0.5 * (float)c.getGreen()); + c.setBlue(0.5 * 255 + 0.5 * (float)c.getBlue()); + image->setPixel(x,y,c); + } +} + +u32 parseImageTransform(std::string_view s) +{ + int total_transform = 0; + + std::string transform_names[8]; + transform_names[0] = "i"; + transform_names[1] = "r90"; + transform_names[2] = "r180"; + transform_names[3] = "r270"; + transform_names[4] = "fx"; + transform_names[6] = "fy"; + + std::size_t pos = 0; + while(pos < s.size()) + { + int transform = -1; + for (int i = 0; i <= 7; ++i) + { + const std::string &name_i = transform_names[i]; + + if (s[pos] == ('0' + i)) + { + transform = i; + pos++; + break; + } + + if (!(name_i.empty()) && lowercase(s.substr(pos, name_i.size())) == name_i) { + transform = i; + pos += name_i.size(); + break; + } + } + if (transform < 0) + break; + + // Multiply total_transform and transform in the group D4 + int new_total = 0; + if (transform < 4) + new_total = (transform + total_transform) % 4; + else + new_total = (transform - total_transform + 8) % 4; + if ((transform >= 4) ^ (total_transform >= 4)) + new_total += 4; + + total_transform = new_total; + } + return total_transform; +} + +core::dimension2d imageTransformDimension(u32 transform, core::dimension2d dim) +{ + if (transform % 2 == 0) + return dim; + + return core::dimension2d(dim.Height, dim.Width); +} + +void imageTransform(u32 transform, video::IImage *src, video::IImage *dst) +{ + if (src == NULL || dst == NULL) + return; + + core::dimension2d dstdim = dst->getDimension(); + + // Pre-conditions + assert(dstdim == imageTransformDimension(transform, src->getDimension())); + assert(transform <= 7); + + /* + Compute the transformation from source coordinates (sx,sy) + to destination coordinates (dx,dy). + */ + int sxn = 0; + int syn = 2; + if (transform == 0) // identity + sxn = 0, syn = 2; // sx = dx, sy = dy + else if (transform == 1) // rotate by 90 degrees ccw + sxn = 3, syn = 0; // sx = (H-1) - dy, sy = dx + else if (transform == 2) // rotate by 180 degrees + sxn = 1, syn = 3; // sx = (W-1) - dx, sy = (H-1) - dy + else if (transform == 3) // rotate by 270 degrees ccw + sxn = 2, syn = 1; // sx = dy, sy = (W-1) - dx + else if (transform == 4) // flip x + sxn = 1, syn = 2; // sx = (W-1) - dx, sy = dy + else if (transform == 5) // flip x then rotate by 90 degrees ccw + sxn = 2, syn = 0; // sx = dy, sy = dx + else if (transform == 6) // flip y + sxn = 0, syn = 3; // sx = dx, sy = (H-1) - dy + else if (transform == 7) // flip y then rotate by 90 degrees ccw + sxn = 3, syn = 1; // sx = (H-1) - dy, sy = (W-1) - dx + + for (u32 dy=0; dygetPixel(sx,sy); + dst->setPixel(dx,dy,c); + } +} + +namespace { + // For more colorspace transformations, see for example + // https://github.com/tobspr/GLSL-Color-Spaces/blob/master/ColorSpaces.inc.glsl + + inline float linear_to_srgb_component(float v) + { + if (v > 0.0031308f) + return 1.055f * powf(v, 1.0f / 2.4f) - 0.055f; + return 12.92f * v; + } + inline float srgb_to_linear_component(float v) + { + if (v > 0.04045f) + return powf((v + 0.055f) / 1.055f, 2.4f); + return v / 12.92f; + } + + v3f srgb_to_linear(const video::SColor col_srgb) + { + v3f col(col_srgb.getRed(), col_srgb.getGreen(), col_srgb.getBlue()); + col /= 255.0f; + col.X = srgb_to_linear_component(col.X); + col.Y = srgb_to_linear_component(col.Y); + col.Z = srgb_to_linear_component(col.Z); + return col; + } + + video::SColor linear_to_srgb(const v3f col_linear) + { + v3f col; + col.X = linear_to_srgb_component(col_linear.X); + col.Y = linear_to_srgb_component(col_linear.Y); + col.Z = linear_to_srgb_component(col_linear.Z); + col *= 255.0f; + col.X = core::clamp(col.X, 0.0f, 255.0f); + col.Y = core::clamp(col.Y, 0.0f, 255.0f); + col.Z = core::clamp(col.Z, 0.0f, 255.0f); + return video::SColor(0xff, myround(col.X), myround(col.Y), + myround(col.Z)); + } +} + + +/////////////////////////// +// ImageSource Functions // +/////////////////////////// + +#define CHECK_BASEIMG() \ + do { \ + if (!baseimg) { \ + errorstream << "generateImagePart(): baseimg == NULL" \ + << " for part_of_name=\"" << part_of_name \ + << "\", cancelling." << std::endl; \ + return false; \ + } \ + } while(0) + +#define COMPLAIN_INVALID(description) \ + do { \ + errorstream << "generateImagePart(): invalid " << (description) \ + << " for part_of_name=\"" << part_of_name \ + << "\", cancelling." << std::endl; \ + return false; \ + } while(0) + +#define CHECK_DIM(w, h) \ + do { \ + if ((w) <= 0 || (h) <= 0 || (w) >= 0xffff || (h) >= 0xffff) { \ + COMPLAIN_INVALID("width or height"); \ + } \ + } while(0) + +bool ImageSource::generateImagePart(std::string_view part_of_name, + video::IImage *& baseimg, std::set &source_image_names) +{ + const char escape = '\\'; // same as in generateImage() + video::IVideoDriver *driver = RenderingEngine::get_video_driver(); + sanity_check(driver); + + if (baseimg && (baseimg->getDimension().Width == 0 || + baseimg->getDimension().Height == 0)) { + errorstream << "generateImagePart(): baseimg is zero-sized?!" + << std::endl; + baseimg->drop(); + baseimg = nullptr; + } + + // Stuff starting with [ are special commands + if (part_of_name.empty() || part_of_name[0] != '[') { + std::string part_s(part_of_name); + source_image_names.insert(part_s); + video::IImage *image = m_sourcecache.getOrLoad(part_s); + if (!image) { + // Do not create the dummy texture + if (part_of_name.empty()) + return true; + + // Do not create normalmap dummies + if (str_ends_with(part_of_name, "_normal.png")) { + warningstream << "generateImagePart(): Could not load normal map \"" + << part_of_name << "\"" << std::endl; + return true; + } + + errorstream << "generateImagePart(): Could not load image \"" + << part_of_name << "\" while building texture; " + "Creating a dummy image" << std::endl; + + core::dimension2d dim(1,1); + image = driver->createImage(video::ECF_A8R8G8B8, dim); + sanity_check(image != NULL); + image->setPixel(0,0, video::SColor(255,myrand()%256, + myrand()%256,myrand()%256)); + } + + // If base image is NULL, load as base. + if (baseimg == NULL) + { + /* + Copy it this way to get an alpha channel. + Otherwise images with alpha cannot be blitted on + images that don't have alpha in the original file. + */ + core::dimension2d dim = image->getDimension(); + baseimg = driver->createImage(video::ECF_A8R8G8B8, dim); + image->copyTo(baseimg); + } + // Else blit on base. + else + { + blitBaseImage(image, baseimg); + } + + image->drop(); + } + else + { + // A special texture modification + + /* + [crack:N:P + [cracko:N:P + Adds a cracking texture + N = animation frame count, P = crack progression + */ + if (str_starts_with(part_of_name, "[crack")) + { + CHECK_BASEIMG(); + + // Crack image number and overlay option + // Format: crack[o][:]:: + bool use_overlay = (part_of_name[6] == 'o'); + Strfnd sf(part_of_name); + sf.next(":"); + s32 frame_count = stoi(sf.next(":")); + s32 progression = stoi(sf.next(":")); + s32 tiles = 1; + // Check whether there is the argument, that is, + // whether there are 3 arguments. If so, shift values + // as the first and not the last argument is optional. + auto s = sf.next(":"); + if (!s.empty()) { + tiles = frame_count; + frame_count = progression; + progression = stoi(s); + } + + if (progression >= 0) { + /* + Load crack image. + + It is an image with a number of cracking stages + horizontally tiled. + */ + video::IImage *img_crack = m_sourcecache.getOrLoad( + "crack_anylength.png"); + + if (img_crack) { + draw_crack(img_crack, baseimg, + use_overlay, frame_count, + progression, driver, tiles); + img_crack->drop(); + } + } + } + /* + [combine:WxH:X,Y=filename:X,Y=filename2 + Creates a bigger texture from any amount of smaller ones + */ + else if (str_starts_with(part_of_name, "[combine")) + { + Strfnd sf(part_of_name); + sf.next(":"); + u32 w0 = stoi(sf.next("x")); + u32 h0 = stoi(sf.next(":")); + if (!baseimg) { + CHECK_DIM(w0, h0); + baseimg = driver->createImage(video::ECF_A8R8G8B8, {w0, h0}); + baseimg->fill(video::SColor(0,0,0,0)); + } + + while (!sf.at_end()) { + v2s32 pos_base; + pos_base.X = stoi(sf.next(",")); + pos_base.Y = stoi(sf.next("=")); + std::string filename = unescape_string(sf.next_esc(":", escape), escape); + + auto basedim = baseimg->getDimension(); + if (pos_base.X > (s32)basedim.Width || pos_base.Y > (s32)basedim.Height) { + warningstream << "generateImagePart(): Skipping \"" + << filename << "\" as it's out-of-bounds " << pos_base + << " for [combine" << std::endl; + continue; + } + infostream << "Adding \"" << filename<< "\" to combined " + << pos_base << std::endl; + + video::IImage *img = generateImage(filename, source_image_names); + if (!img) { + errorstream << "generateImagePart(): Failed to load image \"" + << filename << "\" for [combine" << std::endl; + continue; + } + const auto dim = img->getDimension(); + if (pos_base.X + dim.Width <= 0 || pos_base.Y + dim.Height <= 0) { + warningstream << "generateImagePart(): Skipping \"" + << filename << "\" as it's out-of-bounds " << pos_base + << " for [combine" << std::endl; + img->drop(); + continue; + } + + blit_with_alpha(img, baseimg, pos_base, dim); + img->drop(); + } + } + /* + [fill:WxH:color + [fill:WxH:X,Y:color + Creates a texture of the given size and color, optionally with an , + position. An alpha value may be specified in the `Colorstring`. + */ + else if (str_starts_with(part_of_name, "[fill")) + { + u32 x = 0; + u32 y = 0; + + Strfnd sf(part_of_name); + sf.next(":"); + u32 width = stoi(sf.next("x")); + u32 height = stoi(sf.next(":")); + std::string color_or_x = sf.next(","); + + video::SColor color; + if (!parseColorString(color_or_x, color, true)) { + x = stoi(color_or_x); + y = stoi(sf.next(":")); + std::string color_str = sf.next(":"); + + if (!parseColorString(color_str, color, false)) + return false; + } + core::dimension2d dim(width, height); + + CHECK_DIM(dim.Width, dim.Height); + if (baseimg) { + auto basedim = baseimg->getDimension(); + if (x >= basedim.Width || y >= basedim.Height) + COMPLAIN_INVALID("X or Y offset"); + } + + video::IImage *img = driver->createImage(video::ECF_A8R8G8B8, dim); + img->fill(color); + + if (baseimg == nullptr) { + baseimg = img; + } else { + blit_with_alpha(img, baseimg, v2s32(x, y), dim); + img->drop(); + } + } + /* + [brighten + */ + else if (str_starts_with(part_of_name, "[brighten")) + { + CHECK_BASEIMG(); + + brighten(baseimg); + } + /* + [noalpha + Make image completely opaque. + Used for the leaves texture when in old leaves mode, so + that the transparent parts don't look completely black + when simple alpha channel is used for rendering. + */ + else if (str_starts_with(part_of_name, "[noalpha")) + { + CHECK_BASEIMG(); + core::dimension2d dim = baseimg->getDimension(); + + // Set alpha to full + for (u32 y=0; ygetPixel(x,y); + c.setAlpha(255); + baseimg->setPixel(x,y,c); + } + } + /* + [makealpha:R,G,B + Convert one color to transparent. + */ + else if (str_starts_with(part_of_name, "[makealpha:")) + { + CHECK_BASEIMG(); + + Strfnd sf(part_of_name.substr(11)); + u32 r1 = stoi(sf.next(",")); + u32 g1 = stoi(sf.next(",")); + u32 b1 = stoi(sf.next("")); + + core::dimension2d dim = baseimg->getDimension(); + + for (u32 y=0; ygetPixel(x,y); + u32 r = c.getRed(); + u32 g = c.getGreen(); + u32 b = c.getBlue(); + if (!(r == r1 && g == g1 && b == b1)) + continue; + c.setAlpha(0); + baseimg->setPixel(x,y,c); + } + } + /* + [transformN + Rotates and/or flips the image. + + N can be a number (between 0 and 7) or a transform name. + Rotations are counter-clockwise. + 0 I identity + 1 R90 rotate by 90 degrees + 2 R180 rotate by 180 degrees + 3 R270 rotate by 270 degrees + 4 FX flip X + 5 FXR90 flip X then rotate by 90 degrees + 6 FY flip Y + 7 FYR90 flip Y then rotate by 90 degrees + + Note: Transform names can be concatenated to produce + their product (applies the first then the second). + The resulting transform will be equivalent to one of the + eight existing ones, though (see: dihedral group). + */ + else if (str_starts_with(part_of_name, "[transform")) + { + CHECK_BASEIMG(); + + u32 transform = parseImageTransform(part_of_name.substr(10)); + core::dimension2d dim = imageTransformDimension( + transform, baseimg->getDimension()); + video::IImage *image = driver->createImage( + baseimg->getColorFormat(), dim); + sanity_check(image != NULL); + imageTransform(transform, baseimg, image); + baseimg->drop(); + baseimg = image; + } + /* + [inventorycube{topimage{leftimage{rightimage + In every subimage, replace ^ with &. + Create an "inventory cube". + NOTE: This should be used only on its own. + Example (a grass block (not actually used in game): + "[inventorycube{grass.png{mud.png&grass_side.png{mud.png&grass_side.png" + */ + else if (str_starts_with(part_of_name, "[inventorycube")) + { + if (baseimg) { + errorstream<<"generateImagePart(): baseimg != NULL " + <<"for part_of_name=\""<drop(); + img_left->drop(); + img_right->drop(); + + return true; + } + /* + [lowpart:percent:filename + Adds the lower part of a texture + */ + else if (str_starts_with(part_of_name, "[lowpart:")) + { + Strfnd sf(part_of_name); + sf.next(":"); + u32 percent = stoi(sf.next(":"), 0, 100); + std::string filename = unescape_string(sf.next_esc(":", escape), escape); + + video::IImage *img = generateImage(filename, source_image_names); + if (img) { + core::dimension2d dim = img->getDimension(); + if (!baseimg) + baseimg = driver->createImage(video::ECF_A8R8G8B8, dim); + + core::position2d pos_base(0, 0); + core::position2d clippos(0, 0); + clippos.Y = dim.Height * (100-percent) / 100; + core::dimension2d clipdim = dim; + clipdim.Height = clipdim.Height * percent / 100 + 1; + core::rect cliprect(clippos, clipdim); + img->copyToWithAlpha(baseimg, pos_base, + core::rect(v2s32(0,0), dim), + video::SColor(255,255,255,255), + &cliprect); + img->drop(); + } + } + /* + [verticalframe:N:I + Crops a frame of a vertical animation. + N = frame count, I = frame index + */ + else if (str_starts_with(part_of_name, "[verticalframe:")) + { + CHECK_BASEIMG(); + + Strfnd sf(part_of_name); + sf.next(":"); + u32 frame_count = stoi(sf.next(":")); + u32 frame_index = stoi(sf.next(":")); + + if (frame_count == 0){ + errorstream << "generateImagePart(): invalid frame_count " + << "for part_of_name=\"" << part_of_name + << "\", using frame_count = 1 instead." << std::endl; + frame_count = 1; + } + if (frame_index >= frame_count) + frame_index = frame_count - 1; + + v2u32 frame_size = baseimg->getDimension(); + frame_size.Y /= frame_count; + + video::IImage *img = driver->createImage(video::ECF_A8R8G8B8, + frame_size); + + // Fill target image with transparency + img->fill(video::SColor(0,0,0,0)); + + core::dimension2d dim = frame_size; + core::position2d pos_dst(0, 0); + core::position2d pos_src(0, frame_index * frame_size.Y); + baseimg->copyToWithAlpha(img, pos_dst, + core::rect(pos_src, dim), + video::SColor(255,255,255,255), + NULL); + // Replace baseimg + baseimg->drop(); + baseimg = img; + } + /* + [mask:filename + Applies a mask to an image + */ + else if (str_starts_with(part_of_name, "[mask:")) + { + CHECK_BASEIMG(); + + Strfnd sf(part_of_name); + sf.next(":"); + std::string filename = unescape_string(sf.next_esc(":", escape), escape); + + video::IImage *img = generateImage(filename, source_image_names); + if (img) { + apply_mask(img, baseimg, v2s32(0, 0), v2s32(0, 0), + img->getDimension()); + img->drop(); + } else { + errorstream << "generateImagePart(): Failed to load image \"" + << filename << "\" for [mask" << std::endl; + } + } + /* + [multiply:color + or + [screen:color + Multiply and Screen blend modes are basic blend modes for darkening and lightening + images, respectively. + A Multiply blend multiplies a given color to every pixel of an image. + A Screen blend has the opposite effect to a Multiply blend. + color = color as ColorString + */ + else if (str_starts_with(part_of_name, "[multiply:") || + str_starts_with(part_of_name, "[screen:")) { + Strfnd sf(part_of_name); + sf.next(":"); + std::string color_str = sf.next(":"); + + CHECK_BASEIMG(); + + video::SColor color; + + if (!parseColorString(color_str, color, false)) + return false; + if (str_starts_with(part_of_name, "[multiply:")) { + apply_multiplication(baseimg, v2u32(0, 0), + baseimg->getDimension(), color); + } else { + apply_screen(baseimg, v2u32(0, 0), baseimg->getDimension(), color); + } + } + /* + [colorize:color:ratio + Overlays image with given color + color = color as ColorString + ratio = optional string "alpha", or a weighting between 0 and 255 + */ + else if (str_starts_with(part_of_name, "[colorize:")) + { + Strfnd sf(part_of_name); + sf.next(":"); + std::string color_str = sf.next(":"); + std::string ratio_str = sf.next(":"); + + CHECK_BASEIMG(); + + video::SColor color; + int ratio = -1; + bool keep_alpha = false; + + if (!parseColorString(color_str, color, false)) + return false; + + if (is_number(ratio_str)) + ratio = mystoi(ratio_str, 0, 255); + else if (ratio_str == "alpha") + keep_alpha = true; + + apply_colorize(baseimg, v2u32(0, 0), baseimg->getDimension(), color, ratio, keep_alpha); + } + /* + [applyfiltersformesh + Internal modifier + */ + else if (str_starts_with(part_of_name, "[applyfiltersformesh")) + { + /* IMPORTANT: When changing this, getTextureForMesh() needs to be + * updated too. */ + + CHECK_BASEIMG(); + + // Apply the "clean transparent" filter, if needed + if (m_setting_mipmap || m_setting_bilinear_filter || + m_setting_trilinear_filter || m_setting_anisotropic_filter) + imageCleanTransparent(baseimg, 127); + + /* Upscale textures to user's requested minimum size. This is a trick to make + * filters look as good on low-res textures as on high-res ones, by making + * low-res textures BECOME high-res ones. This is helpful for worlds that + * mix high- and low-res textures, or for mods with least-common-denominator + * textures that don't have the resources to offer high-res alternatives. + */ + const bool filter = m_setting_trilinear_filter || m_setting_bilinear_filter; + const s32 scaleto = filter ? g_settings->getU16("texture_min_size") : 1; + if (scaleto > 1) { + const core::dimension2d dim = baseimg->getDimension(); + + /* Calculate scaling needed to make the shortest texture dimension + * equal to the target minimum. If e.g. this is a vertical frames + * animation, the short dimension will be the real size. + */ + u32 xscale = scaleto / dim.Width; + u32 yscale = scaleto / dim.Height; + const s32 scale = std::max(xscale, yscale); + + // Never downscale; only scale up by 2x or more. + if (scale > 1) { + u32 w = scale * dim.Width; + u32 h = scale * dim.Height; + const core::dimension2d newdim(w, h); + video::IImage *newimg = driver->createImage( + baseimg->getColorFormat(), newdim); + baseimg->copyToScaling(newimg); + baseimg->drop(); + baseimg = newimg; + } + } + } + /* + [resize:WxH + Resizes the base image to the given dimensions + */ + else if (str_starts_with(part_of_name, "[resize")) + { + CHECK_BASEIMG(); + + Strfnd sf(part_of_name); + sf.next(":"); + u32 width = stoi(sf.next("x")); + u32 height = stoi(sf.next("")); + CHECK_DIM(width, height); + + video::IImage *image = driver-> + createImage(video::ECF_A8R8G8B8, {width, height}); + baseimg->copyToScaling(image); + baseimg->drop(); + baseimg = image; + } + /* + [opacity:R + Makes the base image transparent according to the given ratio. + R must be between 0 and 255. + 0 means totally transparent. + 255 means totally opaque. + */ + else if (str_starts_with(part_of_name, "[opacity:")) { + CHECK_BASEIMG(); + + Strfnd sf(part_of_name); + sf.next(":"); + + u32 ratio = mystoi(sf.next(""), 0, 255); + + core::dimension2d dim = baseimg->getDimension(); + + for (u32 y = 0; y < dim.Height; y++) + for (u32 x = 0; x < dim.Width; x++) + { + video::SColor c = baseimg->getPixel(x, y); + c.setAlpha(floor((c.getAlpha() * ratio) / 255 + 0.5)); + baseimg->setPixel(x, y, c); + } + } + /* + [invert:mode + Inverts the given channels of the base image. + Mode may contain the characters "r", "g", "b", "a". + Only the channels that are mentioned in the mode string + will be inverted. + */ + else if (str_starts_with(part_of_name, "[invert:")) { + CHECK_BASEIMG(); + + Strfnd sf(part_of_name); + sf.next(":"); + + std::string mode = sf.next(""); + u32 mask = 0; + if (mode.find('a') != std::string::npos) + mask |= 0xff000000UL; + if (mode.find('r') != std::string::npos) + mask |= 0x00ff0000UL; + if (mode.find('g') != std::string::npos) + mask |= 0x0000ff00UL; + if (mode.find('b') != std::string::npos) + mask |= 0x000000ffUL; + + core::dimension2d dim = baseimg->getDimension(); + + for (u32 y = 0; y < dim.Height; y++) + for (u32 x = 0; x < dim.Width; x++) + { + video::SColor c = baseimg->getPixel(x, y); + c.color ^= mask; + baseimg->setPixel(x, y, c); + } + } + /* + [sheet:WxH:X,Y + Retrieves a tile at position X,Y (in tiles) + from the base image it assumes to be a + tilesheet with dimensions W,H (in tiles). + */ + else if (str_starts_with(part_of_name, "[sheet:")) { + CHECK_BASEIMG(); + + Strfnd sf(part_of_name); + sf.next(":"); + u32 w0 = stoi(sf.next("x")); + u32 h0 = stoi(sf.next(":")); + u32 x0 = stoi(sf.next(",")); + u32 y0 = stoi(sf.next(":")); + + CHECK_DIM(w0, h0); + if (x0 >= w0 || y0 >= h0) + COMPLAIN_INVALID("tile position (X,Y)"); + + core::dimension2d img_dim = baseimg->getDimension(); + core::dimension2d tile_dim(v2u32(img_dim) / v2u32(w0, h0)); + if (tile_dim.Width == 0) + tile_dim.Width = 1; + if (tile_dim.Height == 0) + tile_dim.Height = 1; + + video::IImage *img = driver->createImage( + video::ECF_A8R8G8B8, tile_dim); + img->fill(video::SColor(0,0,0,0)); + + v2u32 vdim(tile_dim); + core::rect rect(v2s32(x0 * vdim.X, y0 * vdim.Y), tile_dim); + baseimg->copyToWithAlpha(img, v2s32(0), rect, + video::SColor(255,255,255,255), NULL); + + // Replace baseimg + baseimg->drop(); + baseimg = img; + } + /* + [png:base64 + Decodes a PNG image in base64 form. + Use minetest.encode_png and minetest.encode_base64 + to produce a valid string. + */ + else if (str_starts_with(part_of_name, "[png:")) { + std::string png; + { + auto blob = part_of_name.substr(5); + if (!base64_is_valid(blob)) { + errorstream << "generateImagePart(): " + << "malformed base64 in [png" << std::endl; + return false; + } + png = base64_decode(blob); + } + + auto *device = RenderingEngine::get_raw_device(); + auto *fs = device->getFileSystem(); + auto *vd = device->getVideoDriver(); + auto *memfile = fs->createMemoryReadFile(png.data(), png.size(), "__temp_png"); + video::IImage* pngimg = vd->createImageFromFile(memfile); + memfile->drop(); + + if (!pngimg) { + errorstream << "generateImagePart(): Invalid PNG data" << std::endl; + return false; + } + + if (baseimg) { + blitBaseImage(pngimg, baseimg); + } else { + core::dimension2d dim = pngimg->getDimension(); + baseimg = driver->createImage(video::ECF_A8R8G8B8, dim); + pngimg->copyTo(baseimg); + } + pngimg->drop(); + } + /* + [hsl:hue:saturation:lightness + or + [colorizehsl:hue:saturation:lightness + + Adjust the hue, saturation, and lightness of the base image. Like + "Hue-Saturation" in GIMP, but with 0 as the mid-point. + Hue should be from -180 to +180, though 0 to 360 is also supported. + Saturation and lightness are optional, with lightness from -100 to + +100, and sauration from -100 to +100-or-higher. + + If colorize is true then saturation is from 0 to 100, and the image + will be converted to a grayscale image as though seen through a + colored glass, like "Colorize" in GIMP. + */ + else if (str_starts_with(part_of_name, "[hsl:") || + str_starts_with(part_of_name, "[colorizehsl:")) { + + CHECK_BASEIMG(); + + bool colorize = str_starts_with(part_of_name, "[colorizehsl:"); + + // saturation range is 0 to 100 when colorize is true + s32 defaultSaturation = colorize ? 50 : 0; + + Strfnd sf(part_of_name); + sf.next(":"); + s32 hue = mystoi(sf.next(":"), -180, 360); + s32 saturation = sf.at_end() ? defaultSaturation : mystoi(sf.next(":"), -100, 1000); + s32 lightness = sf.at_end() ? 0 : mystoi(sf.next(":"), -100, 100); + + + apply_hue_saturation(baseimg, v2u32(0, 0), baseimg->getDimension(), + hue, saturation, lightness, colorize); + } + /* + [overlay:filename + or + [hardlight:filename + + "A.png^[hardlight:B.png" is the same as "B.png^[overlay:A.Png" + + Applies an Overlay or Hard Light blend between two images, like the + layer modes of the same names in GIMP. + Overlay combines Multiply and Screen blend modes. The parts of the + top layer where the base layer is light become lighter, the parts + where the base layer is dark become darker. Areas where the base + layer are mid grey are unaffected. An overlay with the same picture + looks like an S-curve. + + Swapping the top layer and base layer is a Hard Light blend + */ + else if (str_starts_with(part_of_name, "[overlay:") || + str_starts_with(part_of_name, "[hardlight:")) { + + CHECK_BASEIMG(); + + Strfnd sf(part_of_name); + sf.next(":"); + std::string filename = unescape_string(sf.next_esc(":", escape), escape); + + video::IImage *img = generateImage(filename, source_image_names); + if (img) { + upscaleImagesToMatchLargest(baseimg, img); + + bool hardlight = str_starts_with(part_of_name, "[hardlight:"); + apply_overlay(img, baseimg, v2s32(0, 0), v2s32(0, 0), + img->getDimension(), hardlight); + img->drop(); + } else { + errorstream << "generateImage(): Failed to load image \"" + << filename << "\" for [overlay or [hardlight" << std::endl; + } + } + /* + [contrast:C:B + + Adjust the brightness and contrast of the base image. Conceptually + like GIMP's "Brightness-Contrast" feature but allows brightness to + be wound all the way up to white or down to black. + C and B are both values from -127 to +127. + B is optional. + */ + else if (str_starts_with(part_of_name, "[contrast:")) { + + CHECK_BASEIMG(); + + Strfnd sf(part_of_name); + sf.next(":"); + s32 contrast = mystoi(sf.next(":"), -127, 127); + s32 brightness = sf.at_end() ? 0 : mystoi(sf.next(":"), -127, 127); + + apply_brightness_contrast(baseimg, v2u32(0, 0), + baseimg->getDimension(), brightness, contrast); + } + else + { + errorstream << "generateImagePart(): Invalid " + " modification: \"" << part_of_name << "\"" << std::endl; + } + } + + return true; +} + +#undef CHECK_BASEIMG + +#undef COMPLAIN_INVALID + +#undef CHECK_DIM + + +video::IImage* ImageSource::generateImage(std::string_view name, + std::set &source_image_names) +{ + // Get the base image + + const char separator = '^'; + const char escape = '\\'; + const char paren_open = '('; + const char paren_close = ')'; + + // Find last separator in the name + s32 last_separator_pos = -1; + u8 paren_bal = 0; + for (s32 i = name.size() - 1; i >= 0; i--) { + if (i > 0 && name[i-1] == escape) + continue; + switch (name[i]) { + case separator: + if (paren_bal == 0) { + last_separator_pos = i; + i = -1; // break out of loop + } + break; + case paren_open: + if (paren_bal == 0) { + errorstream << "generateImage(): unbalanced parentheses" + << "(extranous '(') while generating texture \"" + << name << "\"" << std::endl; + return NULL; + } + paren_bal--; + break; + case paren_close: + paren_bal++; + break; + default: + break; + } + } + if (paren_bal > 0) { + errorstream << "generateImage(): unbalanced parentheses" + << "(missing matching '(') while generating texture \"" + << name << "\"" << std::endl; + return NULL; + } + + + video::IImage *baseimg = NULL; + + /* + If separator was found, make the base image + using a recursive call. + */ + if (last_separator_pos != -1) { + baseimg = generateImage(name.substr(0, last_separator_pos), source_image_names); + } + + /* + Parse out the last part of the name of the image and act + according to it + */ + + auto last_part_of_name = name.substr(last_separator_pos + 1); + + /* + If this name is enclosed in parentheses, generate it + and blit it onto the base image + */ + if (last_part_of_name.empty()) { + // keep baseimg == nullptr + } else if (last_part_of_name[0] == paren_open + && last_part_of_name.back() == paren_close) { + auto name2 = last_part_of_name.substr(1, + last_part_of_name.size() - 2); + video::IImage *tmp = generateImage(name2, source_image_names); + if (!tmp) { + errorstream << "generateImage(): " + "Failed to generate \"" << name2 << "\"" + << std::endl; + return NULL; + } + + if (baseimg) { + core::dimension2d dim = tmp->getDimension(); + blit_with_alpha(tmp, baseimg, v2s32(0, 0), dim); + tmp->drop(); + } else { + baseimg = tmp; + } + } 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 << "\"" + << std::endl; + } + + // If no resulting image, print a warning + if (baseimg == NULL) { + errorstream << "generateImage(): baseimg is NULL (attempted to" + " create texture \"" << name << "\")" << std::endl; + } else if (baseimg->getDimension().Width == 0 || + baseimg->getDimension().Height == 0) { + errorstream << "generateImage(): zero-sized image was created?! " + "(attempted to create texture \"" << name << "\")" << std::endl; + baseimg->drop(); + baseimg = nullptr; + } + + return baseimg; +} + +video::SColor ImageSource::getImageAverageColor(const video::IImage &image) +{ + video::SColor c(0, 0, 0, 0); + u32 total = 0; + v3f col_acc(0, 0, 0); + core::dimension2d dim = image.getDimension(); + u16 step = 1; + if (dim.Width > 16) + step = dim.Width / 16; + for (u16 x = 0; x < dim.Width; x += step) { + for (u16 y = 0; y < dim.Width; y += step) { + c = image.getPixel(x,y); + if (c.getAlpha() > 0) { + total++; + col_acc += srgb_to_linear(c); + } + } + } + if (total > 0) { + col_acc /= total; + c = linear_to_srgb(col_acc); + } + c.setAlpha(255); + return c; +} + +void ImageSource::insertImage(const std::string &name, video::IImage *img, bool prefer_local) { + m_sourcecache.insert(name, img, prefer_local); +} diff --git a/src/client/imagesource.h b/src/client/imagesource.h new file mode 100644 index 000000000..15c982f8a --- /dev/null +++ b/src/client/imagesource.h @@ -0,0 +1,84 @@ +/* +Minetest +Copyright (C) 2010-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 +#include +#include "settings.h" + +// This file is only for internal generation/modification of images. +// Use texturesource.h instead to handle textures. + +// A cache used for storing source images. +class SourceImageCache { +public: + ~SourceImageCache(); + + void insert(const std::string &name, video::IImage *img, bool prefer_local); + + video::IImage* get(const std::string &name); + + // Primarily fetches from cache, secondarily tries to read from filesystem + video::IImage *getOrLoad(const std::string &name); +private: + std::map m_images; +}; + +/* + * Generates and caches images. + * The image name defines the image by filename and texture modifiers. +*/ +struct ImageSource { + /*! Generates an image from a full string like + * "stone.png^mineral_coal.png^[crack:1:0". + * The returned Image should be dropped. + * source_image_names is important to determine when to flush the image from a cache (dynamic media) + */ + video::IImage* generateImage(std::string_view name, std::set &source_image_names); + + // To add self made images. + void insertImage(const std::string &name, video::IImage *img, bool prefer_local); + + // TODO should probably be moved elsewhere + static video::SColor getImageAverageColor(const video::IImage &image); + + ImageSource() : + m_setting_mipmap{g_settings->getBool("mip_map")}, + m_setting_trilinear_filter{g_settings->getBool("trilinear_filter")}, + m_setting_bilinear_filter{g_settings->getBool("bilinear_filter")}, + m_setting_anisotropic_filter{g_settings->getBool("anisotropic_filter")} + {}; + +private: + + // Generate image based on a string like "stone.png" or "[crack:1:0". + // if baseimg is NULL, it is created. Otherwise stuff is made on it. + // source_image_names is important to determine when to flush the image from a cache (dynamic media) + bool generateImagePart(std::string_view part_of_name, video::IImage *& baseimg, std::set &source_image_names); + + // Cached settings needed for making textures from meshes + bool m_setting_mipmap; + bool m_setting_trilinear_filter; + bool m_setting_bilinear_filter; + bool m_setting_anisotropic_filter; + + // Cache of source images + SourceImageCache m_sourcecache; +}; diff --git a/src/client/texturesource.cpp b/src/client/texturesource.cpp index e23b88dbe..7e56a57cd 100644 --- a/src/client/texturesource.cpp +++ b/src/client/texturesource.cpp @@ -20,17 +20,12 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "texturesource.h" #include -#include #include "util/thread.h" -#include "settings.h" -#include "util/strfnd.h" #include "imagefilters.h" #include "guiscalingfilter.h" #include "renderingengine.h" -#include "util/base64.h" -#include "irrlicht_changes/printing.h" -#include "util/numeric.h" #include "texturepaths.h" +#include "imagesource.h" /* Stores internal information about a texture. @@ -63,90 +58,6 @@ struct TextureInfo } }; -/* - SourceImageCache: A cache used for storing source images. -*/ - -class SourceImageCache -{ -public: - ~SourceImageCache() { - for (auto &m_image : m_images) { - m_image.second->drop(); - } - m_images.clear(); - } - void insert(const std::string &name, video::IImage *img, bool prefer_local) - { - assert(img); // Pre-condition - // Remove old image - std::map::iterator n; - n = m_images.find(name); - if (n != m_images.end()){ - if (n->second) - n->second->drop(); - } - - video::IImage* toadd = img; - bool need_to_grab = true; - - // Try to use local texture instead if asked to - if (prefer_local) { - bool is_base_pack; - std::string path = getTexturePath(name, &is_base_pack); - // Ignore base pack - if (!path.empty() && !is_base_pack) { - video::IImage *img2 = RenderingEngine::get_video_driver()-> - createImageFromFile(path.c_str()); - if (img2){ - toadd = img2; - need_to_grab = false; - } - } - } - - if (need_to_grab) - toadd->grab(); - m_images[name] = toadd; - } - video::IImage* get(const std::string &name) - { - std::map::iterator n; - n = m_images.find(name); - if (n != m_images.end()) - return n->second; - return NULL; - } - // Primarily fetches from cache, secondarily tries to read from filesystem - video::IImage *getOrLoad(const std::string &name) - { - std::map::iterator n; - n = m_images.find(name); - if (n != m_images.end()){ - n->second->grab(); // Grab for caller - return n->second; - } - video::IVideoDriver *driver = RenderingEngine::get_video_driver(); - std::string path = getTexturePath(name); - if (path.empty()) { - infostream<<"SourceImageCache::getOrLoad(): No path found for \"" - <createImageFromFile(path.c_str()); - - if (img){ - m_images[name] = img; - img->grab(); // Grab for caller - } - return img; - } -private: - std::map m_images; -}; - /* TextureSource */ @@ -256,9 +167,9 @@ private: // The id of the thread that is allowed to use irrlicht directly std::thread::id m_main_thread; - // Cache of source images + // Generates and caches source images // This should be only accessed from the main thread - SourceImageCache m_sourcecache; + ImageSource m_imagesource; // Rebuild images and textures from the current set of source images // Shall be called from the main thread. @@ -268,19 +179,6 @@ private: // Generate a texture u32 generateTexture(const std::string &name); - // Generate image based on a string like "stone.png" or "[crack:1:0". - // if baseimg is NULL, it is created. Otherwise stuff is made on it. - // source_image_names is important to determine when to flush the image from a cache (dynamic media) - bool generateImagePart(std::string_view part_of_name, video::IImage *& baseimg, std::set &source_image_names); - - /*! Generates an image from a full string like - * "stone.png^mineral_coal.png^[crack:1:0". - * Shall be called from the main thread. - * The returned Image should be dropped. - * source_image_names is important to determine when to flush the image from a cache (dynamic media) - */ - video::IImage* generateImage(std::string_view name, std::set &source_image_names); - // Thread-safe cache of what source images are known (true = known) MutexedMap m_source_image_existence; @@ -404,78 +302,6 @@ u32 TextureSource::getTextureId(const std::string &name) return 0; } - -/** Draw an image on top of another one with gamma-incorrect alpha compositing - * - * This exists because IImage::copyToWithAlpha() doesn't seem to always work. - * - * \tparam overlay If enabled, only modify pixels in dst which are fully opaque. - * Defaults to false. - * \param src Top image. This image must have the ECF_A8R8G8B8 color format. - * \param dst Bottom image. - * The top image is drawn onto this base image in-place. - * \param dst_pos An offset vector to move src before drawing it onto dst - * \param size Size limit of the copied area -*/ -template -static void blit_with_alpha(video::IImage *src, video::IImage *dst, - v2s32 dst_pos, v2u32 size); - -// Apply a color to an image. Uses an int (0-255) to calculate the ratio. -// If the ratio is 255 or -1 and keep_alpha is true, then it multiples the -// color alpha with the destination alpha. -// Otherwise, any pixels that are not fully transparent get the color alpha. -static void apply_colorize(video::IImage *dst, v2u32 dst_pos, v2u32 size, - const video::SColor color, int ratio, bool keep_alpha); - -// paint a texture using the given color -static void apply_multiplication(video::IImage *dst, v2u32 dst_pos, v2u32 size, - const video::SColor color); - -// Perform a Screen blend with the given color. The opposite effect of a -// Multiply blend. -static void apply_screen(video::IImage *dst, v2u32 dst_pos, v2u32 size, - const video::SColor color); - -// Adjust the hue, saturation, and lightness of destination. Like -// "Hue-Saturation" in GIMP. -// If colorize is true then the image will be converted to a grayscale -// image as though seen through a colored glass, like "Colorize" in GIMP. -static void apply_hue_saturation(video::IImage *dst, v2u32 dst_pos, v2u32 size, - s32 hue, s32 saturation, s32 lightness, bool colorize); - -// Apply an overlay blend to an images. -// Overlay blend combines Multiply and Screen blend modes.The parts of the top -// layer where the base layer is light become lighter, the parts where the base -// layer is dark become darker.Areas where the base layer are mid grey are -// unaffected.An overlay with the same picture looks like an S - curve. -static void apply_overlay(video::IImage *overlay, video::IImage *dst, - v2s32 overlay_pos, v2s32 dst_pos, v2u32 size, bool hardlight); - -// Adjust the brightness and contrast of the base image. Conceptually like -// "Brightness-Contrast" in GIMP but allowing brightness to be wound all the -// way up to white or down to black. -static void apply_brightness_contrast(video::IImage *dst, v2u32 dst_pos, v2u32 size, - s32 brightness, s32 contrast); - -// Apply a mask to an image -static void apply_mask(video::IImage *mask, video::IImage *dst, - v2s32 mask_pos, v2s32 dst_pos, v2u32 size); - -// Draw or overlay a crack -static void draw_crack(video::IImage *crack, video::IImage *dst, - bool use_overlay, s32 frame_count, s32 progression, - video::IVideoDriver *driver, u8 tiles = 1); - -// Brighten image -void brighten(video::IImage *image); -// Parse a transform name -u32 parseImageTransform(std::string_view s); -// Apply transform to image dimension -core::dimension2d imageTransformDimension(u32 transform, core::dimension2d dim); -// Apply transform to image data -void imageTransform(u32 transform, video::IImage *src, video::IImage *dst); - /* This method generates all the textures */ @@ -512,7 +338,7 @@ u32 TextureSource::generateTexture(const std::string &name) // passed into texture info for dynamic media tracking std::set source_image_names; - video::IImage *img = generateImage(name, source_image_names); + video::IImage *img = m_imagesource.generateImage(name, source_image_names); video::ITexture *tex = NULL; @@ -595,7 +421,7 @@ Palette* TextureSource::getPalette(const std::string &name) if (it == m_palettes.end()) { // Create palette std::set source_image_names; // unused, sadly. - video::IImage *img = generateImage(name, source_image_names); + video::IImage *img = m_imagesource.generateImage(name, source_image_names); if (!img) { warningstream << "TextureSource::getPalette(): palette \"" << name << "\" could not be loaded." << std::endl; @@ -658,7 +484,7 @@ void TextureSource::insertSourceImage(const std::string &name, video::IImage *im { sanity_check(std::this_thread::get_id() == m_main_thread); - m_sourcecache.insert(name, img, true); + m_imagesource.insertImage(name, img, true); m_source_image_existence.set(name, true); // now we need to check for any textures that need updating @@ -707,7 +533,7 @@ void TextureSource::rebuildTexture(video::IVideoDriver *driver, TextureInfo &ti) // replaces the previous sourceImages // shouldn't really need to be done, but can't hurt std::set source_image_names; - video::IImage *img = generateImage(ti.name, source_image_names); + video::IImage *img = m_imagesource.generateImage(ti.name, source_image_names); img = Align2Npot2(img, driver); // Create texture from resulting image video::ITexture *t = NULL; @@ -725,1715 +551,6 @@ void TextureSource::rebuildTexture(video::IVideoDriver *driver, TextureInfo &ti) m_texture_trash.push_back(t_old); } -inline static void applyShadeFactor(video::SColor &color, u32 factor) -{ - u32 f = core::clamp(factor, 0, 256); - color.setRed(color.getRed() * f / 256); - color.setGreen(color.getGreen() * f / 256); - color.setBlue(color.getBlue() * f / 256); -} - -static video::IImage *createInventoryCubeImage( - video::IImage *top, video::IImage *left, video::IImage *right) -{ - core::dimension2du size_top = top->getDimension(); - core::dimension2du size_left = left->getDimension(); - core::dimension2du size_right = right->getDimension(); - - u32 size = npot2(std::max({ - size_top.Width, size_top.Height, - size_left.Width, size_left.Height, - size_right.Width, size_right.Height, - })); - - // It must be divisible by 4, to let everything work correctly. - // But it is a power of 2, so being at least 4 is the same. - // And the resulting texture should't be too large as well. - size = core::clamp(size, 4, 64); - - // With such parameters, the cube fits exactly, touching each image line - // from `0` to `cube_size - 1`. (Note that division is exact here). - u32 cube_size = 9 * size; - u32 offset = size / 2; - - video::IVideoDriver *driver = RenderingEngine::get_video_driver(); - - auto lock_image = [size, driver] (video::IImage *&image) -> const u32 * { - image->grab(); - core::dimension2du dim = image->getDimension(); - video::ECOLOR_FORMAT format = image->getColorFormat(); - if (dim.Width != size || dim.Height != size || format != video::ECF_A8R8G8B8) { - video::IImage *scaled = driver->createImage(video::ECF_A8R8G8B8, {size, size}); - image->copyToScaling(scaled); - image->drop(); - image = scaled; - } - sanity_check(image->getPitch() == 4 * size); - return reinterpret_cast(image->getData()); - }; - auto free_image = [] (video::IImage *image) -> void { - image->drop(); - }; - - video::IImage *result = driver->createImage(video::ECF_A8R8G8B8, {cube_size, cube_size}); - sanity_check(result->getPitch() == 4 * cube_size); - result->fill(video::SColor(0x00000000u)); - u32 *target = reinterpret_cast(result->getData()); - - // Draws single cube face - // `shade_factor` is face brightness, in range [0.0, 1.0] - // (xu, xv, x1; yu, yv, y1) form coordinate transformation matrix - // `offsets` list pixels to be drawn for single source pixel - auto draw_image = [=] (video::IImage *image, float shade_factor, - s16 xu, s16 xv, s16 x1, - s16 yu, s16 yv, s16 y1, - std::initializer_list offsets) -> void { - u32 brightness = core::clamp(256 * shade_factor, 0, 256); - const u32 *source = lock_image(image); - for (u16 v = 0; v < size; v++) { - for (u16 u = 0; u < size; u++) { - video::SColor pixel(*source); - applyShadeFactor(pixel, brightness); - s16 x = xu * u + xv * v + x1; - s16 y = yu * u + yv * v + y1; - for (const auto &off : offsets) - target[(y + off.Y) * cube_size + (x + off.X) + offset] = pixel.color; - source++; - } - } - free_image(image); - }; - - draw_image(top, 1.000000f, - 4, -4, 4 * (size - 1), - 2, 2, 0, - { - {2, 0}, {3, 0}, {4, 0}, {5, 0}, - {0, 1}, {1, 1}, {2, 1}, {3, 1}, {4, 1}, {5, 1}, {6, 1}, {7, 1}, - {2, 2}, {3, 2}, {4, 2}, {5, 2}, - }); - - draw_image(left, 0.836660f, - 4, 0, 0, - 2, 5, 2 * size, - { - {0, 0}, {1, 0}, - {0, 1}, {1, 1}, {2, 1}, {3, 1}, - {0, 2}, {1, 2}, {2, 2}, {3, 2}, - {0, 3}, {1, 3}, {2, 3}, {3, 3}, - {0, 4}, {1, 4}, {2, 4}, {3, 4}, - {2, 5}, {3, 5}, - }); - - draw_image(right, 0.670820f, - 4, 0, 4 * size, - -2, 5, 4 * size - 2, - { - {2, 0}, {3, 0}, - {0, 1}, {1, 1}, {2, 1}, {3, 1}, - {0, 2}, {1, 2}, {2, 2}, {3, 2}, - {0, 3}, {1, 3}, {2, 3}, {3, 3}, - {0, 4}, {1, 4}, {2, 4}, {3, 4}, - {0, 5}, {1, 5}, - }); - - return result; -} - -video::IImage* TextureSource::generateImage(std::string_view name, - std::set &source_image_names) -{ - // Get the base image - - const char separator = '^'; - const char escape = '\\'; - const char paren_open = '('; - const char paren_close = ')'; - - // Find last separator in the name - s32 last_separator_pos = -1; - u8 paren_bal = 0; - for (s32 i = name.size() - 1; i >= 0; i--) { - if (i > 0 && name[i-1] == escape) - continue; - switch (name[i]) { - case separator: - if (paren_bal == 0) { - last_separator_pos = i; - i = -1; // break out of loop - } - break; - case paren_open: - if (paren_bal == 0) { - errorstream << "generateImage(): unbalanced parentheses" - << "(extranous '(') while generating texture \"" - << name << "\"" << std::endl; - return NULL; - } - paren_bal--; - break; - case paren_close: - paren_bal++; - break; - default: - break; - } - } - if (paren_bal > 0) { - errorstream << "generateImage(): unbalanced parentheses" - << "(missing matching '(') while generating texture \"" - << name << "\"" << std::endl; - return NULL; - } - - - video::IImage *baseimg = NULL; - - /* - If separator was found, make the base image - using a recursive call. - */ - if (last_separator_pos != -1) { - baseimg = generateImage(name.substr(0, last_separator_pos), source_image_names); - } - - /* - Parse out the last part of the name of the image and act - according to it - */ - - auto last_part_of_name = name.substr(last_separator_pos + 1); - - /* - If this name is enclosed in parentheses, generate it - and blit it onto the base image - */ - if (last_part_of_name.empty()) { - // keep baseimg == nullptr - } else if (last_part_of_name[0] == paren_open - && last_part_of_name.back() == paren_close) { - auto name2 = last_part_of_name.substr(1, - last_part_of_name.size() - 2); - video::IImage *tmp = generateImage(name2, source_image_names); - if (!tmp) { - errorstream << "generateImage(): " - "Failed to generate \"" << name2 << "\"" - << std::endl; - return NULL; - } - - if (baseimg) { - core::dimension2d dim = tmp->getDimension(); - blit_with_alpha(tmp, baseimg, v2s32(0, 0), dim); - tmp->drop(); - } else { - baseimg = tmp; - } - } 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 << "\"" - << std::endl; - } - - // If no resulting image, print a warning - if (baseimg == NULL) { - errorstream << "generateImage(): baseimg is NULL (attempted to" - " create texture \"" << name << "\")" << std::endl; - } else if (baseimg->getDimension().Width == 0 || - baseimg->getDimension().Height == 0) { - errorstream << "generateImage(): zero-sized image was created?! " - "(attempted to create texture \"" << name << "\")" << std::endl; - baseimg->drop(); - baseimg = nullptr; - } - - return baseimg; -} - -static std::string unescape_string(const std::string &str, const char esc = '\\') -{ - std::string out; - size_t pos = 0, cpos; - out.reserve(str.size()); - while (1) { - cpos = str.find_first_of(esc, pos); - if (cpos == std::string::npos) { - out += str.substr(pos); - break; - } - out += str.substr(pos, cpos - pos) + str[cpos + 1]; - pos = cpos + 2; - } - return out; -} - -/* - Replaces the smaller of the two images with one upscaled to match the - dimensions of the other. - Ensure no other references to these images are being held, as one may - get dropped and switched with a new image. -*/ -void upscaleImagesToMatchLargest(video::IImage *& img1, - video::IImage *& img2) -{ - core::dimension2d dim1 = img1->getDimension(); - core::dimension2d dim2 = img2->getDimension(); - - if (dim1 == dim2) { - // image dimensions match, no scaling required - - } - else if (dim1.Width * dim1.Height < dim2.Width * dim2.Height) { - // Upscale img1 - video::IImage *scaled_image = RenderingEngine::get_video_driver()-> - createImage(video::ECF_A8R8G8B8, dim2); - img1->copyToScaling(scaled_image); - img1->drop(); - img1 = scaled_image; - - } else { - // Upscale img2 - video::IImage *scaled_image = RenderingEngine::get_video_driver()-> - createImage(video::ECF_A8R8G8B8, dim1); - img2->copyToScaling(scaled_image); - img2->drop(); - img2 = scaled_image; - } -} - -void blitBaseImage(video::IImage* &src, video::IImage* &dst) -{ - //infostream<<"Blitting "< dim_dst = dst->getDimension(); - // Position to copy the blitted to in the base image - core::position2d pos_to(0,0); - - blit_with_alpha(src, dst, pos_to, dim_dst); -} - -#define CHECK_BASEIMG() \ - do { \ - if (!baseimg) { \ - errorstream << "generateImagePart(): baseimg == NULL" \ - << " for part_of_name=\"" << part_of_name \ - << "\", cancelling." << std::endl; \ - return false; \ - } \ - } while(0) - -#define COMPLAIN_INVALID(description) \ - do { \ - errorstream << "generateImagePart(): invalid " << (description) \ - << " for part_of_name=\"" << part_of_name \ - << "\", cancelling." << std::endl; \ - return false; \ - } while(0) - -#define CHECK_DIM(w, h) \ - do { \ - if ((w) <= 0 || (h) <= 0 || (w) >= 0xffff || (h) >= 0xffff) { \ - COMPLAIN_INVALID("width or height"); \ - } \ - } while(0) - -bool TextureSource::generateImagePart(std::string_view part_of_name, - video::IImage *& baseimg, std::set &source_image_names) -{ - const char escape = '\\'; // same as in generateImage() - video::IVideoDriver *driver = RenderingEngine::get_video_driver(); - sanity_check(driver); - - if (baseimg && (baseimg->getDimension().Width == 0 || - baseimg->getDimension().Height == 0)) { - errorstream << "generateImagePart(): baseimg is zero-sized?!" - << std::endl; - baseimg->drop(); - baseimg = nullptr; - } - - // Stuff starting with [ are special commands - if (part_of_name.empty() || part_of_name[0] != '[') { - std::string part_s(part_of_name); - source_image_names.insert(part_s); - video::IImage *image = m_sourcecache.getOrLoad(part_s); - if (!image) { - // Do not create the dummy texture - if (part_of_name.empty()) - return true; - - // Do not create normalmap dummies - if (str_ends_with(part_of_name, "_normal.png")) { - warningstream << "generateImagePart(): Could not load normal map \"" - << part_of_name << "\"" << std::endl; - return true; - } - - errorstream << "generateImagePart(): Could not load image \"" - << part_of_name << "\" while building texture; " - "Creating a dummy image" << std::endl; - - core::dimension2d dim(1,1); - image = driver->createImage(video::ECF_A8R8G8B8, dim); - sanity_check(image != NULL); - image->setPixel(0,0, video::SColor(255,myrand()%256, - myrand()%256,myrand()%256)); - } - - // If base image is NULL, load as base. - if (baseimg == NULL) - { - /* - Copy it this way to get an alpha channel. - Otherwise images with alpha cannot be blitted on - images that don't have alpha in the original file. - */ - core::dimension2d dim = image->getDimension(); - baseimg = driver->createImage(video::ECF_A8R8G8B8, dim); - image->copyTo(baseimg); - } - // Else blit on base. - else - { - blitBaseImage(image, baseimg); - } - - image->drop(); - } - else - { - // A special texture modification - - /* - [crack:N:P - [cracko:N:P - Adds a cracking texture - N = animation frame count, P = crack progression - */ - if (str_starts_with(part_of_name, "[crack")) - { - CHECK_BASEIMG(); - - // Crack image number and overlay option - // Format: crack[o][:]:: - bool use_overlay = (part_of_name[6] == 'o'); - Strfnd sf(part_of_name); - sf.next(":"); - s32 frame_count = stoi(sf.next(":")); - s32 progression = stoi(sf.next(":")); - s32 tiles = 1; - // Check whether there is the argument, that is, - // whether there are 3 arguments. If so, shift values - // as the first and not the last argument is optional. - auto s = sf.next(":"); - if (!s.empty()) { - tiles = frame_count; - frame_count = progression; - progression = stoi(s); - } - - if (progression >= 0) { - /* - Load crack image. - - It is an image with a number of cracking stages - horizontally tiled. - */ - video::IImage *img_crack = m_sourcecache.getOrLoad( - "crack_anylength.png"); - - if (img_crack) { - draw_crack(img_crack, baseimg, - use_overlay, frame_count, - progression, driver, tiles); - img_crack->drop(); - } - } - } - /* - [combine:WxH:X,Y=filename:X,Y=filename2 - Creates a bigger texture from any amount of smaller ones - */ - else if (str_starts_with(part_of_name, "[combine")) - { - Strfnd sf(part_of_name); - sf.next(":"); - u32 w0 = stoi(sf.next("x")); - u32 h0 = stoi(sf.next(":")); - if (!baseimg) { - CHECK_DIM(w0, h0); - baseimg = driver->createImage(video::ECF_A8R8G8B8, {w0, h0}); - baseimg->fill(video::SColor(0,0,0,0)); - } - - while (!sf.at_end()) { - v2s32 pos_base; - pos_base.X = stoi(sf.next(",")); - pos_base.Y = stoi(sf.next("=")); - std::string filename = unescape_string(sf.next_esc(":", escape), escape); - - auto basedim = baseimg->getDimension(); - if (pos_base.X > (s32)basedim.Width || pos_base.Y > (s32)basedim.Height) { - warningstream << "generateImagePart(): Skipping \"" - << filename << "\" as it's out-of-bounds " << pos_base - << " for [combine" << std::endl; - continue; - } - infostream << "Adding \"" << filename<< "\" to combined " - << pos_base << std::endl; - - video::IImage *img = generateImage(filename, source_image_names); - if (!img) { - errorstream << "generateImagePart(): Failed to load image \"" - << filename << "\" for [combine" << std::endl; - continue; - } - const auto dim = img->getDimension(); - if (pos_base.X + dim.Width <= 0 || pos_base.Y + dim.Height <= 0) { - warningstream << "generateImagePart(): Skipping \"" - << filename << "\" as it's out-of-bounds " << pos_base - << " for [combine" << std::endl; - img->drop(); - continue; - } - - blit_with_alpha(img, baseimg, pos_base, dim); - img->drop(); - } - } - /* - [fill:WxH:color - [fill:WxH:X,Y:color - Creates a texture of the given size and color, optionally with an , - position. An alpha value may be specified in the `Colorstring`. - */ - else if (str_starts_with(part_of_name, "[fill")) - { - u32 x = 0; - u32 y = 0; - - Strfnd sf(part_of_name); - sf.next(":"); - u32 width = stoi(sf.next("x")); - u32 height = stoi(sf.next(":")); - std::string color_or_x = sf.next(","); - - video::SColor color; - if (!parseColorString(color_or_x, color, true)) { - x = stoi(color_or_x); - y = stoi(sf.next(":")); - std::string color_str = sf.next(":"); - - if (!parseColorString(color_str, color, false)) - return false; - } - core::dimension2d dim(width, height); - - CHECK_DIM(dim.Width, dim.Height); - if (baseimg) { - auto basedim = baseimg->getDimension(); - if (x >= basedim.Width || y >= basedim.Height) - COMPLAIN_INVALID("X or Y offset"); - } - - video::IImage *img = driver->createImage(video::ECF_A8R8G8B8, dim); - img->fill(color); - - if (baseimg == nullptr) { - baseimg = img; - } else { - blit_with_alpha(img, baseimg, v2s32(x, y), dim); - img->drop(); - } - } - /* - [brighten - */ - else if (str_starts_with(part_of_name, "[brighten")) - { - CHECK_BASEIMG(); - - brighten(baseimg); - } - /* - [noalpha - Make image completely opaque. - Used for the leaves texture when in old leaves mode, so - that the transparent parts don't look completely black - when simple alpha channel is used for rendering. - */ - else if (str_starts_with(part_of_name, "[noalpha")) - { - CHECK_BASEIMG(); - core::dimension2d dim = baseimg->getDimension(); - - // Set alpha to full - for (u32 y=0; ygetPixel(x,y); - c.setAlpha(255); - baseimg->setPixel(x,y,c); - } - } - /* - [makealpha:R,G,B - Convert one color to transparent. - */ - else if (str_starts_with(part_of_name, "[makealpha:")) - { - CHECK_BASEIMG(); - - Strfnd sf(part_of_name.substr(11)); - u32 r1 = stoi(sf.next(",")); - u32 g1 = stoi(sf.next(",")); - u32 b1 = stoi(sf.next("")); - - core::dimension2d dim = baseimg->getDimension(); - - for (u32 y=0; ygetPixel(x,y); - u32 r = c.getRed(); - u32 g = c.getGreen(); - u32 b = c.getBlue(); - if (!(r == r1 && g == g1 && b == b1)) - continue; - c.setAlpha(0); - baseimg->setPixel(x,y,c); - } - } - /* - [transformN - Rotates and/or flips the image. - - N can be a number (between 0 and 7) or a transform name. - Rotations are counter-clockwise. - 0 I identity - 1 R90 rotate by 90 degrees - 2 R180 rotate by 180 degrees - 3 R270 rotate by 270 degrees - 4 FX flip X - 5 FXR90 flip X then rotate by 90 degrees - 6 FY flip Y - 7 FYR90 flip Y then rotate by 90 degrees - - Note: Transform names can be concatenated to produce - their product (applies the first then the second). - The resulting transform will be equivalent to one of the - eight existing ones, though (see: dihedral group). - */ - else if (str_starts_with(part_of_name, "[transform")) - { - CHECK_BASEIMG(); - - u32 transform = parseImageTransform(part_of_name.substr(10)); - core::dimension2d dim = imageTransformDimension( - transform, baseimg->getDimension()); - video::IImage *image = driver->createImage( - baseimg->getColorFormat(), dim); - sanity_check(image != NULL); - imageTransform(transform, baseimg, image); - baseimg->drop(); - baseimg = image; - } - /* - [inventorycube{topimage{leftimage{rightimage - In every subimage, replace ^ with &. - Create an "inventory cube". - NOTE: This should be used only on its own. - Example (a grass block (not actually used in game): - "[inventorycube{grass.png{mud.png&grass_side.png{mud.png&grass_side.png" - */ - else if (str_starts_with(part_of_name, "[inventorycube")) - { - if (baseimg) { - errorstream<<"generateImagePart(): baseimg != NULL " - <<"for part_of_name=\""<drop(); - img_left->drop(); - img_right->drop(); - - return true; - } - /* - [lowpart:percent:filename - Adds the lower part of a texture - */ - else if (str_starts_with(part_of_name, "[lowpart:")) - { - Strfnd sf(part_of_name); - sf.next(":"); - u32 percent = stoi(sf.next(":"), 0, 100); - std::string filename = unescape_string(sf.next_esc(":", escape), escape); - - video::IImage *img = generateImage(filename, source_image_names); - if (img) { - core::dimension2d dim = img->getDimension(); - if (!baseimg) - baseimg = driver->createImage(video::ECF_A8R8G8B8, dim); - - core::position2d pos_base(0, 0); - core::position2d clippos(0, 0); - clippos.Y = dim.Height * (100-percent) / 100; - core::dimension2d clipdim = dim; - clipdim.Height = clipdim.Height * percent / 100 + 1; - core::rect cliprect(clippos, clipdim); - img->copyToWithAlpha(baseimg, pos_base, - core::rect(v2s32(0,0), dim), - video::SColor(255,255,255,255), - &cliprect); - img->drop(); - } - } - /* - [verticalframe:N:I - Crops a frame of a vertical animation. - N = frame count, I = frame index - */ - else if (str_starts_with(part_of_name, "[verticalframe:")) - { - CHECK_BASEIMG(); - - Strfnd sf(part_of_name); - sf.next(":"); - u32 frame_count = stoi(sf.next(":")); - u32 frame_index = stoi(sf.next(":")); - - if (frame_count == 0){ - errorstream << "generateImagePart(): invalid frame_count " - << "for part_of_name=\"" << part_of_name - << "\", using frame_count = 1 instead." << std::endl; - frame_count = 1; - } - if (frame_index >= frame_count) - frame_index = frame_count - 1; - - v2u32 frame_size = baseimg->getDimension(); - frame_size.Y /= frame_count; - - video::IImage *img = driver->createImage(video::ECF_A8R8G8B8, - frame_size); - - // Fill target image with transparency - img->fill(video::SColor(0,0,0,0)); - - core::dimension2d dim = frame_size; - core::position2d pos_dst(0, 0); - core::position2d pos_src(0, frame_index * frame_size.Y); - baseimg->copyToWithAlpha(img, pos_dst, - core::rect(pos_src, dim), - video::SColor(255,255,255,255), - NULL); - // Replace baseimg - baseimg->drop(); - baseimg = img; - } - /* - [mask:filename - Applies a mask to an image - */ - else if (str_starts_with(part_of_name, "[mask:")) - { - CHECK_BASEIMG(); - - Strfnd sf(part_of_name); - sf.next(":"); - std::string filename = unescape_string(sf.next_esc(":", escape), escape); - - video::IImage *img = generateImage(filename, source_image_names); - if (img) { - apply_mask(img, baseimg, v2s32(0, 0), v2s32(0, 0), - img->getDimension()); - img->drop(); - } else { - errorstream << "generateImagePart(): Failed to load image \"" - << filename << "\" for [mask" << std::endl; - } - } - /* - [multiply:color - or - [screen:color - Multiply and Screen blend modes are basic blend modes for darkening and lightening - images, respectively. - A Multiply blend multiplies a given color to every pixel of an image. - A Screen blend has the opposite effect to a Multiply blend. - color = color as ColorString - */ - else if (str_starts_with(part_of_name, "[multiply:") || - str_starts_with(part_of_name, "[screen:")) { - Strfnd sf(part_of_name); - sf.next(":"); - std::string color_str = sf.next(":"); - - CHECK_BASEIMG(); - - video::SColor color; - - if (!parseColorString(color_str, color, false)) - return false; - if (str_starts_with(part_of_name, "[multiply:")) { - apply_multiplication(baseimg, v2u32(0, 0), - baseimg->getDimension(), color); - } else { - apply_screen(baseimg, v2u32(0, 0), baseimg->getDimension(), color); - } - } - /* - [colorize:color:ratio - Overlays image with given color - color = color as ColorString - ratio = optional string "alpha", or a weighting between 0 and 255 - */ - else if (str_starts_with(part_of_name, "[colorize:")) - { - Strfnd sf(part_of_name); - sf.next(":"); - std::string color_str = sf.next(":"); - std::string ratio_str = sf.next(":"); - - CHECK_BASEIMG(); - - video::SColor color; - int ratio = -1; - bool keep_alpha = false; - - if (!parseColorString(color_str, color, false)) - return false; - - if (is_number(ratio_str)) - ratio = mystoi(ratio_str, 0, 255); - else if (ratio_str == "alpha") - keep_alpha = true; - - apply_colorize(baseimg, v2u32(0, 0), baseimg->getDimension(), color, ratio, keep_alpha); - } - /* - [applyfiltersformesh - Internal modifier - */ - else if (str_starts_with(part_of_name, "[applyfiltersformesh")) - { - /* IMPORTANT: When changing this, getTextureForMesh() needs to be - * updated too. */ - - CHECK_BASEIMG(); - - // Apply the "clean transparent" filter, if needed - if (m_setting_mipmap || m_setting_bilinear_filter || - m_setting_trilinear_filter || m_setting_anisotropic_filter) - imageCleanTransparent(baseimg, 127); - - /* Upscale textures to user's requested minimum size. This is a trick to make - * filters look as good on low-res textures as on high-res ones, by making - * low-res textures BECOME high-res ones. This is helpful for worlds that - * mix high- and low-res textures, or for mods with least-common-denominator - * textures that don't have the resources to offer high-res alternatives. - */ - const bool filter = m_setting_trilinear_filter || m_setting_bilinear_filter; - const s32 scaleto = filter ? g_settings->getU16("texture_min_size") : 1; - if (scaleto > 1) { - const core::dimension2d dim = baseimg->getDimension(); - - /* Calculate scaling needed to make the shortest texture dimension - * equal to the target minimum. If e.g. this is a vertical frames - * animation, the short dimension will be the real size. - */ - u32 xscale = scaleto / dim.Width; - u32 yscale = scaleto / dim.Height; - const s32 scale = std::max(xscale, yscale); - - // Never downscale; only scale up by 2x or more. - if (scale > 1) { - u32 w = scale * dim.Width; - u32 h = scale * dim.Height; - const core::dimension2d newdim(w, h); - video::IImage *newimg = driver->createImage( - baseimg->getColorFormat(), newdim); - baseimg->copyToScaling(newimg); - baseimg->drop(); - baseimg = newimg; - } - } - } - /* - [resize:WxH - Resizes the base image to the given dimensions - */ - else if (str_starts_with(part_of_name, "[resize")) - { - CHECK_BASEIMG(); - - Strfnd sf(part_of_name); - sf.next(":"); - u32 width = stoi(sf.next("x")); - u32 height = stoi(sf.next("")); - CHECK_DIM(width, height); - - video::IImage *image = driver-> - createImage(video::ECF_A8R8G8B8, {width, height}); - baseimg->copyToScaling(image); - baseimg->drop(); - baseimg = image; - } - /* - [opacity:R - Makes the base image transparent according to the given ratio. - R must be between 0 and 255. - 0 means totally transparent. - 255 means totally opaque. - */ - else if (str_starts_with(part_of_name, "[opacity:")) { - CHECK_BASEIMG(); - - Strfnd sf(part_of_name); - sf.next(":"); - - u32 ratio = mystoi(sf.next(""), 0, 255); - - core::dimension2d dim = baseimg->getDimension(); - - for (u32 y = 0; y < dim.Height; y++) - for (u32 x = 0; x < dim.Width; x++) - { - video::SColor c = baseimg->getPixel(x, y); - c.setAlpha(floor((c.getAlpha() * ratio) / 255 + 0.5)); - baseimg->setPixel(x, y, c); - } - } - /* - [invert:mode - Inverts the given channels of the base image. - Mode may contain the characters "r", "g", "b", "a". - Only the channels that are mentioned in the mode string - will be inverted. - */ - else if (str_starts_with(part_of_name, "[invert:")) { - CHECK_BASEIMG(); - - Strfnd sf(part_of_name); - sf.next(":"); - - std::string mode = sf.next(""); - u32 mask = 0; - if (mode.find('a') != std::string::npos) - mask |= 0xff000000UL; - if (mode.find('r') != std::string::npos) - mask |= 0x00ff0000UL; - if (mode.find('g') != std::string::npos) - mask |= 0x0000ff00UL; - if (mode.find('b') != std::string::npos) - mask |= 0x000000ffUL; - - core::dimension2d dim = baseimg->getDimension(); - - for (u32 y = 0; y < dim.Height; y++) - for (u32 x = 0; x < dim.Width; x++) - { - video::SColor c = baseimg->getPixel(x, y); - c.color ^= mask; - baseimg->setPixel(x, y, c); - } - } - /* - [sheet:WxH:X,Y - Retrieves a tile at position X,Y (in tiles) - from the base image it assumes to be a - tilesheet with dimensions W,H (in tiles). - */ - else if (str_starts_with(part_of_name, "[sheet:")) { - CHECK_BASEIMG(); - - Strfnd sf(part_of_name); - sf.next(":"); - u32 w0 = stoi(sf.next("x")); - u32 h0 = stoi(sf.next(":")); - u32 x0 = stoi(sf.next(",")); - u32 y0 = stoi(sf.next(":")); - - CHECK_DIM(w0, h0); - if (x0 >= w0 || y0 >= h0) - COMPLAIN_INVALID("tile position (X,Y)"); - - core::dimension2d img_dim = baseimg->getDimension(); - core::dimension2d tile_dim(v2u32(img_dim) / v2u32(w0, h0)); - if (tile_dim.Width == 0) - tile_dim.Width = 1; - if (tile_dim.Height == 0) - tile_dim.Height = 1; - - video::IImage *img = driver->createImage( - video::ECF_A8R8G8B8, tile_dim); - img->fill(video::SColor(0,0,0,0)); - - v2u32 vdim(tile_dim); - core::rect rect(v2s32(x0 * vdim.X, y0 * vdim.Y), tile_dim); - baseimg->copyToWithAlpha(img, v2s32(0), rect, - video::SColor(255,255,255,255), NULL); - - // Replace baseimg - baseimg->drop(); - baseimg = img; - } - /* - [png:base64 - Decodes a PNG image in base64 form. - Use minetest.encode_png and minetest.encode_base64 - to produce a valid string. - */ - else if (str_starts_with(part_of_name, "[png:")) { - std::string png; - { - auto blob = part_of_name.substr(5); - if (!base64_is_valid(blob)) { - errorstream << "generateImagePart(): " - << "malformed base64 in [png" << std::endl; - return false; - } - png = base64_decode(blob); - } - - auto *device = RenderingEngine::get_raw_device(); - auto *fs = device->getFileSystem(); - auto *vd = device->getVideoDriver(); - auto *memfile = fs->createMemoryReadFile(png.data(), png.size(), "__temp_png"); - video::IImage* pngimg = vd->createImageFromFile(memfile); - memfile->drop(); - - if (!pngimg) { - errorstream << "generateImagePart(): Invalid PNG data" << std::endl; - return false; - } - - if (baseimg) { - blitBaseImage(pngimg, baseimg); - } else { - core::dimension2d dim = pngimg->getDimension(); - baseimg = driver->createImage(video::ECF_A8R8G8B8, dim); - pngimg->copyTo(baseimg); - } - pngimg->drop(); - } - /* - [hsl:hue:saturation:lightness - or - [colorizehsl:hue:saturation:lightness - - Adjust the hue, saturation, and lightness of the base image. Like - "Hue-Saturation" in GIMP, but with 0 as the mid-point. - Hue should be from -180 to +180, though 0 to 360 is also supported. - Saturation and lightness are optional, with lightness from -100 to - +100, and sauration from -100 to +100-or-higher. - - If colorize is true then saturation is from 0 to 100, and the image - will be converted to a grayscale image as though seen through a - colored glass, like "Colorize" in GIMP. - */ - else if (str_starts_with(part_of_name, "[hsl:") || - str_starts_with(part_of_name, "[colorizehsl:")) { - - CHECK_BASEIMG(); - - bool colorize = str_starts_with(part_of_name, "[colorizehsl:"); - - // saturation range is 0 to 100 when colorize is true - s32 defaultSaturation = colorize ? 50 : 0; - - Strfnd sf(part_of_name); - sf.next(":"); - s32 hue = mystoi(sf.next(":"), -180, 360); - s32 saturation = sf.at_end() ? defaultSaturation : mystoi(sf.next(":"), -100, 1000); - s32 lightness = sf.at_end() ? 0 : mystoi(sf.next(":"), -100, 100); - - - apply_hue_saturation(baseimg, v2u32(0, 0), baseimg->getDimension(), - hue, saturation, lightness, colorize); - } - /* - [overlay:filename - or - [hardlight:filename - - "A.png^[hardlight:B.png" is the same as "B.png^[overlay:A.Png" - - Applies an Overlay or Hard Light blend between two images, like the - layer modes of the same names in GIMP. - Overlay combines Multiply and Screen blend modes. The parts of the - top layer where the base layer is light become lighter, the parts - where the base layer is dark become darker. Areas where the base - layer are mid grey are unaffected. An overlay with the same picture - looks like an S-curve. - - Swapping the top layer and base layer is a Hard Light blend - */ - else if (str_starts_with(part_of_name, "[overlay:") || - str_starts_with(part_of_name, "[hardlight:")) { - - CHECK_BASEIMG(); - - Strfnd sf(part_of_name); - sf.next(":"); - std::string filename = unescape_string(sf.next_esc(":", escape), escape); - - video::IImage *img = generateImage(filename, source_image_names); - if (img) { - upscaleImagesToMatchLargest(baseimg, img); - - bool hardlight = str_starts_with(part_of_name, "[hardlight:"); - apply_overlay(img, baseimg, v2s32(0, 0), v2s32(0, 0), - img->getDimension(), hardlight); - img->drop(); - } else { - errorstream << "generateImage(): Failed to load image \"" - << filename << "\" for [overlay or [hardlight" << std::endl; - } - } - /* - [contrast:C:B - - Adjust the brightness and contrast of the base image. Conceptually - like GIMP's "Brightness-Contrast" feature but allows brightness to - be wound all the way up to white or down to black. - C and B are both values from -127 to +127. - B is optional. - */ - else if (str_starts_with(part_of_name, "[contrast:")) { - - CHECK_BASEIMG(); - - Strfnd sf(part_of_name); - sf.next(":"); - s32 contrast = mystoi(sf.next(":"), -127, 127); - s32 brightness = sf.at_end() ? 0 : mystoi(sf.next(":"), -127, 127); - - apply_brightness_contrast(baseimg, v2u32(0, 0), - baseimg->getDimension(), brightness, contrast); - } - else - { - errorstream << "generateImagePart(): Invalid " - " modification: \"" << part_of_name << "\"" << std::endl; - } - } - - return true; -} - -#undef CHECK_BASEIMG - -#undef COMPLAIN_INVALID - -#undef CHECK_DIM - - -namespace { - -/** Calculate the color of a single pixel drawn on top of another pixel without - * gamma correction - * - * The color mixing is a little more complicated than just - * video::SColor::getInterpolated because getInterpolated does not handle alpha - * correctly. - * For example, a pixel with alpha=64 drawn atop a pixel with alpha=128 should - * yield a pixel with alpha=160, while getInterpolated would yield alpha=96. - * - * \tparam overlay If enabled, only modify dst_col if it is fully opaque - * \param src_col Color of the top pixel - * \param dst_col Color of the bottom pixel. This color is modified in-place to - * store the result. -*/ -template -void blit_pixel(video::SColor src_col, video::SColor &dst_col) -{ - u8 dst_a = (u8)dst_col.getAlpha(); - if constexpr (overlay) { - if (dst_a != 255) - // The bottom pixel has transparency -> do nothing - return; - } - u8 src_a = (u8)src_col.getAlpha(); - if (src_a == 0) { - // A fully transparent pixel is on top -> do nothing - return; - } - if (src_a == 255 || dst_a == 0) { - // The top pixel is fully opaque or the bottom pixel is - // fully transparent -> replace the color - dst_col = src_col; - return; - } - struct Color { u8 r, g, b; }; - Color src{(u8)src_col.getRed(), (u8)src_col.getGreen(), - (u8)src_col.getBlue()}; - Color dst{(u8)dst_col.getRed(), (u8)dst_col.getGreen(), - (u8)dst_col.getBlue()}; - if (dst_a == 255) { - // A semi-transparent pixel is on top and an opaque one in - // the bottom -> lerp r, g, and b - dst.r = (dst.r * (255 - src_a) + src.r * src_a) / 255; - dst.g = (dst.g * (255 - src_a) + src.g * src_a) / 255; - dst.b = (dst.b * (255 - src_a) + src.b * src_a) / 255; - dst_col.set(255, dst.r, dst.g, dst.b); - return; - } - // A semi-transparent pixel is on top of a - // semi-transparent pixel -> general alpha compositing - u16 a_new_255 = src_a * 255 + (255 - src_a) * dst_a; - dst.r = (dst.r * (255 - src_a) * dst_a + src.r * src_a * 255) / a_new_255; - dst.g = (dst.g * (255 - src_a) * dst_a + src.g * src_a * 255) / a_new_255; - dst.b = (dst.b * (255 - src_a) * dst_a + src.b * src_a * 255) / a_new_255; - dst_a = (a_new_255 + 127) / 255; - dst_col.set(dst_a, dst.r, dst.g, dst.b); -} - -} // namespace - -template -void blit_with_alpha(video::IImage *src, video::IImage *dst, v2s32 dst_pos, - v2u32 size) -{ - if (dst->getColorFormat() != video::ECF_A8R8G8B8) - throw BaseException("blit_with_alpha() supports only ECF_A8R8G8B8 " - "destination images."); - - core::dimension2d src_dim = src->getDimension(); - core::dimension2d dst_dim = dst->getDimension(); - bool drop_src = false; - if (src->getColorFormat() != video::ECF_A8R8G8B8) { - video::IVideoDriver *driver = RenderingEngine::get_video_driver(); - video::IImage *src_converted = driver->createImage(video::ECF_A8R8G8B8, - src_dim); - if (!src_converted) - throw BaseException("blit_with_alpha() failed to convert the " - "source image to ECF_A8R8G8B8."); - src->copyTo(src_converted); - src = src_converted; - drop_src = true; - } - video::SColor *pixels_src = - reinterpret_cast(src->getData()); - video::SColor *pixels_dst = - reinterpret_cast(dst->getData()); - // Limit y and x to the overlapping ranges - // s.t. the positions are all in bounds after offsetting. - u32 x_start = (u32)std::max(0, -dst_pos.X); - u32 y_start = (u32)std::max(0, -dst_pos.Y); - u32 x_end = (u32)std::min({size.X, src_dim.Width, - dst_dim.Width - (s64)dst_pos.X}); - u32 y_end = (u32)std::min({size.Y, src_dim.Height, - dst_dim.Height - (s64)dst_pos.Y}); - for (u32 y0 = y_start; y0 < y_end; ++y0) { - size_t i_src = y0 * src_dim.Width + x_start; - size_t i_dst = (dst_pos.Y + y0) * dst_dim.Width + dst_pos.X + x_start; - for (u32 x0 = x_start; x0 < x_end; ++x0) { - blit_pixel(pixels_src[i_src++], pixels_dst[i_dst++]); - } - } - if (drop_src) - src->drop(); -} - -/* - Apply color to destination, using a weighted interpolation blend -*/ -static void apply_colorize(video::IImage *dst, v2u32 dst_pos, v2u32 size, - const video::SColor color, int ratio, bool keep_alpha) -{ - u32 alpha = color.getAlpha(); - video::SColor dst_c; - if ((ratio == -1 && alpha == 255) || ratio == 255) { // full replacement of color - if (keep_alpha) { // replace the color with alpha = dest alpha * color alpha - dst_c = color; - for (u32 y = dst_pos.Y; y < dst_pos.Y + size.Y; y++) - for (u32 x = dst_pos.X; x < dst_pos.X + size.X; x++) { - u32 dst_alpha = dst->getPixel(x, y).getAlpha(); - if (dst_alpha > 0) { - dst_c.setAlpha(dst_alpha * alpha / 255); - dst->setPixel(x, y, dst_c); - } - } - } else { // replace the color including the alpha - for (u32 y = dst_pos.Y; y < dst_pos.Y + size.Y; y++) - for (u32 x = dst_pos.X; x < dst_pos.X + size.X; x++) - if (dst->getPixel(x, y).getAlpha() > 0) - dst->setPixel(x, y, color); - } - } else { // interpolate between the color and destination - float interp = (ratio == -1 ? color.getAlpha() / 255.0f : ratio / 255.0f); - for (u32 y = dst_pos.Y; y < dst_pos.Y + size.Y; y++) - for (u32 x = dst_pos.X; x < dst_pos.X + size.X; x++) { - dst_c = dst->getPixel(x, y); - if (dst_c.getAlpha() > 0) { - dst_c = color.getInterpolated(dst_c, interp); - dst->setPixel(x, y, dst_c); - } - } - } -} - -/* - Apply color to destination, using a Multiply blend mode -*/ -static void apply_multiplication(video::IImage *dst, v2u32 dst_pos, v2u32 size, - const video::SColor color) -{ - video::SColor dst_c; - - for (u32 y = dst_pos.Y; y < dst_pos.Y + size.Y; y++) - for (u32 x = dst_pos.X; x < dst_pos.X + size.X; x++) { - dst_c = dst->getPixel(x, y); - dst_c.set( - dst_c.getAlpha(), - (dst_c.getRed() * color.getRed()) / 255, - (dst_c.getGreen() * color.getGreen()) / 255, - (dst_c.getBlue() * color.getBlue()) / 255 - ); - dst->setPixel(x, y, dst_c); - } -} - -/* - Apply color to destination, using a Screen blend mode -*/ -static void apply_screen(video::IImage *dst, v2u32 dst_pos, v2u32 size, - const video::SColor color) -{ - video::SColor dst_c; - - for (u32 y = dst_pos.Y; y < dst_pos.Y + size.Y; y++) - for (u32 x = dst_pos.X; x < dst_pos.X + size.X; x++) { - dst_c = dst->getPixel(x, y); - dst_c.set( - dst_c.getAlpha(), - 255 - ((255 - dst_c.getRed()) * (255 - color.getRed())) / 255, - 255 - ((255 - dst_c.getGreen()) * (255 - color.getGreen())) / 255, - 255 - ((255 - dst_c.getBlue()) * (255 - color.getBlue())) / 255 - ); - dst->setPixel(x, y, dst_c); - } -} - -/* - Adjust the hue, saturation, and lightness of destination. Like - "Hue-Saturation" in GIMP, but with 0 as the mid-point. - Hue should be from -180 to +180, or from 0 to 360. - Saturation and Lightness are percentages. - Lightness is from -100 to +100. - Saturation goes down to -100 (fully desaturated) but can go above 100, - allowing for even muted colors to become saturated. - - If colorize is true then saturation is from 0 to 100, and destination will - be converted to a grayscale image as seen through a colored glass, like - "Colorize" in GIMP. -*/ -static void apply_hue_saturation(video::IImage *dst, v2u32 dst_pos, v2u32 size, - s32 hue, s32 saturation, s32 lightness, bool colorize) -{ - video::SColorf colorf; - video::SColorHSL hsl; - f32 norm_s = core::clamp(saturation, -100, 1000) / 100.0f; - f32 norm_l = core::clamp(lightness, -100, 100) / 100.0f; - - if (colorize) { - hsl.Saturation = core::clamp((f32)saturation, 0.0f, 100.0f); - } - - for (u32 y = dst_pos.Y; y < dst_pos.Y + size.Y; y++) - for (u32 x = dst_pos.X; x < dst_pos.X + size.X; x++) { - - if (colorize) { - f32 lum = dst->getPixel(x, y).getLuminance() / 255.0f; - - if (norm_l < 0) { - lum *= norm_l + 1.0f; - } else { - lum = lum * (1.0f - norm_l) + norm_l; - } - hsl.Hue = 0; - hsl.Luminance = lum * 100; - - } else { - // convert the RGB to HSL - colorf = video::SColorf(dst->getPixel(x, y)); - hsl.fromRGB(colorf); - - if (norm_l < 0) { - hsl.Luminance *= norm_l + 1.0f; - } else{ - hsl.Luminance = hsl.Luminance + norm_l * (100.0f - hsl.Luminance); - } - - // Adjusting saturation in the same manner as lightness resulted in - // muted colors being affected too much and bright colors not - // affected enough, so I'm borrowing a leaf out of gimp's book and - // using a different scaling approach for saturation. - // https://github.com/GNOME/gimp/blob/6cc1e035f1822bf5198e7e99a53f7fa6e281396a/app/operations/gimpoperationhuesaturation.c#L139-L145= - // This difference is why values over 100% are not necessary for - // lightness but are very useful with saturation. An alternative UI - // approach would be to have an upper saturation limit of 100, but - // multiply positive values by ~3 to make it a more useful positive - // range scale. - hsl.Saturation *= norm_s + 1.0f; - hsl.Saturation = core::clamp(hsl.Saturation, 0.0f, 100.0f); - } - - // Apply the specified HSL adjustments - hsl.Hue = fmod(hsl.Hue + hue, 360); - if (hsl.Hue < 0) - hsl.Hue += 360; - - // Convert back to RGB - hsl.toRGB(colorf); - dst->setPixel(x, y, colorf.toSColor()); - } -} - - -/* - Apply an Overlay blend to destination - If hardlight is true then swap the dst & blend images (a hardlight blend) -*/ -static void apply_overlay(video::IImage *blend, video::IImage *dst, - v2s32 blend_pos, v2s32 dst_pos, v2u32 size, bool hardlight) -{ - video::IImage *blend_layer = hardlight ? dst : blend; - video::IImage *base_layer = hardlight ? blend : dst; - v2s32 blend_layer_pos = hardlight ? dst_pos : blend_pos; - v2s32 base_layer_pos = hardlight ? blend_pos : dst_pos; - - for (u32 y = 0; y < size.Y; y++) - for (u32 x = 0; x < size.X; x++) { - s32 base_x = x + base_layer_pos.X; - s32 base_y = y + base_layer_pos.Y; - - video::SColor blend_c = - blend_layer->getPixel(x + blend_layer_pos.X, y + blend_layer_pos.Y); - video::SColor base_c = base_layer->getPixel(base_x, base_y); - double blend_r = blend_c.getRed() / 255.0; - double blend_g = blend_c.getGreen() / 255.0; - double blend_b = blend_c.getBlue() / 255.0; - double base_r = base_c.getRed() / 255.0; - double base_g = base_c.getGreen() / 255.0; - double base_b = base_c.getBlue() / 255.0; - - base_c.set( - base_c.getAlpha(), - // Do a Multiply blend if less that 0.5, otherwise do a Screen blend - (u32)((base_r < 0.5 ? 2 * base_r * blend_r : 1 - 2 * (1 - base_r) * (1 - blend_r)) * 255), - (u32)((base_g < 0.5 ? 2 * base_g * blend_g : 1 - 2 * (1 - base_g) * (1 - blend_g)) * 255), - (u32)((base_b < 0.5 ? 2 * base_b * blend_b : 1 - 2 * (1 - base_b) * (1 - blend_b)) * 255) - ); - dst->setPixel(base_x, base_y, base_c); - } -} - -/* - Adjust the brightness and contrast of the base image. - - Conceptually like GIMP's "Brightness-Contrast" feature but allows brightness to be - wound all the way up to white or down to black. -*/ -static void apply_brightness_contrast(video::IImage *dst, v2u32 dst_pos, v2u32 size, - s32 brightness, s32 contrast) -{ - video::SColor dst_c; - // Only allow normalized contrast to get as high as 127/128 to avoid infinite slope. - // (we could technically allow -128/128 here as that would just result in 0 slope) - double norm_c = core::clamp(contrast, -127, 127) / 128.0; - double norm_b = core::clamp(brightness, -127, 127) / 127.0; - - // Scale brightness so its range is -127.5 to 127.5, otherwise brightness - // adjustments will outputs values from 0.5 to 254.5 instead of 0 to 255. - double scaled_b = brightness * 127.5 / 127; - - // Calculate a contrast slope such that that no colors will get clamped due - // to the brightness setting. - // This allows the texture modifier to used as a brightness modifier without - // the user having to calculate a contrast to avoid clipping at that brightness. - double slope = 1 - fabs(norm_b); - - // Apply the user's contrast adjustment to the calculated slope, such that - // -127 will make it near-vertical and +127 will make it horizontal - double angle = atan(slope); - angle += norm_c <= 0 - ? norm_c * angle // allow contrast slope to be lowered to 0 - : norm_c * (M_PI_2 - angle); // allow contrast slope to be raised almost vert. - slope = tan(angle); - - double c = slope <= 1 - ? -slope * 127.5 + 127.5 + scaled_b // shift up/down when slope is horiz. - : -slope * (127.5 - scaled_b) + 127.5; // shift left/right when slope is vert. - - // add 0.5 to c so that when the final result is cast to int, it is effectively - // rounded rather than trunc'd. - c += 0.5; - - for (u32 y = dst_pos.Y; y < dst_pos.Y + size.Y; y++) - for (u32 x = dst_pos.X; x < dst_pos.X + size.X; x++) { - dst_c = dst->getPixel(x, y); - - dst_c.set( - dst_c.getAlpha(), - core::clamp((int)(slope * dst_c.getRed() + c), 0, 255), - core::clamp((int)(slope * dst_c.getGreen() + c), 0, 255), - core::clamp((int)(slope * dst_c.getBlue() + c), 0, 255) - ); - dst->setPixel(x, y, dst_c); - } -} - -/* - Apply mask to destination -*/ -static void apply_mask(video::IImage *mask, video::IImage *dst, - v2s32 mask_pos, v2s32 dst_pos, v2u32 size) -{ - for (u32 y0 = 0; y0 < size.Y; y0++) { - for (u32 x0 = 0; x0 < size.X; x0++) { - s32 mask_x = x0 + mask_pos.X; - s32 mask_y = y0 + mask_pos.Y; - s32 dst_x = x0 + dst_pos.X; - s32 dst_y = y0 + dst_pos.Y; - video::SColor mask_c = mask->getPixel(mask_x, mask_y); - video::SColor dst_c = dst->getPixel(dst_x, dst_y); - dst_c.color &= mask_c.color; - dst->setPixel(dst_x, dst_y, dst_c); - } - } -} - -video::IImage *create_crack_image(video::IImage *crack, s32 frame_index, - core::dimension2d size, u8 tiles, video::IVideoDriver *driver) -{ - core::dimension2d strip_size = crack->getDimension(); - - if (tiles == 0 || strip_size.getArea() == 0) - return nullptr; - - core::dimension2d frame_size(strip_size.Width, strip_size.Width); - core::dimension2d tile_size(size / tiles); - s32 frame_count = strip_size.Height / strip_size.Width; - if (frame_index >= frame_count) - frame_index = frame_count - 1; - core::rect frame(v2s32(0, frame_index * frame_size.Height), frame_size); - video::IImage *result = nullptr; - - // extract crack frame - video::IImage *crack_tile = driver->createImage(video::ECF_A8R8G8B8, tile_size); - if (!crack_tile) - return nullptr; - if (tile_size == frame_size) { - crack->copyTo(crack_tile, v2s32(0, 0), frame); - } else { - video::IImage *crack_frame = driver->createImage(video::ECF_A8R8G8B8, frame_size); - if (!crack_frame) - goto exit__has_tile; - crack->copyTo(crack_frame, v2s32(0, 0), frame); - crack_frame->copyToScaling(crack_tile); - crack_frame->drop(); - } - if (tiles == 1) - return crack_tile; - - // tile it - result = driver->createImage(video::ECF_A8R8G8B8, size); - if (!result) - goto exit__has_tile; - result->fill({}); - for (u8 i = 0; i < tiles; i++) - for (u8 j = 0; j < tiles; j++) - crack_tile->copyTo(result, v2s32(i * tile_size.Width, j * tile_size.Height)); - -exit__has_tile: - crack_tile->drop(); - return result; -} - -static void draw_crack(video::IImage *crack, video::IImage *dst, - bool use_overlay, s32 frame_count, s32 progression, - video::IVideoDriver *driver, u8 tiles) -{ - // Dimension of destination image - core::dimension2d dim_dst = dst->getDimension(); - // Limit frame_count - if (frame_count > (s32) dim_dst.Height) - frame_count = dim_dst.Height; - if (frame_count < 1) - frame_count = 1; - // Dimension of the scaled crack stage, - // which is the same as the dimension of a single destination frame - core::dimension2d frame_size( - dim_dst.Width, - dim_dst.Height / frame_count - ); - video::IImage *crack_scaled = create_crack_image(crack, progression, - frame_size, tiles, driver); - if (!crack_scaled) - return; - - auto blit = use_overlay ? blit_with_alpha : blit_with_alpha; - for (s32 i = 0; i < frame_count; ++i) { - v2s32 dst_pos(0, frame_size.Height * i); - blit(crack_scaled, dst, dst_pos, frame_size); - } - - crack_scaled->drop(); -} - -void brighten(video::IImage *image) -{ - if (image == NULL) - return; - - core::dimension2d dim = image->getDimension(); - - for (u32 y=0; ygetPixel(x,y); - c.setRed(0.5 * 255 + 0.5 * (float)c.getRed()); - c.setGreen(0.5 * 255 + 0.5 * (float)c.getGreen()); - c.setBlue(0.5 * 255 + 0.5 * (float)c.getBlue()); - image->setPixel(x,y,c); - } -} - -u32 parseImageTransform(std::string_view s) -{ - int total_transform = 0; - - std::string transform_names[8]; - transform_names[0] = "i"; - transform_names[1] = "r90"; - transform_names[2] = "r180"; - transform_names[3] = "r270"; - transform_names[4] = "fx"; - transform_names[6] = "fy"; - - std::size_t pos = 0; - while(pos < s.size()) - { - int transform = -1; - for (int i = 0; i <= 7; ++i) - { - const std::string &name_i = transform_names[i]; - - if (s[pos] == ('0' + i)) - { - transform = i; - pos++; - break; - } - - if (!(name_i.empty()) && lowercase(s.substr(pos, name_i.size())) == name_i) { - transform = i; - pos += name_i.size(); - break; - } - } - if (transform < 0) - break; - - // Multiply total_transform and transform in the group D4 - int new_total = 0; - if (transform < 4) - new_total = (transform + total_transform) % 4; - else - new_total = (transform - total_transform + 8) % 4; - if ((transform >= 4) ^ (total_transform >= 4)) - new_total += 4; - - total_transform = new_total; - } - return total_transform; -} - -core::dimension2d imageTransformDimension(u32 transform, core::dimension2d dim) -{ - if (transform % 2 == 0) - return dim; - - return core::dimension2d(dim.Height, dim.Width); -} - -void imageTransform(u32 transform, video::IImage *src, video::IImage *dst) -{ - if (src == NULL || dst == NULL) - return; - - core::dimension2d dstdim = dst->getDimension(); - - // Pre-conditions - assert(dstdim == imageTransformDimension(transform, src->getDimension())); - assert(transform <= 7); - - /* - Compute the transformation from source coordinates (sx,sy) - to destination coordinates (dx,dy). - */ - int sxn = 0; - int syn = 2; - if (transform == 0) // identity - sxn = 0, syn = 2; // sx = dx, sy = dy - else if (transform == 1) // rotate by 90 degrees ccw - sxn = 3, syn = 0; // sx = (H-1) - dy, sy = dx - else if (transform == 2) // rotate by 180 degrees - sxn = 1, syn = 3; // sx = (W-1) - dx, sy = (H-1) - dy - else if (transform == 3) // rotate by 270 degrees ccw - sxn = 2, syn = 1; // sx = dy, sy = (W-1) - dx - else if (transform == 4) // flip x - sxn = 1, syn = 2; // sx = (W-1) - dx, sy = dy - else if (transform == 5) // flip x then rotate by 90 degrees ccw - sxn = 2, syn = 0; // sx = dy, sy = dx - else if (transform == 6) // flip y - sxn = 0, syn = 3; // sx = dx, sy = (H-1) - dy - else if (transform == 7) // flip y then rotate by 90 degrees ccw - sxn = 3, syn = 1; // sx = (H-1) - dy, sy = (W-1) - dx - - for (u32 dy=0; dygetPixel(sx,sy); - dst->setPixel(dx,dy,c); - } -} - video::ITexture* TextureSource::getNormalTexture(const std::string &name) { if (isKnownSourceImage("override_normal.png")) @@ -2455,48 +572,6 @@ video::ITexture* TextureSource::getNormalTexture(const std::string &name) return NULL; } -namespace { - // For more colorspace transformations, see for example - // https://github.com/tobspr/GLSL-Color-Spaces/blob/master/ColorSpaces.inc.glsl - - inline float linear_to_srgb_component(float v) - { - if (v > 0.0031308f) - return 1.055f * powf(v, 1.0f / 2.4f) - 0.055f; - return 12.92f * v; - } - inline float srgb_to_linear_component(float v) - { - if (v > 0.04045f) - return powf((v + 0.055f) / 1.055f, 2.4f); - return v / 12.92f; - } - - v3f srgb_to_linear(const video::SColor col_srgb) - { - v3f col(col_srgb.getRed(), col_srgb.getGreen(), col_srgb.getBlue()); - col /= 255.0f; - col.X = srgb_to_linear_component(col.X); - col.Y = srgb_to_linear_component(col.Y); - col.Z = srgb_to_linear_component(col.Z); - return col; - } - - video::SColor linear_to_srgb(const v3f col_linear) - { - v3f col; - col.X = linear_to_srgb_component(col_linear.X); - col.Y = linear_to_srgb_component(col_linear.Y); - col.Z = linear_to_srgb_component(col_linear.Z); - col *= 255.0f; - col.X = core::clamp(col.X, 0.0f, 255.0f); - col.Y = core::clamp(col.Y, 0.0f, 255.0f); - col.Z = core::clamp(col.Z, 0.0f, 255.0f); - return video::SColor(0xff, myround(col.X), myround(col.Y), - myround(col.Z)); - } -} - video::SColor TextureSource::getTextureAverageColor(const std::string &name) { video::IVideoDriver *driver = RenderingEngine::get_video_driver(); @@ -2510,27 +585,9 @@ video::SColor TextureSource::getTextureAverageColor(const std::string &name) if (!image) return c; - u32 total = 0; - v3f col_acc(0, 0, 0); - core::dimension2d dim = image->getDimension(); - u16 step = 1; - if (dim.Width > 16) - step = dim.Width / 16; - for (u16 x = 0; x < dim.Width; x += step) { - for (u16 y = 0; y < dim.Width; y += step) { - c = image->getPixel(x,y); - if (c.getAlpha() > 0) { - total++; - col_acc += srgb_to_linear(c); - } - } - } + c = ImageSource::getImageAverageColor(*image); image->drop(); - if (total > 0) { - col_acc /= total; - c = linear_to_srgb(col_acc); - } - c.setAlpha(255); + return c; } From 673d2499e8643474576ae7937bebd6391d0ac63a Mon Sep 17 00:00:00 2001 From: cx384 Date: Sun, 17 Mar 2024 15:04:40 +0100 Subject: [PATCH 04/34] Refactor texturepaths.cpp and SourceImageCache --- src/client/imagesource.cpp | 15 ++-- src/client/imagesource.h | 24 ++--- src/client/texturesource.cpp | 169 +++++++++++++---------------------- 3 files changed, 80 insertions(+), 128 deletions(-) diff --git a/src/client/imagesource.cpp b/src/client/imagesource.cpp index 92da3ee27..d401e98ed 100644 --- a/src/client/imagesource.cpp +++ b/src/client/imagesource.cpp @@ -75,13 +75,14 @@ void SourceImageCache::insert(const std::string &name, video::IImage *img, bool toadd->grab(); m_images[name] = toadd; } + video::IImage* SourceImageCache::get(const std::string &name) { std::map::iterator n; n = m_images.find(name); if (n != m_images.end()) return n->second; - return NULL; + return nullptr; } // Primarily fetches from cache, secondarily tries to read from filesystem @@ -96,12 +97,12 @@ video::IImage* SourceImageCache::getOrLoad(const std::string &name) video::IVideoDriver *driver = RenderingEngine::get_video_driver(); std::string path = getTexturePath(name); if (path.empty()) { - infostream<<"SourceImageCache::getOrLoad(): No path found for \"" - <createImageFromFile(path.c_str()); if (img){ @@ -1970,6 +1971,6 @@ video::SColor ImageSource::getImageAverageColor(const video::IImage &image) return c; } -void ImageSource::insertImage(const std::string &name, video::IImage *img, bool prefer_local) { +void ImageSource::insertSourceImage(const std::string &name, video::IImage *img, bool prefer_local) { m_sourcecache.insert(name, img, prefer_local); } diff --git a/src/client/imagesource.h b/src/client/imagesource.h index 15c982f8a..f3a30617a 100644 --- a/src/client/imagesource.h +++ b/src/client/imagesource.h @@ -23,10 +23,12 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include "settings.h" -// This file is only for internal generation/modification of images. -// Use texturesource.h instead to handle textures. +// This file is only used for internal generation of images. +// Use texturesource.h to handle textures. // A cache used for storing source images. +// (A "source image" is an unmodified image directly taken from the filesystem.) +// Does not contain modified images. class SourceImageCache { public: ~SourceImageCache(); @@ -35,16 +37,13 @@ public: video::IImage* get(const std::string &name); - // Primarily fetches from cache, secondarily tries to read from filesystem + // Primarily fetches from cache, secondarily tries to read from filesystem. video::IImage *getOrLoad(const std::string &name); private: std::map m_images; }; -/* - * Generates and caches images. - * The image name defines the image by filename and texture modifiers. -*/ +// Generates images using texture modifiers, and caches source images. struct ImageSource { /*! Generates an image from a full string like * "stone.png^mineral_coal.png^[crack:1:0". @@ -53,8 +52,8 @@ struct ImageSource { */ video::IImage* generateImage(std::string_view name, std::set &source_image_names); - // To add self made images. - void insertImage(const std::string &name, video::IImage *img, bool prefer_local); + // Insert a source image into the cache without touching the filesystem. + void insertSourceImage(const std::string &name, video::IImage *img, bool prefer_local); // TODO should probably be moved elsewhere static video::SColor getImageAverageColor(const video::IImage &image); @@ -69,9 +68,10 @@ struct ImageSource { private: // Generate image based on a string like "stone.png" or "[crack:1:0". - // if baseimg is NULL, it is created. Otherwise stuff is made on it. - // source_image_names is important to determine when to flush the image from a cache (dynamic media) - bool generateImagePart(std::string_view part_of_name, video::IImage *& baseimg, std::set &source_image_names); + // If baseimg is NULL, it is created. Otherwise stuff is made on it. + // source_image_names is important to determine when to flush the image from a cache (dynamic media). + bool generateImagePart(std::string_view part_of_name, video::IImage *& baseimg, + std::set &source_image_names); // Cached settings needed for making textures from meshes bool m_setting_mipmap; diff --git a/src/client/texturesource.cpp b/src/client/texturesource.cpp index 7e56a57cd..12a21771a 100644 --- a/src/client/texturesource.cpp +++ b/src/client/texturesource.cpp @@ -27,41 +27,18 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "texturepaths.h" #include "imagesource.h" -/* - Stores internal information about a texture. -*/ +// Stores internal information about a texture. struct TextureInfo { std::string name; - video::ITexture *texture; - std::set sourceImages; + video::ITexture *texture = nullptr; - TextureInfo( - const std::string &name_, - video::ITexture *texture_=NULL - ): - name(name_), - texture(texture_) - { - } - - TextureInfo( - const std::string &name_, - video::ITexture *texture_, - std::set &&sourceImages_ - ): - name(name_), - texture(texture_), - sourceImages(std::move(sourceImages_)) - { - } + // Stores source image names which ImageSource::generateImage used. + std::set sourceImages{}; }; -/* - TextureSource -*/ - +// TextureSource class TextureSource : public IWritableTextureSource { public: @@ -150,7 +127,7 @@ public: // Shall be called from the main thread. void processQueue(); - // Insert an image into the cache without touching the filesystem. + // Insert a source image into the cache without touching the filesystem. // Shall be called from the main thread. void insertSourceImage(const std::string &name, video::IImage *img); @@ -200,11 +177,8 @@ private: // Maps image file names to loaded palettes. std::unordered_map m_palettes; - // Cached settings needed for making textures from meshes - bool m_setting_mipmap; - bool m_setting_trilinear_filter; - bool m_setting_bilinear_filter; - bool m_setting_anisotropic_filter; + // Cached from settings for making textures from meshes + bool mesh_filter_needed; }; IWritableTextureSource *createTextureSource() @@ -217,16 +191,17 @@ TextureSource::TextureSource() m_main_thread = std::this_thread::get_id(); // Add a NULL TextureInfo as the first index, named "" - m_textureinfo_cache.emplace_back(""); + m_textureinfo_cache.emplace_back(TextureInfo{""}); m_name_to_id[""] = 0; // Cache some settings // Note: Since this is only done once, the game must be restarted - // for these settings to take effect - m_setting_mipmap = g_settings->getBool("mip_map"); - m_setting_trilinear_filter = g_settings->getBool("trilinear_filter"); - m_setting_bilinear_filter = g_settings->getBool("bilinear_filter"); - m_setting_anisotropic_filter = g_settings->getBool("anisotropic_filter"); + // for these settings to take effect. + mesh_filter_needed = + g_settings->getBool("mip_map") || + g_settings->getBool("trilinear_filter") || + g_settings->getBool("bilinear_filter") || + g_settings->getBool("anisotropic_filter"); } TextureSource::~TextureSource() @@ -236,45 +211,37 @@ TextureSource::~TextureSource() unsigned int textures_before = driver->getTextureCount(); for (const auto &iter : m_textureinfo_cache) { - //cleanup texture + // cleanup texture if (iter.texture) driver->removeTexture(iter.texture); } m_textureinfo_cache.clear(); for (auto t : m_texture_trash) { - //cleanup trashed texture + // cleanup trashed texture driver->removeTexture(t); } - infostream << "~TextureSource() before cleanup: "<< textures_before + infostream << "~TextureSource() before cleanup: " << textures_before << " after: " << driver->getTextureCount() << std::endl; } u32 TextureSource::getTextureId(const std::string &name) { - { - /* - See if texture already exists - */ + { // See if texture already exists MutexAutoLock lock(m_textureinfo_cache_mutex); - std::map::iterator n; - n = m_name_to_id.find(name); + auto n = m_name_to_id.find(name); if (n != m_name_to_id.end()) - { return n->second; - } } - /* - Get texture - */ + // Get texture if (std::this_thread::get_id() == m_main_thread) { return generateTexture(name); } - infostream<<"getTextureId(): Queued: name=\""< result_queue; @@ -302,34 +269,26 @@ u32 TextureSource::getTextureId(const std::string &name) return 0; } -/* - This method generates all the textures -*/ +// This method generates all the textures u32 TextureSource::generateTexture(const std::string &name) { // Empty name means texture 0 if (name.empty()) { - infostream<<"generateTexture(): name is empty"<second; - } } - /* - Calling only allowed from main thread - */ + // Calling only allowed from main thread if (std::this_thread::get_id() != m_main_thread) { - errorstream<<"TextureSource::generateTexture() " - "called not from main thread"< source_image_names; video::IImage *img = m_imagesource.generateImage(name, source_image_names); - video::ITexture *tex = NULL; + video::ITexture *tex = nullptr; - if (img != NULL) { + if (img) { img = Align2Npot2(img, driver); // Create texture from resulting image tex = driver->addTexture(name.c_str(), img); @@ -350,14 +309,12 @@ u32 TextureSource::generateTexture(const std::string &name) img->drop(); } - /* - Add texture to caches (add NULL textures too) - */ + // Add texture to caches (add NULL textures too) MutexAutoLock lock(m_textureinfo_cache_mutex); u32 id = m_textureinfo_cache.size(); - TextureInfo ti(name, tex, std::move(source_image_names)); + TextureInfo ti{name, tex, std::move(source_image_names)}; m_textureinfo_cache.emplace_back(std::move(ti)); m_name_to_id[name] = id; @@ -368,11 +325,10 @@ std::string TextureSource::getTextureName(u32 id) { MutexAutoLock lock(m_textureinfo_cache_mutex); - if (id >= m_textureinfo_cache.size()) - { - errorstream<<"TextureSource::getTextureName(): id="<= m_textureinfo_cache.size()=" - <= m_textureinfo_cache.size()) { + errorstream << "TextureSource::getTextureName(): id=" << id + << " >= m_textureinfo_cache.size()=" << m_textureinfo_cache.size() + << std::endl; return ""; } @@ -384,7 +340,7 @@ video::ITexture* TextureSource::getTexture(u32 id) MutexAutoLock lock(m_textureinfo_cache_mutex); if (id >= m_textureinfo_cache.size()) - return NULL; + return nullptr; return m_textureinfo_cache[id].texture; } @@ -392,19 +348,16 @@ video::ITexture* TextureSource::getTexture(u32 id) video::ITexture* TextureSource::getTexture(const std::string &name, u32 *id) { u32 actual_id = getTextureId(name); - if (id){ + if (id) *id = actual_id; - } + return getTexture(actual_id); } video::ITexture* TextureSource::getTextureForMesh(const std::string &name, u32 *id) { // Avoid duplicating texture if it won't actually change - const bool filter_needed = - m_setting_mipmap || m_setting_trilinear_filter || - m_setting_bilinear_filter || m_setting_anisotropic_filter; - if (filter_needed && !name.empty()) + if (mesh_filter_needed && !name.empty()) return getTexture(name + "^[applyfiltersformesh", id); return getTexture(name, id); } @@ -415,7 +368,7 @@ Palette* TextureSource::getPalette(const std::string &name) sanity_check(std::this_thread::get_id() == m_main_thread); if (name.empty()) - return NULL; + return nullptr; auto it = m_palettes.find(name); if (it == m_palettes.end()) { @@ -425,7 +378,7 @@ Palette* TextureSource::getPalette(const std::string &name) if (!img) { warningstream << "TextureSource::getPalette(): palette \"" << name << "\" could not be loaded." << std::endl; - return NULL; + return nullptr; } Palette new_palette; u32 w = img->getDimension().Width; @@ -433,7 +386,7 @@ Palette* TextureSource::getPalette(const std::string &name) // Real area of the image u32 area = h * w; if (area == 0) - return NULL; + return nullptr; if (area > 256) { warningstream << "TextureSource::getPalette(): the specified" << " palette image \"" << name << "\" is larger than 256" @@ -462,17 +415,14 @@ Palette* TextureSource::getPalette(const std::string &name) } if (it != m_palettes.end()) return &((*it).second); - return NULL; + return nullptr; } void TextureSource::processQueue() { - /* - Fetch textures - */ + // Fetch textures // NOTE: process outstanding requests from all mesh generation threads - while (!m_get_texture_queue.empty()) - { + while (!m_get_texture_queue.empty()) { GetRequest request = m_get_texture_queue.pop(); @@ -484,7 +434,7 @@ void TextureSource::insertSourceImage(const std::string &name, video::IImage *im { sanity_check(std::this_thread::get_id() == m_main_thread); - m_imagesource.insertImage(name, img, true); + m_imagesource.insertSourceImage(name, img, true); m_source_image_existence.set(name, true); // now we need to check for any textures that need updating @@ -505,7 +455,8 @@ void TextureSource::insertSourceImage(const std::string &name, video::IImage *im } } if (affected > 0) - verbosestream << "TextureSource: inserting \"" << name << "\" caused rebuild of " << affected << " textures." << std::endl; + verbosestream << "TextureSource: inserting \"" << name << "\" caused rebuild of " + << affected << " textures." << std::endl; } void TextureSource::rebuildImagesAndTextures() @@ -516,7 +467,7 @@ void TextureSource::rebuildImagesAndTextures() sanity_check(driver); infostream << "TextureSource: recreating " << m_textureinfo_cache.size() - << " textures" << std::endl; + << " textures" << std::endl; // Recreate textures for (TextureInfo &ti : m_textureinfo_cache) { @@ -529,14 +480,15 @@ void TextureSource::rebuildImagesAndTextures() void TextureSource::rebuildTexture(video::IVideoDriver *driver, TextureInfo &ti) { assert(!ti.name.empty()); + sanity_check(std::this_thread::get_id() == m_main_thread); - // replaces the previous sourceImages - // shouldn't really need to be done, but can't hurt + // Replaces the previous sourceImages. + // Shouldn't really need to be done, but can't hurt. std::set source_image_names; video::IImage *img = m_imagesource.generateImage(ti.name, source_image_names); img = Align2Npot2(img, driver); // Create texture from resulting image - video::ITexture *t = NULL; + video::ITexture *t = nullptr; if (img) { t = driver->addTexture(ti.name.c_str(), img); guiScalingCache(io::path(ti.name.c_str()), driver, img); @@ -569,23 +521,22 @@ video::ITexture* TextureSource::getNormalTexture(const std::string &name) } return getTexture(fname_base); } - return NULL; + return nullptr; } video::SColor TextureSource::getTextureAverageColor(const std::string &name) { video::IVideoDriver *driver = RenderingEngine::get_video_driver(); - video::SColor c(0, 0, 0, 0); video::ITexture *texture = getTexture(name); if (!texture) - return c; + return {0, 0, 0, 0}; video::IImage *image = driver->createImage(texture, core::position2d(0, 0), texture->getOriginalSize()); if (!image) - return c; + return {0, 0, 0, 0}; - c = ImageSource::getImageAverageColor(*image); + video::SColor c = ImageSource::getImageAverageColor(*image); image->drop(); return c; @@ -604,7 +555,7 @@ video::ITexture *TextureSource::getShaderFlagsTexture(bool normalmap_present) video::IVideoDriver *driver = RenderingEngine::get_video_driver(); video::IImage *flags_image = driver->createImage( video::ECF_A8R8G8B8, core::dimension2d(1, 1)); - sanity_check(flags_image != NULL); + sanity_check(flags_image); video::SColor c(255, normalmap_present ? 255 : 0, 0, 0); flags_image->setPixel(0, 0, c); insertSourceImage(tname, flags_image); From d1a1aed23ec28f1f3ac28c49cdbd833440d099ab Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sat, 16 Mar 2024 15:07:18 +0100 Subject: [PATCH 05/34] Reduce unnecessary include in serialize.h --- src/content_nodemeta.cpp | 1 + src/nodemetadata.cpp | 1 + src/object_properties.cpp | 1 + src/script/common/c_converter.cpp | 1 + src/serialization.cpp | 2 +- src/util/serialize.h | 2 +- 6 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/content_nodemeta.cpp b/src/content_nodemeta.cpp index 39743c11f..f3ffc4bef 100644 --- a/src/content_nodemeta.cpp +++ b/src/content_nodemeta.cpp @@ -22,6 +22,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "nodetimer.h" #include "inventory.h" #include "log.h" +#include "debug.h" #include "serialization.h" #include "util/serialize.h" #include "util/string.h" diff --git a/src/nodemetadata.cpp b/src/nodemetadata.cpp index 43770fee1..a11503ebe 100644 --- a/src/nodemetadata.cpp +++ b/src/nodemetadata.cpp @@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "inventory.h" #include "irrlicht_changes/printing.h" #include "log.h" +#include "debug.h" #include "util/serialize.h" #include "constants.h" // MAP_BLOCKSIZE #include diff --git a/src/object_properties.cpp b/src/object_properties.cpp index df3d245f9..5fb6a7d41 100644 --- a/src/object_properties.cpp +++ b/src/object_properties.cpp @@ -21,6 +21,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "irrlicht_changes/printing.h" #include "irrlichttypes_bloated.h" #include "exceptions.h" +#include "log.h" #include "util/serialize.h" #include diff --git a/src/script/common/c_converter.cpp b/src/script/common/c_converter.cpp index a7b18365a..446f88f3d 100644 --- a/src/script/common/c_converter.cpp +++ b/src/script/common/c_converter.cpp @@ -25,6 +25,7 @@ extern "C" { #include "util/numeric.h" #include "util/serialize.h" #include "util/string.h" +#include "log.h" #include "common/c_converter.h" #include "common/c_internal.h" #include "constants.h" diff --git a/src/serialization.cpp b/src/serialization.cpp index 689e4d986..4134126ca 100644 --- a/src/serialization.cpp +++ b/src/serialization.cpp @@ -18,7 +18,7 @@ with this program; if not, write to the Free Software Foundation, Inc., */ #include "serialization.h" - +#include "log.h" #include "util/serialize.h" #include diff --git a/src/util/serialize.h b/src/util/serialize.h index 00250ece5..d7526a765 100644 --- a/src/util/serialize.h +++ b/src/util/serialize.h @@ -21,7 +21,6 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "irrlichttypes_bloated.h" #include "exceptions.h" // for SerializationError -#include "debug.h" // for assert #include "ieee_float.h" #include "config.h" @@ -39,6 +38,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #endif #endif #include // for memcpy +#include #include #include #include From 008d6be9005cf05a575a588503ea791d02c31c8e Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sat, 16 Mar 2024 15:10:11 +0100 Subject: [PATCH 06/34] Rework iconv encoding detection WCHAR_T doesn't seem as portable as we thought, so it's just easier to detect the right encoding using macros at this point. --- src/util/serialize.h | 37 ++++++++++++++++++++++++------------- src/util/string.cpp | 30 +++++++++++------------------- 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/src/util/serialize.h b/src/util/serialize.h index d7526a765..5065de61d 100644 --- a/src/util/serialize.h +++ b/src/util/serialize.h @@ -24,25 +24,36 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "ieee_float.h" #include "config.h" -#if HAVE_ENDIAN_H - #ifdef _WIN32 - #define __BYTE_ORDER 0 - #define __LITTLE_ENDIAN 0 - #define __BIG_ENDIAN 1 - #elif defined(__MACH__) && defined(__APPLE__) - #include - #elif defined(__FreeBSD__) || defined(__DragonFly__) - #include - #else - #include - #endif -#endif #include // for memcpy #include #include #include #include +/* make sure BYTE_ORDER macros are available */ +#ifdef _WIN32 + #define BYTE_ORDER 1234 +#elif defined(__MACH__) && defined(__APPLE__) + #include +#elif defined(__FreeBSD__) || defined(__DragonFly__) + #include +#elif HAVE_ENDIAN_H + #include +#else + #error "Can't detect endian (missing header)" +#endif +#ifndef LITTLE_ENDIAN + #define LITTLE_ENDIAN 1234 +#endif +#ifndef BIG_ENDIAN + #define BIG_ENDIAN 4321 +#endif +#if !defined(BYTE_ORDER) && defined(_BYTE_ORDER) + #define BYTE_ORDER _BYTE_ORDER +#elif !defined(BYTE_ORDER) && defined(__BYTE_ORDER) + #define BYTE_ORDER __BYTE_ORDER +#endif + #define FIXEDPOINT_FACTOR 1000.0f // 0x7FFFFFFF / 1000.0f is not serializable. diff --git a/src/util/string.cpp b/src/util/string.cpp index 6c2ae7ca7..eb65373a4 100644 --- a/src/util/string.cpp +++ b/src/util/string.cpp @@ -18,7 +18,7 @@ with this program; if not, write to the Free Software Foundation, Inc., */ #include "string.h" -#include "pointer.h" +#include "serialize.h" // BYTE_ORDER #include "numeric.h" #include "log.h" @@ -67,20 +67,16 @@ static bool convert(const char *to, const char *from, char *outbuf, return true; } -#ifdef __ANDROID__ -// On Android iconv disagrees how big a wchar_t is for whatever reason -const char *DEFAULT_ENCODING = "UTF-32LE"; -#elif defined(__NetBSD__) || defined(__OpenBSD__) || defined(__FreeBSD__) - // NetBSD does not allow "WCHAR_T" as a charset input to iconv. - #include - #if BYTE_ORDER == BIG_ENDIAN - const char *DEFAULT_ENCODING = "UTF-32BE"; - #else - const char *DEFAULT_ENCODING = "UTF-32LE"; - #endif -#else -const char *DEFAULT_ENCODING = "WCHAR_T"; -#endif +// select right encoding for wchar_t size +constexpr auto DEFAULT_ENCODING = ([] () -> const char* { + constexpr auto sz = sizeof(wchar_t); + static_assert(sz == 2 || sz == 4, "Unexpected wide char size"); + if constexpr (sz == 2) { + return (BYTE_ORDER == BIG_ENDIAN) ? "UTF-16BE" : "UTF-16LE"; + } else { + return (BYTE_ORDER == BIG_ENDIAN) ? "UTF-32BE" : "UTF-32LE"; + } +})(); std::wstring utf8_to_wide(std::string_view input) { @@ -93,10 +89,6 @@ std::wstring utf8_to_wide(std::string_view input) std::wstring out; out.resize(outbuf_size / sizeof(wchar_t)); -#if defined(__ANDROID__) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__FreeBSD__) - static_assert(sizeof(wchar_t) == 4, "Unexpected wide char size"); -#endif - char *outbuf = reinterpret_cast(&out[0]); if (!convert(DEFAULT_ENCODING, "UTF-8", outbuf, &outbuf_size, inbuf, inbuf_size)) { infostream << "Couldn't convert UTF-8 string 0x" << hex_encode(input) From 5df60d85f71215b091430a32c7834dd9c90bf79a Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sun, 24 Mar 2024 19:53:54 +0100 Subject: [PATCH 07/34] Cache iconv context per-thread --- src/unittest/test_utilities.cpp | 7 +++++ src/util/string.cpp | 49 ++++++++++++++++++++++++++------- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/unittest/test_utilities.cpp b/src/unittest/test_utilities.cpp index 5c653a529..16032d130 100644 --- a/src/unittest/test_utilities.cpp +++ b/src/unittest/test_utilities.cpp @@ -318,6 +318,7 @@ void TestUtilities::testUTF8() UASSERTEQ(std::string, wide_to_utf8(utf8_to_wide("")), ""); UASSERTEQ(std::string, wide_to_utf8(utf8_to_wide("the shovel dug a crumbly node!")), "the shovel dug a crumbly node!"); + UASSERTEQ(std::string, wide_to_utf8(utf8_to_wide(u8"-ä-")), u8"-ä-"); UASSERTEQ(std::string, wide_to_utf8(utf8_to_wide(u8"-\U0002000b-")), @@ -326,6 +327,12 @@ void TestUtilities::testUTF8() const auto *literal = U"-\U0002000b-"; UASSERT(utf8_to_wide(u8"-\U0002000b-") == reinterpret_cast(literal)); } + + // try to check that the conversion function does not accidentally keep + // its internal state across invocations. + // \xC4\x81 is UTF-8 for \u0101 + utf8_to_wide("\xC4"); + UASSERT(utf8_to_wide("\x81") != L"\u0101"); } void TestUtilities::testRemoveEscapes() diff --git a/src/util/string.cpp b/src/util/string.cpp index eb65373a4..0c896e6ec 100644 --- a/src/util/string.cpp +++ b/src/util/string.cpp @@ -41,28 +41,49 @@ with this program; if not, write to the Free Software Foundation, Inc., #ifndef _WIN32 -static bool convert(const char *to, const char *from, char *outbuf, - size_t *outbuf_size, char *inbuf, size_t inbuf_size) +namespace { + class IconvSmartPointer { + iconv_t m_cd; + static const iconv_t null_value; + public: + IconvSmartPointer() : m_cd(null_value) {} + ~IconvSmartPointer() { reset(); } + + DISABLE_CLASS_COPY(IconvSmartPointer) + ALLOW_CLASS_MOVE(IconvSmartPointer) + + iconv_t get() const { return m_cd; } + operator bool() const { return m_cd != null_value; } + void reset(iconv_t cd = null_value) { + if (m_cd != null_value) + iconv_close(m_cd); + m_cd = cd; + } + }; + + // note that this can't be constexpr if iconv_t is a pointer + const iconv_t IconvSmartPointer::null_value = (iconv_t) -1; +} + +static bool convert(iconv_t cd, char *outbuf, size_t *outbuf_size, + char *inbuf, size_t inbuf_size) { - iconv_t cd = iconv_open(to, from); + // reset conversion state + iconv(cd, nullptr, nullptr, nullptr, nullptr); char *inbuf_ptr = inbuf; char *outbuf_ptr = outbuf; - size_t *inbuf_left_ptr = &inbuf_size; - const size_t old_outbuf_size = *outbuf_size; size_t old_size = inbuf_size; while (inbuf_size > 0) { - iconv(cd, &inbuf_ptr, inbuf_left_ptr, &outbuf_ptr, outbuf_size); + iconv(cd, &inbuf_ptr, &inbuf_size, &outbuf_ptr, outbuf_size); if (inbuf_size == old_size) { - iconv_close(cd); return false; } old_size = inbuf_size; } - iconv_close(cd); *outbuf_size = old_outbuf_size - *outbuf_size; return true; } @@ -80,6 +101,10 @@ constexpr auto DEFAULT_ENCODING = ([] () -> const char* { std::wstring utf8_to_wide(std::string_view input) { + thread_local IconvSmartPointer cd; + if (!cd) + cd.reset(iconv_open(DEFAULT_ENCODING, "UTF-8")); + const size_t inbuf_size = input.length(); // maximum possible size, every character is sizeof(wchar_t) bytes size_t outbuf_size = input.length() * sizeof(wchar_t); @@ -90,7 +115,7 @@ std::wstring utf8_to_wide(std::string_view input) out.resize(outbuf_size / sizeof(wchar_t)); char *outbuf = reinterpret_cast(&out[0]); - if (!convert(DEFAULT_ENCODING, "UTF-8", outbuf, &outbuf_size, inbuf, inbuf_size)) { + if (!convert(cd.get(), outbuf, &outbuf_size, inbuf, inbuf_size)) { infostream << "Couldn't convert UTF-8 string 0x" << hex_encode(input) << " into wstring" << std::endl; delete[] inbuf; @@ -104,6 +129,10 @@ std::wstring utf8_to_wide(std::string_view input) std::string wide_to_utf8(std::wstring_view input) { + thread_local IconvSmartPointer cd; + if (!cd) + cd.reset(iconv_open("UTF-8", DEFAULT_ENCODING)); + const size_t inbuf_size = input.length() * sizeof(wchar_t); // maximum possible size: utf-8 encodes codepoints using 1 up to 4 bytes size_t outbuf_size = input.length() * 4; @@ -113,7 +142,7 @@ std::string wide_to_utf8(std::wstring_view input) std::string out; out.resize(outbuf_size); - if (!convert("UTF-8", DEFAULT_ENCODING, &out[0], &outbuf_size, inbuf, inbuf_size)) { + if (!convert(cd.get(), &out[0], &outbuf_size, inbuf, inbuf_size)) { infostream << "Couldn't convert wstring 0x" << hex_encode(inbuf, inbuf_size) << " into UTF-8 string" << std::endl; delete[] inbuf; From 97066bf79524cc0ecf61f45023dc2b32acaecad9 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sat, 23 Mar 2024 17:16:21 +0100 Subject: [PATCH 08/34] Sanity-check reliable packet size at earlier point --- src/network/connection.cpp | 39 ++++++++++++++++++------------- src/network/connectionthreads.cpp | 2 +- src/network/connectionthreads.h | 2 +- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/network/connection.cpp b/src/network/connection.cpp index d16d59f59..84a6c53c5 100644 --- a/src/network/connection.cpp +++ b/src/network/connection.cpp @@ -103,12 +103,10 @@ void makeSplitPacket(const SharedBuffer &data, u32 chunksize_max, u16 seqnum std::list> *chunks) { // Chunk packets, containing the TYPE_SPLIT header - u32 chunk_header_size = 7; - u32 maximum_data_size = chunksize_max - chunk_header_size; - u32 start = 0; - u32 end = 0; - u32 chunk_num = 0; - u16 chunk_count = 0; + 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) @@ -126,16 +124,16 @@ void makeSplitPacket(const SharedBuffer &data, u32 chunksize_max, u16 seqnum memcpy(&chunk[chunk_header_size], &data[start], payload_size); chunks->push_back(chunk); - chunk_count++; start = end + 1; + sanity_check(chunk_num < 0xFFFF); // overflow chunk_num++; } while (end != data.getSize() - 1); - for (SharedBuffer &chunk : *chunks) { + for (auto &chunk : *chunks) { // Write chunk_count - writeU16(&(chunk[3]), chunk_count); + writeU16(&chunk[3], chunk_num); } } @@ -1061,22 +1059,22 @@ bool UDPPeer::processReliableSendCommand( const auto &c = *c_ptr; Channel &chan = channels[c.channelnum]; - u32 chunksize_max = max_packet_size + const u32 chunksize_max = max_packet_size - BASE_HEADER_SIZE - RELIABLE_HEADER_SIZE; - sanity_check(c.data.getSize() < MAX_RELIABLE_WINDOW_SIZE*512); - std::list> originals; - u16 split_sequence_number = chan.readNextSplitSeqNum(); if (c.raw) { originals.emplace_back(c.data); } else { - makeAutoSplitPacket(c.data, chunksize_max,split_sequence_number, &originals); - chan.setNextSplitSeqNum(split_sequence_number); + 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; @@ -1271,7 +1269,7 @@ Connection::Connection(u32 protocol_id, u32 max_packet_size, float timeout, m_udpSocket(ipv6), m_protocol_id(protocol_id), m_sendThread(new ConnectionSendThread(max_packet_size, timeout)), - m_receiveThread(new ConnectionReceiveThread(max_packet_size)), + m_receiveThread(new ConnectionReceiveThread()), m_bc_peerhandler(peerhandler) { @@ -1505,6 +1503,15 @@ void Connection::Send(session_t peer_id, u8 channelnum, { 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)); } diff --git a/src/network/connectionthreads.cpp b/src/network/connectionthreads.cpp index 3d217ec6c..d5c9a39ed 100644 --- a/src/network/connectionthreads.cpp +++ b/src/network/connectionthreads.cpp @@ -796,7 +796,7 @@ void ConnectionSendThread::sendAsPacket(session_t peer_id, u8 channelnum, m_outgoing_queue.push(packet); } -ConnectionReceiveThread::ConnectionReceiveThread(unsigned int max_packet_size) : +ConnectionReceiveThread::ConnectionReceiveThread() : Thread("ConnectionReceive") { } diff --git a/src/network/connectionthreads.h b/src/network/connectionthreads.h index ce96e4342..fff71f657 100644 --- a/src/network/connectionthreads.h +++ b/src/network/connectionthreads.h @@ -113,7 +113,7 @@ private: class ConnectionReceiveThread : public Thread { public: - ConnectionReceiveThread(unsigned int max_packet_size); + ConnectionReceiveThread(); void *run(); From 6e3246c5fd7aa78dc697f7a588e6a01b5f00db7c Mon Sep 17 00:00:00 2001 From: grorp Date: Sat, 30 Mar 2024 11:07:36 +0100 Subject: [PATCH 09/34] Limit crosshair scaling to integer multiples to avoid distortion (#14501) --- src/client/hud.cpp | 31 ++++++++++++++++++++++--------- src/client/hud.h | 1 + 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/client/hud.cpp b/src/client/hud.cpp index e1641366e..5a6f5d5c2 100644 --- a/src/client/hud.cpp +++ b/src/client/hud.cpp @@ -44,6 +44,11 @@ with this program; if not, write to the Free Software Foundation, Inc., #define OBJECT_CROSSHAIR_LINE_SIZE 8 #define CROSSHAIR_LINE_SIZE 10 +static void setting_changed_callback(const std::string &name, void *data) +{ + static_cast(data)->readScalingSetting(); +} + Hud::Hud(Client *client, LocalPlayer *player, Inventory *inventory) { @@ -52,12 +57,8 @@ Hud::Hud(Client *client, LocalPlayer *player, this->player = player; this->inventory = inventory; - m_hud_scaling = g_settings->getFloat("hud_scaling", 0.5f, 20.0f); - m_scale_factor = m_hud_scaling * RenderingEngine::getDisplayDensity(); - m_hotbar_imagesize = std::floor(HOTBAR_IMAGE_SIZE * - RenderingEngine::getDisplayDensity() + 0.5f); - m_hotbar_imagesize *= m_hud_scaling; - m_padding = m_hotbar_imagesize / 12; + readScalingSetting(); + g_settings->registerChangedCallback("hud_scaling", setting_changed_callback, this); for (auto &hbar_color : hbar_colors) hbar_color = video::SColor(255, 255, 255, 255); @@ -138,8 +139,20 @@ Hud::Hud(Client *client, LocalPlayer *player, m_rotation_mesh_buffer.setHardwareMappingHint(scene::EHM_STATIC); } +void Hud::readScalingSetting() +{ + m_hud_scaling = g_settings->getFloat("hud_scaling", 0.5f, 20.0f); + m_scale_factor = m_hud_scaling * RenderingEngine::getDisplayDensity(); + m_hotbar_imagesize = std::floor(HOTBAR_IMAGE_SIZE * + RenderingEngine::getDisplayDensity() + 0.5f); + m_hotbar_imagesize *= m_hud_scaling; + m_padding = m_hotbar_imagesize / 12; +} + Hud::~Hud() { + g_settings->deregisterChangedCallback("hud_scaling", setting_changed_callback, this); + if (m_selection_mesh) m_selection_mesh->drop(); } @@ -786,9 +799,9 @@ void Hud::drawCrosshair() { auto draw_image_crosshair = [this] (video::ITexture *tex) { core::dimension2di orig_size(tex->getOriginalSize()); - core::dimension2di scaled_size( - core::round32(orig_size.Width * m_scale_factor), - core::round32(orig_size.Height * m_scale_factor)); + // Integer scaling to avoid artifacts, floor instead of round since too + // small looks better than too large in this case. + core::dimension2di scaled_size = orig_size * std::floor(m_scale_factor); core::rect src_rect(orig_size); core::position2d pos(m_displaycenter.X - scaled_size.Width / 2, diff --git a/src/client/hud.h b/src/client/hud.h index 303feb783..746a1b33e 100644 --- a/src/client/hud.h +++ b/src/client/hud.h @@ -57,6 +57,7 @@ public: Hud(Client *client, LocalPlayer *player, Inventory *inventory); + void readScalingSetting(); ~Hud(); enum BlockBoundsMode toggleBlockBounds(); From e8a8525bcdae6b2109b9e4158f928a7c6f9c1404 Mon Sep 17 00:00:00 2001 From: Muhammad Rifqi Priyo Susanto Date: Sat, 30 Mar 2024 17:08:18 +0700 Subject: [PATCH 10/34] Fix path splitting in translation updater script --- util/mod_translation_updater.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/mod_translation_updater.py b/util/mod_translation_updater.py index c941331a8..8c468c8d2 100755 --- a/util/mod_translation_updater.py +++ b/util/mod_translation_updater.py @@ -425,7 +425,7 @@ def generate_template(folder, mod_name): sources = sorted(list(sources), key=str.lower) newSources = [] for i in sources: - i = "/".join(os.path.split(i)).lstrip("/") + i = i.replace("\\", "/") newSources.append(f"{symbol_source_prefix} {i} {symbol_source_suffix}") dOut[d] = newSources From 517f1602aa77c93af5b06f6cd1f2c20075f9547b Mon Sep 17 00:00:00 2001 From: Gregor Parzefall Date: Sun, 24 Mar 2024 22:35:55 +0100 Subject: [PATCH 11/34] Re-add "long tap to punch" as a client-side setting --- builtin/settingtypes.txt | 13 +++++++++++++ doc/lua_api.md | 9 +++++---- src/client/game.cpp | 6 ++++-- src/defaultsettings.cpp | 1 + src/gui/touchscreengui.cpp | 4 ++-- src/itemdef.cpp | 31 ++++++++++++++++++++++--------- src/itemdef.h | 8 ++++++-- src/script/common/c_types.cpp | 1 + 8 files changed, 54 insertions(+), 19 deletions(-) diff --git a/builtin/settingtypes.txt b/builtin/settingtypes.txt index b039780bc..b9c0cda80 100644 --- a/builtin/settingtypes.txt +++ b/builtin/settingtypes.txt @@ -185,6 +185,19 @@ fixed_virtual_joystick (Fixed virtual joystick) bool false # Requires: touchscreen_gui virtual_joystick_triggers_aux1 (Virtual joystick triggers Aux1 button) bool false +# The gesture for for punching players/entities. +# This can be overridden by games and mods. +# +# * short_tap +# Easy to use and well-known from other games that shall not be named. +# +# * long_tap +# Known from the classic Minetest mobile controls. +# Combat is more or less impossible. +# +# Requires: touchscreen_gui +touch_punch_gesture (Punch gesture) enum short_tap short_tap,long_tap + [Graphics and Audio] diff --git a/doc/lua_api.md b/doc/lua_api.md index 8a1df92ef..30b7e46bc 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -9098,15 +9098,16 @@ Used by `minetest.register_node`, `minetest.register_craftitem`, and touch_interaction = { -- Only affects touchscreen clients. -- Defines the meaning of short and long taps with the item in hand. - -- The fields in this table have two valid values: + -- The fields in this table can be set to the following 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) -- The field to be used is selected according to the current -- `pointed_thing`. - pointed_nothing = "long_dig_short_place", - pointed_node = "long_dig_short_place", - pointed_object = "short_dig_long_place", + pointed_nothing = "user", + pointed_node = "user", + pointed_object = "user", }, sound = { diff --git a/src/client/game.cpp b/src/client/game.cpp index 67529681b..3c3feaa72 100644 --- a/src/client/game.cpp +++ b/src/client/game.cpp @@ -3280,8 +3280,10 @@ void Game::processPlayerInteraction(f32 dtime, bool show_hud) if (pointed != runData.pointed_old) infostream << "Pointing at " << pointed.dump() << std::endl; - if (g_touchscreengui) - g_touchscreengui->applyContextControls(selected_def.touch_interaction.getMode(pointed)); + if (g_touchscreengui) { + auto mode = selected_def.touch_interaction.getMode(pointed.type); + g_touchscreengui->applyContextControls(mode); + } // Note that updating the selection mesh every frame is not particularly efficient, // but the halo rendering code is already inefficient so there's no point in optimizing it here diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp index bb50a00aa..11b42c779 100644 --- a/src/defaultsettings.cpp +++ b/src/defaultsettings.cpp @@ -490,6 +490,7 @@ void set_default_settings() settings->setDefault("touch_use_crosshair", "false"); settings->setDefault("fixed_virtual_joystick", "false"); settings->setDefault("virtual_joystick_triggers_aux1", "false"); + settings->setDefault("touch_punch_gesture", "short_tap"); #ifdef ENABLE_TOUCH settings->setDefault("clickable_chat_weblinks", "false"); #else diff --git a/src/gui/touchscreengui.cpp b/src/gui/touchscreengui.cpp index 07fe90585..2d41d4fde 100644 --- a/src/gui/touchscreengui.cpp +++ b/src/gui/touchscreengui.cpp @@ -1105,11 +1105,11 @@ void TouchScreenGUI::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. + sanity_check(mode != TouchInteractionMode_USER); + u64 now = porting::getTimeMs(); bool target_dig_pressed = false; bool target_place_pressed = false; - u64 now = porting::getTimeMs(); - // If the meanings of short and long taps have been swapped, abort any ongoing // short taps because they would do something else than the player expected. // Long taps don't need this, they're adjusted to the swapped meanings instead. diff --git a/src/itemdef.cpp b/src/itemdef.cpp index 5031487ea..ad2ed4847 100644 --- a/src/itemdef.cpp +++ b/src/itemdef.cpp @@ -40,24 +40,37 @@ with this program; if not, write to the Free Software Foundation, Inc., TouchInteraction::TouchInteraction() { - pointed_nothing = LONG_DIG_SHORT_PLACE; - pointed_node = LONG_DIG_SHORT_PLACE; - // Map punching to single tap by default. - pointed_object = SHORT_DIG_LONG_PLACE; + pointed_nothing = TouchInteractionMode_USER; + pointed_node = TouchInteractionMode_USER; + pointed_object = TouchInteractionMode_USER; } -TouchInteractionMode TouchInteraction::getMode(const PointedThing &pointed) const +TouchInteractionMode TouchInteraction::getMode(PointedThingType pointed_type) const { - switch (pointed.type) { + TouchInteractionMode result; + switch (pointed_type) { case POINTEDTHING_NOTHING: - return pointed_nothing; + result = pointed_nothing; + break; case POINTEDTHING_NODE: - return pointed_node; + result = pointed_node; + break; case POINTEDTHING_OBJECT: - return pointed_object; + result = pointed_object; + break; default: FATAL_ERROR("Invalid PointedThingType given to TouchInteraction::getMode"); } + + if (result == TouchInteractionMode_USER) { + if (pointed_type == POINTEDTHING_OBJECT) + result = g_settings->get("touch_punch_gesture") == "long_tap" ? + LONG_DIG_SHORT_PLACE : SHORT_DIG_LONG_PLACE; + else + result = LONG_DIG_SHORT_PLACE; + } + + return result; } void TouchInteraction::serialize(std::ostream &os) const diff --git a/src/itemdef.h b/src/itemdef.h index 884d31501..782dad816 100644 --- a/src/itemdef.h +++ b/src/itemdef.h @@ -30,10 +30,11 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "texture_override.h" // TextureOverride #include "tool.h" #include "util/pointabilities.h" +#include "util/pointedthing.h" + class IGameDef; class Client; struct ToolCapabilities; -struct PointedThing; #ifndef SERVER #include "client/texturesource.h" struct ItemMesh; @@ -57,6 +58,7 @@ enum TouchInteractionMode : u8 { LONG_DIG_SHORT_PLACE, SHORT_DIG_LONG_PLACE, + TouchInteractionMode_USER, // Meaning depends on client-side settings TouchInteractionMode_END, // Dummy for validity check }; @@ -67,7 +69,9 @@ struct TouchInteraction TouchInteractionMode pointed_object; TouchInteraction(); - TouchInteractionMode getMode(const PointedThing &pointed) const; + // 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; void serialize(std::ostream &os) const; void deSerialize(std::istream &is); }; diff --git a/src/script/common/c_types.cpp b/src/script/common/c_types.cpp index 7338834f7..be6ef65ae 100644 --- a/src/script/common/c_types.cpp +++ b/src/script/common/c_types.cpp @@ -37,5 +37,6 @@ struct EnumString es_TouchInteractionMode[] = { {LONG_DIG_SHORT_PLACE, "long_dig_short_place"}, {SHORT_DIG_LONG_PLACE, "short_dig_long_place"}, + {TouchInteractionMode_USER, "user"}, {0, NULL}, }; From 8935f2af3ca8b6720e98ecbe25b52c4a56db9bc4 Mon Sep 17 00:00:00 2001 From: Gregor Parzefall Date: Sun, 24 Mar 2024 21:55:39 +0100 Subject: [PATCH 12/34] Make long tap delay customizable and change default to 400ms --- builtin/settingtypes.txt | 15 ++++++++++----- src/defaultsettings.cpp | 3 ++- src/gui/touchscreengui.cpp | 3 ++- src/gui/touchscreengui.h | 2 +- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/builtin/settingtypes.txt b/builtin/settingtypes.txt index b9c0cda80..8ffb1c3c1 100644 --- a/builtin/settingtypes.txt +++ b/builtin/settingtypes.txt @@ -157,16 +157,21 @@ invert_hotbar_mouse_wheel (Hotbar: Invert mouse wheel direction) bool false # Requires: !android enable_touch (Enable touchscreen) bool true -# The length in pixels it takes for touchscreen interaction to start. -# -# Requires: touchscreen_gui -touchscreen_threshold (Touchscreen threshold) int 20 0 100 - # Touchscreen sensitivity multiplier. # # Requires: touchscreen_gui touchscreen_sensitivity (Touchscreen sensitivity) float 0.2 0.001 10.0 +# The length in pixels after which a touch interaction is considered movement. +# +# Requires: touchscreen_gui +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 +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. # diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp index 11b42c779..0907c4c6d 100644 --- a/src/defaultsettings.cpp +++ b/src/defaultsettings.cpp @@ -485,8 +485,9 @@ void set_default_settings() settings->setDefault("keymap_sneak", "KEY_SHIFT"); #endif - settings->setDefault("touchscreen_threshold", "20"); settings->setDefault("touchscreen_sensitivity", "0.2"); + settings->setDefault("touchscreen_threshold", "20"); + settings->setDefault("touch_long_tap_delay", "400"); settings->setDefault("touch_use_crosshair", "false"); settings->setDefault("fixed_virtual_joystick", "false"); settings->setDefault("virtual_joystick_triggers_aux1", "false"); diff --git a/src/gui/touchscreengui.cpp b/src/gui/touchscreengui.cpp index 2d41d4fde..007420b08 100644 --- a/src/gui/touchscreengui.cpp +++ b/src/gui/touchscreengui.cpp @@ -414,6 +414,7 @@ TouchScreenGUI::TouchScreenGUI(IrrlichtDevice *device, IEventReceiver *receiver) } m_touchscreen_threshold = g_settings->getU16("touchscreen_threshold"); + m_long_tap_delay = g_settings->getU16("touch_long_tap_delay"); m_fixed_joystick = g_settings->getBool("fixed_virtual_joystick"); m_joystick_triggers_aux1 = g_settings->getBool("virtual_joystick_triggers_aux1"); m_screensize = m_device->getVideoDriver()->getScreenSize(); @@ -999,7 +1000,7 @@ void TouchScreenGUI::step(float dtime) if (m_has_move_id && !m_move_has_really_moved && m_tap_state == TapState::None) { u64 delta = porting::getDeltaMs(m_move_downtime, porting::getTimeMs()); - if (delta > MIN_DIG_TIME_MS) { + if (delta > m_long_tap_delay) { m_tap_state = TapState::LongTap; } } diff --git a/src/gui/touchscreengui.h b/src/gui/touchscreengui.h index 1e9aa128d..2ee881b93 100644 --- a/src/gui/touchscreengui.h +++ b/src/gui/touchscreengui.h @@ -79,7 +79,6 @@ typedef enum AHBB_Dir_Right_Left } autohide_button_bar_dir; -#define MIN_DIG_TIME_MS 500 #define BUTTON_REPEAT_DELAY 0.2f #define SETTINGS_BAR_Y_OFFSET 5 #define RARE_CONTROLS_BAR_Y_OFFSET 5 @@ -225,6 +224,7 @@ private: v2u32 m_screensize; s32 button_size; double m_touchscreen_threshold; + u16 m_long_tap_delay; bool m_visible; // is the whole touch screen gui visible std::unordered_map> m_hotbar_rects; From e79587c934d2a0497ed8a4300a6e58e1d35e70d7 Mon Sep 17 00:00:00 2001 From: Gregor Parzefall Date: Sun, 4 Feb 2024 19:13:29 +0100 Subject: [PATCH 13/34] Add shorthand form for touch_interaction --- doc/lua_api.md | 26 +++++++++++++------------- src/script/common/c_content.cpp | 15 +++++++++++---- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/doc/lua_api.md b/doc/lua_api.md index 30b7e46bc..329f17b01 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -9095,20 +9095,20 @@ Used by `minetest.register_node`, `minetest.register_craftitem`, and -- Otherwise should be name of node which the client immediately places -- upon digging. Server will always update with actual result shortly. - touch_interaction = { - -- Only affects touchscreen clients. - -- Defines the meaning of short and long taps with the item in hand. - -- The fields in this table can be set to the following 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) - -- The field to be used is selected according to the current - -- `pointed_thing`. - - pointed_nothing = "user", - pointed_node = "user", - pointed_object = "user", + touch_interaction = OR { + pointed_nothing = , + pointed_node = , + pointed_object = , }, + -- Only affects touchscreen clients. + -- Defines the meaning of short and long taps with the item in hand. + -- 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) + -- The default value is "user". sound = { -- Definition of item sounds to be played at various events. diff --git a/src/script/common/c_content.cpp b/src/script/common/c_content.cpp index 02f69b3e1..de4f2fe02 100644 --- a/src/script/common/c_content.cpp +++ b/src/script/common/c_content.cpp @@ -156,17 +156,24 @@ void read_item_definition(lua_State* L, int index, getboolfield(L, index, "wallmounted_rotate_vertical", def.wallmounted_rotate_vertical); + TouchInteraction &inter = def.touch_interaction; lua_getfield(L, index, "touch_interaction"); - if (!lua_isnil(L, -1)) { - luaL_checktype(L, -1, LUA_TTABLE); - - TouchInteraction &inter = def.touch_interaction; + if (lua_istable(L, -1)) { inter.pointed_nothing = (TouchInteractionMode)getenumfield(L, -1, "pointed_nothing", es_TouchInteractionMode, inter.pointed_nothing); inter.pointed_node = (TouchInteractionMode)getenumfield(L, -1, "pointed_node", es_TouchInteractionMode, inter.pointed_node); inter.pointed_object = (TouchInteractionMode)getenumfield(L, -1, "pointed_object", es_TouchInteractionMode, inter.pointed_object); + } else if (lua_isstring(L, -1)) { + int value; + if (string_to_enum(es_TouchInteractionMode, value, lua_tostring(L, -1))) { + inter.pointed_nothing = (TouchInteractionMode)value; + inter.pointed_node = (TouchInteractionMode)value; + inter.pointed_object = (TouchInteractionMode)value; + } + } else if (!lua_isnil(L, -1)) { + throw LuaError("invalid type for 'touch_interaction'"); } lua_pop(L, 1); } From 3a35db6e67f542e923e624c32e9d07c1bdbe919a Mon Sep 17 00:00:00 2001 From: cx384 Date: Fri, 22 Mar 2024 00:46:19 +0100 Subject: [PATCH 14/34] Turn dos files into unix files --- misc/winresource.rc | 132 +-- src/gui/guiButton.cpp | 1538 +++++++++++++++--------------- src/gui/guiButton.h | 560 +++++------ src/gui/guiSkin.cpp | 2074 ++++++++++++++++++++--------------------- src/gui/guiSkin.h | 720 +++++++------- 5 files changed, 2512 insertions(+), 2512 deletions(-) diff --git a/misc/winresource.rc b/misc/winresource.rc index ffb493873..d5a71797a 100644 --- a/misc/winresource.rc +++ b/misc/winresource.rc @@ -1,66 +1,66 @@ -#include -#include -#include -#include - -#ifndef USE_CMAKE_CONFIG_H -#define USE_CMAKE_CONFIG_H -#endif -#include "config.h" -#undef USE_CMAKE_CONFIG_H - -#if RUN_IN_PLACE - #define BUILDMODE "RUN_IN_PLACE=1" -#else - #define BUILDMODE "RUN_IN_PLACE=0" -#endif - -#ifdef __MINGW32__ -CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "minetest.exe.manifest" -#endif - -LANGUAGE 0, SUBLANG_NEUTRAL -130 ICON "minetest-icon.ico" - -///////////////////////////////////////////////////////////////////////////// -// -// Version -// - -1 VERSIONINFO - FILEVERSION VERSION_MAJOR,VERSION_MINOR,VERSION_PATCH,0 - PRODUCTVERSION VERSION_MAJOR,VERSION_MINOR,VERSION_PATCH,0 - FILEFLAGSMASK 0x3fL -#ifndef NDEBUG - FILEFLAGS 0x1L -#else - FILEFLAGS 0x0L -#endif - FILEOS VOS_NT_WINDOWS32 - FILETYPE VFT_APP - FILESUBTYPE 0x0L -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "040904b0" - BEGIN - VALUE "Comments", "" - VALUE "CompanyName", PROJECT_NAME_C " community" - VALUE "FileDescription", PROJECT_NAME_C " engine" - VALUE "FileVersion", VERSION_STRING - VALUE "InternalName", PROJECT_NAME - VALUE "LegalCopyright", "(c) 2011-2015 celeron55" - VALUE "LegalTrademarks", """Minetest"" is the property of the Minetest community, don't use it without permission!" - VALUE "OriginalFilename", "minetest.exe" - VALUE "PrivateBuild", VERSION_EXTRA - VALUE "ProductName", PROJECT_NAME_C - VALUE "ProductVersion", PRODUCT_VERSION_STRING - VALUE "SpecialBuild", BUILDMODE - END -END -BLOCK "VarFileInfo" -BEGIN - VALUE "Translation", 0x409, 1200 -END -END - +#include +#include +#include +#include + +#ifndef USE_CMAKE_CONFIG_H +#define USE_CMAKE_CONFIG_H +#endif +#include "config.h" +#undef USE_CMAKE_CONFIG_H + +#if RUN_IN_PLACE + #define BUILDMODE "RUN_IN_PLACE=1" +#else + #define BUILDMODE "RUN_IN_PLACE=0" +#endif + +#ifdef __MINGW32__ +CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "minetest.exe.manifest" +#endif + +LANGUAGE 0, SUBLANG_NEUTRAL +130 ICON "minetest-icon.ico" + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +1 VERSIONINFO + FILEVERSION VERSION_MAJOR,VERSION_MINOR,VERSION_PATCH,0 + PRODUCTVERSION VERSION_MAJOR,VERSION_MINOR,VERSION_PATCH,0 + FILEFLAGSMASK 0x3fL +#ifndef NDEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS_NT_WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "Comments", "" + VALUE "CompanyName", PROJECT_NAME_C " community" + VALUE "FileDescription", PROJECT_NAME_C " engine" + VALUE "FileVersion", VERSION_STRING + VALUE "InternalName", PROJECT_NAME + VALUE "LegalCopyright", "(c) 2011-2015 celeron55" + VALUE "LegalTrademarks", """Minetest"" is the property of the Minetest community, don't use it without permission!" + VALUE "OriginalFilename", "minetest.exe" + VALUE "PrivateBuild", VERSION_EXTRA + VALUE "ProductName", PROJECT_NAME_C + VALUE "ProductVersion", PRODUCT_VERSION_STRING + VALUE "SpecialBuild", BUILDMODE + END +END +BLOCK "VarFileInfo" +BEGIN + VALUE "Translation", 0x409, 1200 +END +END + diff --git a/src/gui/guiButton.cpp b/src/gui/guiButton.cpp index 30a9b45cc..4e47e7425 100644 --- a/src/gui/guiButton.cpp +++ b/src/gui/guiButton.cpp @@ -1,769 +1,769 @@ -// 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 - -#include "guiButton.h" - - -#include "client/guiscalingfilter.h" -#include "IGUISkin.h" -#include "IGUIEnvironment.h" -#include "IVideoDriver.h" -#include "IGUIFont.h" -#include "irrlicht_changes/static_text.h" -#include "porting.h" -#include "StyleSpec.h" -#include "util/numeric.h" - -using namespace irr; -using namespace gui; - -// Multiply with a color to get the default corresponding hovered color -#define COLOR_HOVERED_MOD 1.25f - -// Multiply with a color to get the default corresponding pressed color -#define COLOR_PRESSED_MOD 0.85f - -//! constructor -GUIButton::GUIButton(IGUIEnvironment* environment, IGUIElement* parent, - s32 id, core::rect rectangle, ISimpleTextureSource *tsrc, - bool noclip) : - IGUIButton(environment, parent, id, rectangle), - TSrc(tsrc) -{ - setNotClipped(noclip); - - // This element can be tabbed. - setTabStop(true); - setTabOrder(-1); - - // PATCH - for (size_t i = 0; i < 4; i++) { - Colors[i] = Environment->getSkin()->getColor((EGUI_DEFAULT_COLOR)i); - } - StaticText = gui::StaticText::add(Environment, Text.c_str(), core::rect(0,0,rectangle.getWidth(),rectangle.getHeight()), false, false, this, id); - StaticText->setTextAlignment(EGUIA_CENTER, EGUIA_CENTER); - // END PATCH -} - -//! destructor -GUIButton::~GUIButton() -{ - if (OverrideFont) - OverrideFont->drop(); - - if (SpriteBank) - SpriteBank->drop(); -} - - -//! Sets if the images should be scaled to fit the button -void GUIButton::setScaleImage(bool scaleImage) -{ - ScaleImage = scaleImage; -} - - -//! Returns whether the button scale the used images -bool GUIButton::isScalingImage() const -{ - return ScaleImage; -} - - -//! Sets if the button should use the skin to draw its border -void GUIButton::setDrawBorder(bool border) -{ - DrawBorder = border; -} - - -void GUIButton::setSpriteBank(IGUISpriteBank* sprites) -{ - if (sprites) - sprites->grab(); - - if (SpriteBank) - SpriteBank->drop(); - - SpriteBank = sprites; -} - -void GUIButton::setSprite(EGUI_BUTTON_STATE state, s32 index, video::SColor color, bool loop, bool scale) -{ - ButtonSprites[(u32)state].Index = index; - ButtonSprites[(u32)state].Color = color; - ButtonSprites[(u32)state].Loop = loop; - ButtonSprites[(u32)state].Scale = scale; -} - -//! Get the sprite-index for the given state or -1 when no sprite is set -s32 GUIButton::getSpriteIndex(EGUI_BUTTON_STATE state) const -{ - return ButtonSprites[(u32)state].Index; -} - -//! Get the sprite color for the given state. Color is only used when a sprite is set. -video::SColor GUIButton::getSpriteColor(EGUI_BUTTON_STATE state) const -{ - return ButtonSprites[(u32)state].Color; -} - -//! Returns if the sprite in the given state does loop -bool GUIButton::getSpriteLoop(EGUI_BUTTON_STATE state) const -{ - return ButtonSprites[(u32)state].Loop; -} - -//! Returns if the sprite in the given state is scaled -bool GUIButton::getSpriteScale(EGUI_BUTTON_STATE state) const -{ - return ButtonSprites[(u32)state].Scale; -} - -//! called if an event happened. -bool GUIButton::OnEvent(const SEvent& event) -{ - if (!isEnabled()) - return IGUIElement::OnEvent(event); - - switch(event.EventType) - { - case EET_KEY_INPUT_EVENT: - if (event.KeyInput.PressedDown && - (event.KeyInput.Key == KEY_RETURN || event.KeyInput.Key == KEY_SPACE)) - { - if (!IsPushButton) - setPressed(true); - else - setPressed(!Pressed); - - return true; - } - if (Pressed && !IsPushButton && event.KeyInput.PressedDown && event.KeyInput.Key == KEY_ESCAPE) - { - setPressed(false); - return true; - } - else - if (!event.KeyInput.PressedDown && Pressed && - (event.KeyInput.Key == KEY_RETURN || event.KeyInput.Key == KEY_SPACE)) - { - - if (!IsPushButton) - setPressed(false); - - if (Parent) - { - ClickShiftState = event.KeyInput.Shift; - ClickControlState = event.KeyInput.Control; - - SEvent newEvent; - newEvent.EventType = EET_GUI_EVENT; - newEvent.GUIEvent.Caller = this; - newEvent.GUIEvent.Element = 0; - newEvent.GUIEvent.EventType = EGET_BUTTON_CLICKED; - Parent->OnEvent(newEvent); - } - return true; - } - break; - case EET_GUI_EVENT: - if (event.GUIEvent.Caller == this) - { - if (event.GUIEvent.EventType == EGET_ELEMENT_FOCUS_LOST) - { - if (!IsPushButton) - setPressed(false); - FocusTime = (u32)porting::getTimeMs(); - } - else if (event.GUIEvent.EventType == EGET_ELEMENT_FOCUSED) - { - FocusTime = (u32)porting::getTimeMs(); - } - else if (event.GUIEvent.EventType == EGET_ELEMENT_HOVERED || event.GUIEvent.EventType == EGET_ELEMENT_LEFT) - { - HoverTime = (u32)porting::getTimeMs(); - } - } - break; - case EET_MOUSE_INPUT_EVENT: - if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) - { - // Sometimes formspec elements can receive mouse events when the - // mouse is outside of the formspec. Thus, we test the position here. - if ( !IsPushButton && AbsoluteClippingRect.isPointInside( - core::position2d(event.MouseInput.X, event.MouseInput.Y ))) { - Environment->setFocus(this); - setPressed(true); - } - - return true; - } - else - if (event.MouseInput.Event == EMIE_LMOUSE_LEFT_UP) - { - bool wasPressed = Pressed; - - if ( !AbsoluteClippingRect.isPointInside( core::position2d(event.MouseInput.X, event.MouseInput.Y ) ) ) - { - if (!IsPushButton) - setPressed(false); - return true; - } - - if (!IsPushButton) - setPressed(false); - else - { - setPressed(!Pressed); - } - - if ((!IsPushButton && wasPressed && Parent) || - (IsPushButton && wasPressed != Pressed)) - { - ClickShiftState = event.MouseInput.Shift; - ClickControlState = event.MouseInput.Control; - - SEvent newEvent; - newEvent.EventType = EET_GUI_EVENT; - newEvent.GUIEvent.Caller = this; - newEvent.GUIEvent.Element = 0; - newEvent.GUIEvent.EventType = EGET_BUTTON_CLICKED; - Parent->OnEvent(newEvent); - } - - return true; - } - break; - default: - break; - } - - return Parent ? Parent->OnEvent(event) : false; -} - - -//! draws the element and its children -void GUIButton::draw() -{ - if (!IsVisible) - return; - - // PATCH - // Track hovered state, if it has changed then we need to update the style. - bool hovered = isHovered(); - bool focused = isFocused(); - if (hovered != WasHovered || focused != WasFocused) { - WasHovered = hovered; - WasFocused = focused; - setFromState(); - } - - GUISkin* skin = dynamic_cast(Environment->getSkin()); - video::IVideoDriver* driver = Environment->getVideoDriver(); - // END PATCH - - if (DrawBorder) - { - if (!Pressed) - { - // PATCH - skin->drawColored3DButtonPaneStandard(this, AbsoluteRect, - &AbsoluteClippingRect, Colors); - // END PATCH - } - else - { - // PATCH - skin->drawColored3DButtonPanePressed(this, AbsoluteRect, - &AbsoluteClippingRect, Colors); - // END PATCH - } - } - - const core::position2di buttonCenter(AbsoluteRect.getCenter()); - // PATCH - // The image changes based on the state, so we use the default every time. - EGUI_BUTTON_IMAGE_STATE imageState = EGBIS_IMAGE_UP; - // END PATCH - if ( ButtonImages[(u32)imageState].Texture ) - { - core::position2d pos(buttonCenter); - core::rect sourceRect(ButtonImages[(u32)imageState].SourceRect); - if ( sourceRect.getWidth() == 0 && sourceRect.getHeight() == 0 ) - sourceRect = core::rect(core::position2di(0,0), ButtonImages[(u32)imageState].Texture->getOriginalSize()); - - pos.X -= sourceRect.getWidth() / 2; - pos.Y -= sourceRect.getHeight() / 2; - - if ( Pressed ) - { - // Create a pressed-down effect by moving the image when it looks identical to the unpressed state image - EGUI_BUTTON_IMAGE_STATE unpressedState = getImageState(false); - if ( unpressedState == imageState || ButtonImages[(u32)imageState] == ButtonImages[(u32)unpressedState] ) - { - pos.X += skin->getSize(EGDS_BUTTON_PRESSED_IMAGE_OFFSET_X); - pos.Y += skin->getSize(EGDS_BUTTON_PRESSED_IMAGE_OFFSET_Y); - } - } - - // PATCH - video::ITexture* texture = ButtonImages[(u32)imageState].Texture; - video::SColor image_colors[] = { BgColor, BgColor, BgColor, BgColor }; - if (BgMiddle.getArea() == 0) { - driver->draw2DImage(texture, - ScaleImage? AbsoluteRect : core::rect(pos, sourceRect.getSize()), - sourceRect, &AbsoluteClippingRect, - image_colors, UseAlphaChannel); - } else { - draw2DImage9Slice(driver, texture, - ScaleImage ? AbsoluteRect : core::rect(pos, sourceRect.getSize()), - sourceRect, BgMiddle, &AbsoluteClippingRect, image_colors); - } - // END PATCH - } - - if (SpriteBank) - { - if (isEnabled()) - { - core::position2di pos(buttonCenter); - // pressed / unpressed animation - EGUI_BUTTON_STATE state = Pressed ? EGBS_BUTTON_DOWN : EGBS_BUTTON_UP; - drawSprite(state, ClickTime, pos); - - // focused / unfocused animation - state = Environment->hasFocus(this) ? EGBS_BUTTON_FOCUSED : EGBS_BUTTON_NOT_FOCUSED; - drawSprite(state, FocusTime, pos); - - // mouse over / off animation - state = isHovered() ? EGBS_BUTTON_MOUSE_OVER : EGBS_BUTTON_MOUSE_OFF; - drawSprite(state, HoverTime, pos); - } - else - { - // draw disabled -// drawSprite(EGBS_BUTTON_DISABLED, 0, pos); - } - } - - IGUIElement::draw(); -} - -void GUIButton::drawSprite(EGUI_BUTTON_STATE state, u32 startTime, const core::position2di& center) -{ - u32 stateIdx = (u32)state; - - if (ButtonSprites[stateIdx].Index != -1) - { - if ( ButtonSprites[stateIdx].Scale ) - { - const video::SColor colors[] = {ButtonSprites[stateIdx].Color,ButtonSprites[stateIdx].Color,ButtonSprites[stateIdx].Color,ButtonSprites[stateIdx].Color}; - SpriteBank->draw2DSprite(ButtonSprites[stateIdx].Index, AbsoluteRect.UpperLeftCorner, - &AbsoluteClippingRect, colors[0], // FIXME: remove [0] - porting::getTimeMs()-startTime, ButtonSprites[stateIdx].Loop); - } - else - { - SpriteBank->draw2DSprite(ButtonSprites[stateIdx].Index, center, - &AbsoluteClippingRect, ButtonSprites[stateIdx].Color, startTime, porting::getTimeMs(), - ButtonSprites[stateIdx].Loop, true); - } - } -} - -EGUI_BUTTON_IMAGE_STATE GUIButton::getImageState(bool pressed) const -{ - // PATCH - return getImageState(pressed, ButtonImages); - // END PATCH -} - -EGUI_BUTTON_IMAGE_STATE GUIButton::getImageState(bool pressed, const ButtonImage* images) const -{ - // figure state we should have - EGUI_BUTTON_IMAGE_STATE state = EGBIS_IMAGE_DISABLED; - bool focused = isFocused(); - bool mouseOver = isHovered(); - if (isEnabled()) - { - if ( pressed ) - { - if ( focused && mouseOver ) - state = EGBIS_IMAGE_DOWN_FOCUSED_MOUSEOVER; - else if ( focused ) - state = EGBIS_IMAGE_DOWN_FOCUSED; - else if ( mouseOver ) - state = EGBIS_IMAGE_DOWN_MOUSEOVER; - else - state = EGBIS_IMAGE_DOWN; - } - else // !pressed - { - if ( focused && mouseOver ) - state = EGBIS_IMAGE_UP_FOCUSED_MOUSEOVER; - else if ( focused ) - state = EGBIS_IMAGE_UP_FOCUSED; - else if ( mouseOver ) - state = EGBIS_IMAGE_UP_MOUSEOVER; - else - state = EGBIS_IMAGE_UP; - } - } - - // find a compatible state that has images - while ( state != EGBIS_IMAGE_UP && !images[(u32)state].Texture ) - { - // PATCH - switch ( state ) - { - case EGBIS_IMAGE_UP_FOCUSED: - state = EGBIS_IMAGE_UP; - break; - case EGBIS_IMAGE_UP_FOCUSED_MOUSEOVER: - state = EGBIS_IMAGE_UP_FOCUSED; - break; - case EGBIS_IMAGE_DOWN_MOUSEOVER: - state = EGBIS_IMAGE_DOWN; - break; - case EGBIS_IMAGE_DOWN_FOCUSED: - state = EGBIS_IMAGE_DOWN; - break; - case EGBIS_IMAGE_DOWN_FOCUSED_MOUSEOVER: - state = EGBIS_IMAGE_DOWN_FOCUSED; - break; - case EGBIS_IMAGE_DISABLED: - if ( pressed ) - state = EGBIS_IMAGE_DOWN; - else - state = EGBIS_IMAGE_UP; - break; - default: - state = EGBIS_IMAGE_UP; - } - // END PATCH - } - - return state; -} - -//! sets another skin independent font. if this is set to zero, the button uses the font of the skin. -void GUIButton::setOverrideFont(IGUIFont* font) -{ - if (OverrideFont == font) - return; - - if (OverrideFont) - OverrideFont->drop(); - - OverrideFont = font; - - if (OverrideFont) - OverrideFont->grab(); - - StaticText->setOverrideFont(font); -} - -//! Gets the override font (if any) -IGUIFont * GUIButton::getOverrideFont() const -{ - return OverrideFont; -} - -//! Get the font which is used right now for drawing -IGUIFont* GUIButton::getActiveFont() const -{ - if ( OverrideFont ) - return OverrideFont; - IGUISkin* skin = Environment->getSkin(); - if (skin) - return skin->getFont(EGDF_BUTTON); - return 0; -} - -//! Sets another color for the text. -void GUIButton::setOverrideColor(video::SColor color) -{ - OverrideColor = color; - OverrideColorEnabled = true; - - StaticText->setOverrideColor(color); -} - -video::SColor GUIButton::getOverrideColor() const -{ - return OverrideColor; -} - -video::SColor GUIButton::getActiveColor() const -{ - return video::SColor(0,0,0,0); // unused? -} - -void GUIButton::enableOverrideColor(bool enable) -{ - OverrideColorEnabled = enable; -} - -bool GUIButton::isOverrideColorEnabled() const -{ - return OverrideColorEnabled; -} - -void GUIButton::setImage(EGUI_BUTTON_IMAGE_STATE state, video::ITexture* image, const core::rect& sourceRect) -{ - if ( state >= EGBIS_COUNT ) - return; - - if ( image ) - image->grab(); - - u32 stateIdx = (u32)state; - if ( ButtonImages[stateIdx].Texture ) - ButtonImages[stateIdx].Texture->drop(); - - ButtonImages[stateIdx].Texture = image; - ButtonImages[stateIdx].SourceRect = sourceRect; -} - -// PATCH -void GUIButton::setImage(video::ITexture* image) -{ - setImage(gui::EGBIS_IMAGE_UP, image); -} - -void GUIButton::setImage(video::ITexture* image, const core::rect& pos) -{ - setImage(gui::EGBIS_IMAGE_UP, image, pos); -} - -void GUIButton::setPressedImage(video::ITexture* image) -{ - setImage(gui::EGBIS_IMAGE_DOWN, image); -} - -void GUIButton::setPressedImage(video::ITexture* image, const core::rect& pos) -{ - setImage(gui::EGBIS_IMAGE_DOWN, image, pos); -} - -//! Sets the text displayed by the button -void GUIButton::setText(const wchar_t* text) -{ - StaticText->setText(text); - - IGUIButton::setText(text); -} -// END PATCH - -//! Sets if the button should behave like a push button. Which means it -//! can be in two states: Normal or Pressed. With a click on the button, -//! the user can change the state of the button. -void GUIButton::setIsPushButton(bool isPushButton) -{ - IsPushButton = isPushButton; -} - - -//! Returns if the button is currently pressed -bool GUIButton::isPressed() const -{ - return Pressed; -} - -// PATCH -//! Returns if this element (or one of its direct children) is hovered -bool GUIButton::isHovered() const -{ - IGUIElement *hovered = Environment->getHovered(); - return hovered == this || (hovered != nullptr && hovered->getParent() == this); -} - -//! Returns if this element (or one of its direct children) is focused -bool GUIButton::isFocused() const -{ - return Environment->hasFocus((IGUIElement*)this, true); -} -// END PATCH - -//! Sets the pressed state of the button if this is a pushbutton -void GUIButton::setPressed(bool pressed) -{ - if (Pressed != pressed) - { - ClickTime = porting::getTimeMs(); - Pressed = pressed; - setFromState(); - } -} - - -//! Returns whether the button is a push button -bool GUIButton::isPushButton() const -{ - return IsPushButton; -} - - -//! Sets if the alpha channel should be used for drawing images on the button (default is false) -void GUIButton::setUseAlphaChannel(bool useAlphaChannel) -{ - UseAlphaChannel = useAlphaChannel; -} - - -//! Returns if the alpha channel should be used for drawing images on the button -bool GUIButton::isAlphaChannelUsed() const -{ - return UseAlphaChannel; -} - - -bool GUIButton::isDrawingBorder() const -{ - return DrawBorder; -} - - -// PATCH -GUIButton* GUIButton::addButton(IGUIEnvironment *environment, - const core::rect& rectangle, ISimpleTextureSource *tsrc, - IGUIElement* parent, s32 id, const wchar_t* text, - const wchar_t *tooltiptext) -{ - GUIButton* button = new GUIButton(environment, parent ? parent : environment->getRootGUIElement(), id, rectangle, tsrc); - if (text) - button->setText(text); - - if ( tooltiptext ) - button->setToolTipText ( tooltiptext ); - - button->drop(); - return button; -} - -void GUIButton::setColor(video::SColor color) -{ - BgColor = color; - - float d = 0.65f; - for (size_t i = 0; i < 4; i++) { - video::SColor base = Environment->getSkin()->getColor((gui::EGUI_DEFAULT_COLOR)i); - Colors[i] = base.getInterpolated(color, d); - } -} - -//! Set element properties from a StyleSpec corresponding to the button state -void GUIButton::setFromState() -{ - StyleSpec::State state = StyleSpec::STATE_DEFAULT; - - if (isPressed()) - state = static_cast(state | StyleSpec::STATE_PRESSED); - - if (isHovered()) - state = static_cast(state | StyleSpec::STATE_HOVERED); - - if (isFocused()) - state = static_cast(state | StyleSpec::STATE_FOCUSED); - - setFromStyle(StyleSpec::getStyleFromStatePropagation(Styles, state)); -} - -//! Set element properties from a StyleSpec -void GUIButton::setFromStyle(const StyleSpec& style) -{ - bool hovered = (style.getState() & StyleSpec::STATE_HOVERED) != 0; - bool pressed = (style.getState() & StyleSpec::STATE_PRESSED) != 0; - - if (style.isNotDefault(StyleSpec::BGCOLOR)) { - setColor(style.getColor(StyleSpec::BGCOLOR)); - - // If we have a propagated hover/press color, we need to automatically - // lighten/darken it - if (!Styles[style.getState()].isNotDefault(StyleSpec::BGCOLOR)) { - if (pressed) { - BgColor = multiplyColorValue(BgColor, COLOR_PRESSED_MOD); - - for (size_t i = 0; i < 4; i++) - Colors[i] = multiplyColorValue(Colors[i], COLOR_PRESSED_MOD); - } else if (hovered) { - BgColor = multiplyColorValue(BgColor, COLOR_HOVERED_MOD); - - for (size_t i = 0; i < 4; i++) - Colors[i] = multiplyColorValue(Colors[i], COLOR_HOVERED_MOD); - } - } - - } else { - BgColor = video::SColor(255, 255, 255, 255); - for (size_t i = 0; i < 4; i++) { - video::SColor base = - Environment->getSkin()->getColor((gui::EGUI_DEFAULT_COLOR)i); - if (pressed) { - Colors[i] = multiplyColorValue(base, COLOR_PRESSED_MOD); - } else if (hovered) { - Colors[i] = multiplyColorValue(base, COLOR_HOVERED_MOD); - } else { - Colors[i] = base; - } - } - } - - if (style.isNotDefault(StyleSpec::TEXTCOLOR)) { - setOverrideColor(style.getColor(StyleSpec::TEXTCOLOR)); - } else { - setOverrideColor(video::SColor(255,255,255,255)); - OverrideColorEnabled = false; - } - setNotClipped(style.getBool(StyleSpec::NOCLIP, false)); - setDrawBorder(style.getBool(StyleSpec::BORDER, true)); - setUseAlphaChannel(style.getBool(StyleSpec::ALPHA, true)); - setOverrideFont(style.getFont()); - - if (style.isNotDefault(StyleSpec::BGIMG)) { - video::ITexture *texture = style.getTexture(StyleSpec::BGIMG, - getTextureSource()); - setImage(guiScalingImageButton( - Environment->getVideoDriver(), texture, - AbsoluteRect.getWidth(), AbsoluteRect.getHeight())); - setScaleImage(true); - } else { - setImage(nullptr); - } - - BgMiddle = style.getRect(StyleSpec::BGIMG_MIDDLE, BgMiddle); - - // Child padding and offset - Padding = style.getRect(StyleSpec::PADDING, core::rect()); - Padding = core::rect( - Padding.UpperLeftCorner + BgMiddle.UpperLeftCorner, - Padding.LowerRightCorner + BgMiddle.LowerRightCorner); - - GUISkin* skin = dynamic_cast(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)); - ContentOffset = style.getVector2i(StyleSpec::CONTENT_OFFSET, isPressed() - ? defaultPressOffset - : core::vector2d(0)); - - core::rect childBounds( - Padding.UpperLeftCorner.X + ContentOffset.X, - Padding.UpperLeftCorner.Y + ContentOffset.Y, - AbsoluteRect.getWidth() + Padding.LowerRightCorner.X + ContentOffset.X, - AbsoluteRect.getHeight() + Padding.LowerRightCorner.Y + ContentOffset.Y); - - for (IGUIElement *child : getChildren()) { - child->setRelativePosition(childBounds); - } -} - -//! Set the styles used for each state -void GUIButton::setStyles(const std::array& styles) -{ - Styles = styles; - setFromState(); -} -// END PATCH +// 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 + +#include "guiButton.h" + + +#include "client/guiscalingfilter.h" +#include "IGUISkin.h" +#include "IGUIEnvironment.h" +#include "IVideoDriver.h" +#include "IGUIFont.h" +#include "irrlicht_changes/static_text.h" +#include "porting.h" +#include "StyleSpec.h" +#include "util/numeric.h" + +using namespace irr; +using namespace gui; + +// Multiply with a color to get the default corresponding hovered color +#define COLOR_HOVERED_MOD 1.25f + +// Multiply with a color to get the default corresponding pressed color +#define COLOR_PRESSED_MOD 0.85f + +//! constructor +GUIButton::GUIButton(IGUIEnvironment* environment, IGUIElement* parent, + s32 id, core::rect rectangle, ISimpleTextureSource *tsrc, + bool noclip) : + IGUIButton(environment, parent, id, rectangle), + TSrc(tsrc) +{ + setNotClipped(noclip); + + // This element can be tabbed. + setTabStop(true); + setTabOrder(-1); + + // PATCH + for (size_t i = 0; i < 4; i++) { + Colors[i] = Environment->getSkin()->getColor((EGUI_DEFAULT_COLOR)i); + } + StaticText = gui::StaticText::add(Environment, Text.c_str(), core::rect(0,0,rectangle.getWidth(),rectangle.getHeight()), false, false, this, id); + StaticText->setTextAlignment(EGUIA_CENTER, EGUIA_CENTER); + // END PATCH +} + +//! destructor +GUIButton::~GUIButton() +{ + if (OverrideFont) + OverrideFont->drop(); + + if (SpriteBank) + SpriteBank->drop(); +} + + +//! Sets if the images should be scaled to fit the button +void GUIButton::setScaleImage(bool scaleImage) +{ + ScaleImage = scaleImage; +} + + +//! Returns whether the button scale the used images +bool GUIButton::isScalingImage() const +{ + return ScaleImage; +} + + +//! Sets if the button should use the skin to draw its border +void GUIButton::setDrawBorder(bool border) +{ + DrawBorder = border; +} + + +void GUIButton::setSpriteBank(IGUISpriteBank* sprites) +{ + if (sprites) + sprites->grab(); + + if (SpriteBank) + SpriteBank->drop(); + + SpriteBank = sprites; +} + +void GUIButton::setSprite(EGUI_BUTTON_STATE state, s32 index, video::SColor color, bool loop, bool scale) +{ + ButtonSprites[(u32)state].Index = index; + ButtonSprites[(u32)state].Color = color; + ButtonSprites[(u32)state].Loop = loop; + ButtonSprites[(u32)state].Scale = scale; +} + +//! Get the sprite-index for the given state or -1 when no sprite is set +s32 GUIButton::getSpriteIndex(EGUI_BUTTON_STATE state) const +{ + return ButtonSprites[(u32)state].Index; +} + +//! Get the sprite color for the given state. Color is only used when a sprite is set. +video::SColor GUIButton::getSpriteColor(EGUI_BUTTON_STATE state) const +{ + return ButtonSprites[(u32)state].Color; +} + +//! Returns if the sprite in the given state does loop +bool GUIButton::getSpriteLoop(EGUI_BUTTON_STATE state) const +{ + return ButtonSprites[(u32)state].Loop; +} + +//! Returns if the sprite in the given state is scaled +bool GUIButton::getSpriteScale(EGUI_BUTTON_STATE state) const +{ + return ButtonSprites[(u32)state].Scale; +} + +//! called if an event happened. +bool GUIButton::OnEvent(const SEvent& event) +{ + if (!isEnabled()) + return IGUIElement::OnEvent(event); + + switch(event.EventType) + { + case EET_KEY_INPUT_EVENT: + if (event.KeyInput.PressedDown && + (event.KeyInput.Key == KEY_RETURN || event.KeyInput.Key == KEY_SPACE)) + { + if (!IsPushButton) + setPressed(true); + else + setPressed(!Pressed); + + return true; + } + if (Pressed && !IsPushButton && event.KeyInput.PressedDown && event.KeyInput.Key == KEY_ESCAPE) + { + setPressed(false); + return true; + } + else + if (!event.KeyInput.PressedDown && Pressed && + (event.KeyInput.Key == KEY_RETURN || event.KeyInput.Key == KEY_SPACE)) + { + + if (!IsPushButton) + setPressed(false); + + if (Parent) + { + ClickShiftState = event.KeyInput.Shift; + ClickControlState = event.KeyInput.Control; + + SEvent newEvent; + newEvent.EventType = EET_GUI_EVENT; + newEvent.GUIEvent.Caller = this; + newEvent.GUIEvent.Element = 0; + newEvent.GUIEvent.EventType = EGET_BUTTON_CLICKED; + Parent->OnEvent(newEvent); + } + return true; + } + break; + case EET_GUI_EVENT: + if (event.GUIEvent.Caller == this) + { + if (event.GUIEvent.EventType == EGET_ELEMENT_FOCUS_LOST) + { + if (!IsPushButton) + setPressed(false); + FocusTime = (u32)porting::getTimeMs(); + } + else if (event.GUIEvent.EventType == EGET_ELEMENT_FOCUSED) + { + FocusTime = (u32)porting::getTimeMs(); + } + else if (event.GUIEvent.EventType == EGET_ELEMENT_HOVERED || event.GUIEvent.EventType == EGET_ELEMENT_LEFT) + { + HoverTime = (u32)porting::getTimeMs(); + } + } + break; + case EET_MOUSE_INPUT_EVENT: + if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) + { + // Sometimes formspec elements can receive mouse events when the + // mouse is outside of the formspec. Thus, we test the position here. + if ( !IsPushButton && AbsoluteClippingRect.isPointInside( + core::position2d(event.MouseInput.X, event.MouseInput.Y ))) { + Environment->setFocus(this); + setPressed(true); + } + + return true; + } + else + if (event.MouseInput.Event == EMIE_LMOUSE_LEFT_UP) + { + bool wasPressed = Pressed; + + if ( !AbsoluteClippingRect.isPointInside( core::position2d(event.MouseInput.X, event.MouseInput.Y ) ) ) + { + if (!IsPushButton) + setPressed(false); + return true; + } + + if (!IsPushButton) + setPressed(false); + else + { + setPressed(!Pressed); + } + + if ((!IsPushButton && wasPressed && Parent) || + (IsPushButton && wasPressed != Pressed)) + { + ClickShiftState = event.MouseInput.Shift; + ClickControlState = event.MouseInput.Control; + + SEvent newEvent; + newEvent.EventType = EET_GUI_EVENT; + newEvent.GUIEvent.Caller = this; + newEvent.GUIEvent.Element = 0; + newEvent.GUIEvent.EventType = EGET_BUTTON_CLICKED; + Parent->OnEvent(newEvent); + } + + return true; + } + break; + default: + break; + } + + return Parent ? Parent->OnEvent(event) : false; +} + + +//! draws the element and its children +void GUIButton::draw() +{ + if (!IsVisible) + return; + + // PATCH + // Track hovered state, if it has changed then we need to update the style. + bool hovered = isHovered(); + bool focused = isFocused(); + if (hovered != WasHovered || focused != WasFocused) { + WasHovered = hovered; + WasFocused = focused; + setFromState(); + } + + GUISkin* skin = dynamic_cast(Environment->getSkin()); + video::IVideoDriver* driver = Environment->getVideoDriver(); + // END PATCH + + if (DrawBorder) + { + if (!Pressed) + { + // PATCH + skin->drawColored3DButtonPaneStandard(this, AbsoluteRect, + &AbsoluteClippingRect, Colors); + // END PATCH + } + else + { + // PATCH + skin->drawColored3DButtonPanePressed(this, AbsoluteRect, + &AbsoluteClippingRect, Colors); + // END PATCH + } + } + + const core::position2di buttonCenter(AbsoluteRect.getCenter()); + // PATCH + // The image changes based on the state, so we use the default every time. + EGUI_BUTTON_IMAGE_STATE imageState = EGBIS_IMAGE_UP; + // END PATCH + if ( ButtonImages[(u32)imageState].Texture ) + { + core::position2d pos(buttonCenter); + core::rect sourceRect(ButtonImages[(u32)imageState].SourceRect); + if ( sourceRect.getWidth() == 0 && sourceRect.getHeight() == 0 ) + sourceRect = core::rect(core::position2di(0,0), ButtonImages[(u32)imageState].Texture->getOriginalSize()); + + pos.X -= sourceRect.getWidth() / 2; + pos.Y -= sourceRect.getHeight() / 2; + + if ( Pressed ) + { + // Create a pressed-down effect by moving the image when it looks identical to the unpressed state image + EGUI_BUTTON_IMAGE_STATE unpressedState = getImageState(false); + if ( unpressedState == imageState || ButtonImages[(u32)imageState] == ButtonImages[(u32)unpressedState] ) + { + pos.X += skin->getSize(EGDS_BUTTON_PRESSED_IMAGE_OFFSET_X); + pos.Y += skin->getSize(EGDS_BUTTON_PRESSED_IMAGE_OFFSET_Y); + } + } + + // PATCH + video::ITexture* texture = ButtonImages[(u32)imageState].Texture; + video::SColor image_colors[] = { BgColor, BgColor, BgColor, BgColor }; + if (BgMiddle.getArea() == 0) { + driver->draw2DImage(texture, + ScaleImage? AbsoluteRect : core::rect(pos, sourceRect.getSize()), + sourceRect, &AbsoluteClippingRect, + image_colors, UseAlphaChannel); + } else { + draw2DImage9Slice(driver, texture, + ScaleImage ? AbsoluteRect : core::rect(pos, sourceRect.getSize()), + sourceRect, BgMiddle, &AbsoluteClippingRect, image_colors); + } + // END PATCH + } + + if (SpriteBank) + { + if (isEnabled()) + { + core::position2di pos(buttonCenter); + // pressed / unpressed animation + EGUI_BUTTON_STATE state = Pressed ? EGBS_BUTTON_DOWN : EGBS_BUTTON_UP; + drawSprite(state, ClickTime, pos); + + // focused / unfocused animation + state = Environment->hasFocus(this) ? EGBS_BUTTON_FOCUSED : EGBS_BUTTON_NOT_FOCUSED; + drawSprite(state, FocusTime, pos); + + // mouse over / off animation + state = isHovered() ? EGBS_BUTTON_MOUSE_OVER : EGBS_BUTTON_MOUSE_OFF; + drawSprite(state, HoverTime, pos); + } + else + { + // draw disabled +// drawSprite(EGBS_BUTTON_DISABLED, 0, pos); + } + } + + IGUIElement::draw(); +} + +void GUIButton::drawSprite(EGUI_BUTTON_STATE state, u32 startTime, const core::position2di& center) +{ + u32 stateIdx = (u32)state; + + if (ButtonSprites[stateIdx].Index != -1) + { + if ( ButtonSprites[stateIdx].Scale ) + { + const video::SColor colors[] = {ButtonSprites[stateIdx].Color,ButtonSprites[stateIdx].Color,ButtonSprites[stateIdx].Color,ButtonSprites[stateIdx].Color}; + SpriteBank->draw2DSprite(ButtonSprites[stateIdx].Index, AbsoluteRect.UpperLeftCorner, + &AbsoluteClippingRect, colors[0], // FIXME: remove [0] + porting::getTimeMs()-startTime, ButtonSprites[stateIdx].Loop); + } + else + { + SpriteBank->draw2DSprite(ButtonSprites[stateIdx].Index, center, + &AbsoluteClippingRect, ButtonSprites[stateIdx].Color, startTime, porting::getTimeMs(), + ButtonSprites[stateIdx].Loop, true); + } + } +} + +EGUI_BUTTON_IMAGE_STATE GUIButton::getImageState(bool pressed) const +{ + // PATCH + return getImageState(pressed, ButtonImages); + // END PATCH +} + +EGUI_BUTTON_IMAGE_STATE GUIButton::getImageState(bool pressed, const ButtonImage* images) const +{ + // figure state we should have + EGUI_BUTTON_IMAGE_STATE state = EGBIS_IMAGE_DISABLED; + bool focused = isFocused(); + bool mouseOver = isHovered(); + if (isEnabled()) + { + if ( pressed ) + { + if ( focused && mouseOver ) + state = EGBIS_IMAGE_DOWN_FOCUSED_MOUSEOVER; + else if ( focused ) + state = EGBIS_IMAGE_DOWN_FOCUSED; + else if ( mouseOver ) + state = EGBIS_IMAGE_DOWN_MOUSEOVER; + else + state = EGBIS_IMAGE_DOWN; + } + else // !pressed + { + if ( focused && mouseOver ) + state = EGBIS_IMAGE_UP_FOCUSED_MOUSEOVER; + else if ( focused ) + state = EGBIS_IMAGE_UP_FOCUSED; + else if ( mouseOver ) + state = EGBIS_IMAGE_UP_MOUSEOVER; + else + state = EGBIS_IMAGE_UP; + } + } + + // find a compatible state that has images + while ( state != EGBIS_IMAGE_UP && !images[(u32)state].Texture ) + { + // PATCH + switch ( state ) + { + case EGBIS_IMAGE_UP_FOCUSED: + state = EGBIS_IMAGE_UP; + break; + case EGBIS_IMAGE_UP_FOCUSED_MOUSEOVER: + state = EGBIS_IMAGE_UP_FOCUSED; + break; + case EGBIS_IMAGE_DOWN_MOUSEOVER: + state = EGBIS_IMAGE_DOWN; + break; + case EGBIS_IMAGE_DOWN_FOCUSED: + state = EGBIS_IMAGE_DOWN; + break; + case EGBIS_IMAGE_DOWN_FOCUSED_MOUSEOVER: + state = EGBIS_IMAGE_DOWN_FOCUSED; + break; + case EGBIS_IMAGE_DISABLED: + if ( pressed ) + state = EGBIS_IMAGE_DOWN; + else + state = EGBIS_IMAGE_UP; + break; + default: + state = EGBIS_IMAGE_UP; + } + // END PATCH + } + + return state; +} + +//! sets another skin independent font. if this is set to zero, the button uses the font of the skin. +void GUIButton::setOverrideFont(IGUIFont* font) +{ + if (OverrideFont == font) + return; + + if (OverrideFont) + OverrideFont->drop(); + + OverrideFont = font; + + if (OverrideFont) + OverrideFont->grab(); + + StaticText->setOverrideFont(font); +} + +//! Gets the override font (if any) +IGUIFont * GUIButton::getOverrideFont() const +{ + return OverrideFont; +} + +//! Get the font which is used right now for drawing +IGUIFont* GUIButton::getActiveFont() const +{ + if ( OverrideFont ) + return OverrideFont; + IGUISkin* skin = Environment->getSkin(); + if (skin) + return skin->getFont(EGDF_BUTTON); + return 0; +} + +//! Sets another color for the text. +void GUIButton::setOverrideColor(video::SColor color) +{ + OverrideColor = color; + OverrideColorEnabled = true; + + StaticText->setOverrideColor(color); +} + +video::SColor GUIButton::getOverrideColor() const +{ + return OverrideColor; +} + +video::SColor GUIButton::getActiveColor() const +{ + return video::SColor(0,0,0,0); // unused? +} + +void GUIButton::enableOverrideColor(bool enable) +{ + OverrideColorEnabled = enable; +} + +bool GUIButton::isOverrideColorEnabled() const +{ + return OverrideColorEnabled; +} + +void GUIButton::setImage(EGUI_BUTTON_IMAGE_STATE state, video::ITexture* image, const core::rect& sourceRect) +{ + if ( state >= EGBIS_COUNT ) + return; + + if ( image ) + image->grab(); + + u32 stateIdx = (u32)state; + if ( ButtonImages[stateIdx].Texture ) + ButtonImages[stateIdx].Texture->drop(); + + ButtonImages[stateIdx].Texture = image; + ButtonImages[stateIdx].SourceRect = sourceRect; +} + +// PATCH +void GUIButton::setImage(video::ITexture* image) +{ + setImage(gui::EGBIS_IMAGE_UP, image); +} + +void GUIButton::setImage(video::ITexture* image, const core::rect& pos) +{ + setImage(gui::EGBIS_IMAGE_UP, image, pos); +} + +void GUIButton::setPressedImage(video::ITexture* image) +{ + setImage(gui::EGBIS_IMAGE_DOWN, image); +} + +void GUIButton::setPressedImage(video::ITexture* image, const core::rect& pos) +{ + setImage(gui::EGBIS_IMAGE_DOWN, image, pos); +} + +//! Sets the text displayed by the button +void GUIButton::setText(const wchar_t* text) +{ + StaticText->setText(text); + + IGUIButton::setText(text); +} +// END PATCH + +//! Sets if the button should behave like a push button. Which means it +//! can be in two states: Normal or Pressed. With a click on the button, +//! the user can change the state of the button. +void GUIButton::setIsPushButton(bool isPushButton) +{ + IsPushButton = isPushButton; +} + + +//! Returns if the button is currently pressed +bool GUIButton::isPressed() const +{ + return Pressed; +} + +// PATCH +//! Returns if this element (or one of its direct children) is hovered +bool GUIButton::isHovered() const +{ + IGUIElement *hovered = Environment->getHovered(); + return hovered == this || (hovered != nullptr && hovered->getParent() == this); +} + +//! Returns if this element (or one of its direct children) is focused +bool GUIButton::isFocused() const +{ + return Environment->hasFocus((IGUIElement*)this, true); +} +// END PATCH + +//! Sets the pressed state of the button if this is a pushbutton +void GUIButton::setPressed(bool pressed) +{ + if (Pressed != pressed) + { + ClickTime = porting::getTimeMs(); + Pressed = pressed; + setFromState(); + } +} + + +//! Returns whether the button is a push button +bool GUIButton::isPushButton() const +{ + return IsPushButton; +} + + +//! Sets if the alpha channel should be used for drawing images on the button (default is false) +void GUIButton::setUseAlphaChannel(bool useAlphaChannel) +{ + UseAlphaChannel = useAlphaChannel; +} + + +//! Returns if the alpha channel should be used for drawing images on the button +bool GUIButton::isAlphaChannelUsed() const +{ + return UseAlphaChannel; +} + + +bool GUIButton::isDrawingBorder() const +{ + return DrawBorder; +} + + +// PATCH +GUIButton* GUIButton::addButton(IGUIEnvironment *environment, + const core::rect& rectangle, ISimpleTextureSource *tsrc, + IGUIElement* parent, s32 id, const wchar_t* text, + const wchar_t *tooltiptext) +{ + GUIButton* button = new GUIButton(environment, parent ? parent : environment->getRootGUIElement(), id, rectangle, tsrc); + if (text) + button->setText(text); + + if ( tooltiptext ) + button->setToolTipText ( tooltiptext ); + + button->drop(); + return button; +} + +void GUIButton::setColor(video::SColor color) +{ + BgColor = color; + + float d = 0.65f; + for (size_t i = 0; i < 4; i++) { + video::SColor base = Environment->getSkin()->getColor((gui::EGUI_DEFAULT_COLOR)i); + Colors[i] = base.getInterpolated(color, d); + } +} + +//! Set element properties from a StyleSpec corresponding to the button state +void GUIButton::setFromState() +{ + StyleSpec::State state = StyleSpec::STATE_DEFAULT; + + if (isPressed()) + state = static_cast(state | StyleSpec::STATE_PRESSED); + + if (isHovered()) + state = static_cast(state | StyleSpec::STATE_HOVERED); + + if (isFocused()) + state = static_cast(state | StyleSpec::STATE_FOCUSED); + + setFromStyle(StyleSpec::getStyleFromStatePropagation(Styles, state)); +} + +//! Set element properties from a StyleSpec +void GUIButton::setFromStyle(const StyleSpec& style) +{ + bool hovered = (style.getState() & StyleSpec::STATE_HOVERED) != 0; + bool pressed = (style.getState() & StyleSpec::STATE_PRESSED) != 0; + + if (style.isNotDefault(StyleSpec::BGCOLOR)) { + setColor(style.getColor(StyleSpec::BGCOLOR)); + + // If we have a propagated hover/press color, we need to automatically + // lighten/darken it + if (!Styles[style.getState()].isNotDefault(StyleSpec::BGCOLOR)) { + if (pressed) { + BgColor = multiplyColorValue(BgColor, COLOR_PRESSED_MOD); + + for (size_t i = 0; i < 4; i++) + Colors[i] = multiplyColorValue(Colors[i], COLOR_PRESSED_MOD); + } else if (hovered) { + BgColor = multiplyColorValue(BgColor, COLOR_HOVERED_MOD); + + for (size_t i = 0; i < 4; i++) + Colors[i] = multiplyColorValue(Colors[i], COLOR_HOVERED_MOD); + } + } + + } else { + BgColor = video::SColor(255, 255, 255, 255); + for (size_t i = 0; i < 4; i++) { + video::SColor base = + Environment->getSkin()->getColor((gui::EGUI_DEFAULT_COLOR)i); + if (pressed) { + Colors[i] = multiplyColorValue(base, COLOR_PRESSED_MOD); + } else if (hovered) { + Colors[i] = multiplyColorValue(base, COLOR_HOVERED_MOD); + } else { + Colors[i] = base; + } + } + } + + if (style.isNotDefault(StyleSpec::TEXTCOLOR)) { + setOverrideColor(style.getColor(StyleSpec::TEXTCOLOR)); + } else { + setOverrideColor(video::SColor(255,255,255,255)); + OverrideColorEnabled = false; + } + setNotClipped(style.getBool(StyleSpec::NOCLIP, false)); + setDrawBorder(style.getBool(StyleSpec::BORDER, true)); + setUseAlphaChannel(style.getBool(StyleSpec::ALPHA, true)); + setOverrideFont(style.getFont()); + + if (style.isNotDefault(StyleSpec::BGIMG)) { + video::ITexture *texture = style.getTexture(StyleSpec::BGIMG, + getTextureSource()); + setImage(guiScalingImageButton( + Environment->getVideoDriver(), texture, + AbsoluteRect.getWidth(), AbsoluteRect.getHeight())); + setScaleImage(true); + } else { + setImage(nullptr); + } + + BgMiddle = style.getRect(StyleSpec::BGIMG_MIDDLE, BgMiddle); + + // Child padding and offset + Padding = style.getRect(StyleSpec::PADDING, core::rect()); + Padding = core::rect( + Padding.UpperLeftCorner + BgMiddle.UpperLeftCorner, + Padding.LowerRightCorner + BgMiddle.LowerRightCorner); + + GUISkin* skin = dynamic_cast(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)); + ContentOffset = style.getVector2i(StyleSpec::CONTENT_OFFSET, isPressed() + ? defaultPressOffset + : core::vector2d(0)); + + core::rect childBounds( + Padding.UpperLeftCorner.X + ContentOffset.X, + Padding.UpperLeftCorner.Y + ContentOffset.Y, + AbsoluteRect.getWidth() + Padding.LowerRightCorner.X + ContentOffset.X, + AbsoluteRect.getHeight() + Padding.LowerRightCorner.Y + ContentOffset.Y); + + for (IGUIElement *child : getChildren()) { + child->setRelativePosition(childBounds); + } +} + +//! Set the styles used for each state +void GUIButton::setStyles(const std::array& styles) +{ + Styles = styles; + setFromState(); +} +// END PATCH diff --git a/src/gui/guiButton.h b/src/gui/guiButton.h index b0fc4b647..c96f1ca4e 100644 --- a/src/gui/guiButton.h +++ b/src/gui/guiButton.h @@ -1,280 +1,280 @@ -// 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 "irrlicht_changes/static_text.h" -#include "IGUIButton.h" -#include "IGUISpriteBank.h" -#include "ITexture.h" -#include "SColor.h" -#include "guiSkin.h" -#include "StyleSpec.h" - -using namespace irr; - -class ISimpleTextureSource; - -class GUIButton : public gui::IGUIButton -{ -public: - - //! constructor - GUIButton(gui::IGUIEnvironment* environment, gui::IGUIElement* parent, - s32 id, core::rect rectangle, ISimpleTextureSource *tsrc, - bool noclip=false); - - //! destructor - virtual ~GUIButton(); - - //! called if an event happened. - virtual bool OnEvent(const SEvent& event) override; - - //! draws the element and its children - virtual void draw() override; - - //! sets another skin independent font. if this is set to zero, the button uses the font of the skin. - virtual void setOverrideFont(gui::IGUIFont* font=0) override; - - //! Gets the override font (if any) - virtual gui::IGUIFont* getOverrideFont() const override; - - //! Get the font which is used right now for drawing - virtual gui::IGUIFont* getActiveFont() const override; - - //! Sets another color for the button text. - virtual void setOverrideColor(video::SColor color) override; - - //! Gets the override color - virtual video::SColor getOverrideColor() const override; - - //! Gets the currently used text color - virtual video::SColor getActiveColor() const override; - - //! Sets if the button text should use the override color or the color in the gui skin. - virtual void enableOverrideColor(bool enable) override; - - //! Checks if an override color is enabled - virtual bool isOverrideColorEnabled(void) const override; - - // PATCH - //! Sets an image which should be displayed on the button when it is in the given state. - virtual void setImage(gui::EGUI_BUTTON_IMAGE_STATE state, - video::ITexture* image=nullptr, - const core::rect& sourceRect=core::rect(0,0,0,0)) override; - - //! Sets an image which should be displayed on the button when it is in normal state. - virtual void setImage(video::ITexture* image=nullptr) override; - - //! Sets an image which should be displayed on the button when it is in normal state. - virtual void setImage(video::ITexture* image, const core::rect& pos) override; - - //! Sets an image which should be displayed on the button when it is in pressed state. - virtual void setPressedImage(video::ITexture* image=nullptr) override; - - //! Sets an image which should be displayed on the button when it is in pressed state. - virtual void setPressedImage(video::ITexture* image, const core::rect& pos) override; - - //! Sets the text displayed by the button - virtual void setText(const wchar_t* text) override; - // END PATCH - - //! Sets the sprite bank used by the button - virtual void setSpriteBank(gui::IGUISpriteBank* bank=0) override; - - //! Sets the animated sprite for a specific button state - /** \param index: Number of the sprite within the sprite bank, use -1 for no sprite - \param state: State of the button to set the sprite for - \param index: The sprite number from the current sprite bank - \param color: The color of the sprite - */ - virtual void setSprite(gui::EGUI_BUTTON_STATE state, s32 index, - video::SColor color=video::SColor(255,255,255,255), - bool loop=false, bool scale=false) override; - - //! Get the sprite-index for the given state or -1 when no sprite is set - virtual s32 getSpriteIndex(gui::EGUI_BUTTON_STATE state) const override; - - //! Get the sprite color for the given state. Color is only used when a sprite is set. - virtual video::SColor getSpriteColor(gui::EGUI_BUTTON_STATE state) const override; - - //! Returns if the sprite in the given state does loop - virtual bool getSpriteLoop(gui::EGUI_BUTTON_STATE state) const override; - - //! Returns if the sprite in the given state is scaled - virtual bool getSpriteScale(gui::EGUI_BUTTON_STATE state) const override; - - //! Sets if the button should behave like a push button. Which means it - //! can be in two states: Normal or Pressed. With a click on the button, - //! the user can change the state of the button. - virtual void setIsPushButton(bool isPushButton=true) override; - - //! Checks whether the button is a push button - virtual bool isPushButton() const override; - - //! Sets the pressed state of the button if this is a pushbutton - virtual void setPressed(bool pressed=true) override; - - //! Returns if the button is currently pressed - virtual bool isPressed() const override; - - // PATCH - //! Returns if this element (or one of its direct children) is hovered - bool isHovered() const; - - //! Returns if this element (or one of its direct children) is focused - bool isFocused() const; - // END PATCH - - //! Sets if the button should use the skin to draw its border - virtual void setDrawBorder(bool border=true) override; - - //! Checks if the button face and border are being drawn - virtual bool isDrawingBorder() const override; - - //! Sets if the alpha channel should be used for drawing images on the button (default is false) - virtual void setUseAlphaChannel(bool useAlphaChannel=true) override; - - //! Checks if the alpha channel should be used for drawing images on the button - virtual bool isAlphaChannelUsed() const override; - - //! Sets if the button should scale the button images to fit - virtual void setScaleImage(bool scaleImage=true) override; - - //! Checks whether the button scales the used images - virtual bool isScalingImage() const override; - - //! Get if the shift key was pressed in last EGET_BUTTON_CLICKED event - virtual bool getClickShiftState() const override - { - return ClickShiftState; - } - - //! Get if the control key was pressed in last EGET_BUTTON_CLICKED event - virtual bool getClickControlState() const override - { - return ClickControlState; - } - - void setColor(video::SColor color); - // PATCH - //! Set element properties from a StyleSpec corresponding to the button state - void setFromState(); - - //! Set element properties from a StyleSpec - virtual void setFromStyle(const StyleSpec& style); - - //! Set the styles used for each state - void setStyles(const std::array& styles); - // END PATCH - - - //! Do not drop returned handle - static GUIButton* addButton(gui::IGUIEnvironment *environment, - const core::rect& rectangle, ISimpleTextureSource *tsrc, - IGUIElement* parent, s32 id, const wchar_t* text, - const wchar_t *tooltiptext=L""); - -protected: - void drawSprite(gui::EGUI_BUTTON_STATE state, u32 startTime, const core::position2di& center); - gui::EGUI_BUTTON_IMAGE_STATE getImageState(bool pressed) const; - - ISimpleTextureSource *getTextureSource() { return TSrc; } - - struct ButtonImage - { - ButtonImage() = default; - - ButtonImage(const ButtonImage& other) - { - *this = other; - } - - ~ButtonImage() - { - if ( Texture ) - Texture->drop(); - } - - ButtonImage& operator=(const ButtonImage& other) - { - if ( this == &other ) - return *this; - - if (other.Texture) - other.Texture->grab(); - if ( Texture ) - Texture->drop(); - Texture = other.Texture; - SourceRect = other.SourceRect; - return *this; - } - - bool operator==(const ButtonImage& other) const - { - return Texture == other.Texture && SourceRect == other.SourceRect; - } - - - video::ITexture* Texture = nullptr; - core::rect SourceRect = core::rect(0,0,0,0); - }; - - gui::EGUI_BUTTON_IMAGE_STATE getImageState(bool pressed, const ButtonImage* images) const; - -private: - - struct ButtonSprite - { - bool operator==(const ButtonSprite &other) const - { - return Index == other.Index && Color == other.Color && Loop == other.Loop && Scale == other.Scale; - } - - s32 Index = -1; - video::SColor Color; - bool Loop = false; - bool Scale = false; - }; - - ButtonSprite ButtonSprites[gui::EGBS_COUNT]; - gui::IGUISpriteBank* SpriteBank = nullptr; - - ButtonImage ButtonImages[gui::EGBIS_COUNT]; - - std::array Styles; - - gui::IGUIFont* OverrideFont = nullptr; - - bool OverrideColorEnabled = false; - video::SColor OverrideColor = video::SColor(101,255,255,255); - - u32 ClickTime = 0; - u32 HoverTime = 0; - u32 FocusTime = 0; - - bool ClickShiftState = false; - bool ClickControlState = false; - - bool IsPushButton = false; - bool Pressed = false; - bool UseAlphaChannel = false; - bool DrawBorder = true; - bool ScaleImage = false; - - video::SColor Colors[4]; - // PATCH - bool WasHovered = false; - bool WasFocused = false; - ISimpleTextureSource *TSrc; - - gui::IGUIStaticText *StaticText; - - core::rect BgMiddle; - core::rect Padding; - core::vector2d ContentOffset; - video::SColor BgColor = video::SColor(0xFF,0xFF,0xFF,0xFF); - // END PATCH -}; +// 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 "irrlicht_changes/static_text.h" +#include "IGUIButton.h" +#include "IGUISpriteBank.h" +#include "ITexture.h" +#include "SColor.h" +#include "guiSkin.h" +#include "StyleSpec.h" + +using namespace irr; + +class ISimpleTextureSource; + +class GUIButton : public gui::IGUIButton +{ +public: + + //! constructor + GUIButton(gui::IGUIEnvironment* environment, gui::IGUIElement* parent, + s32 id, core::rect rectangle, ISimpleTextureSource *tsrc, + bool noclip=false); + + //! destructor + virtual ~GUIButton(); + + //! called if an event happened. + virtual bool OnEvent(const SEvent& event) override; + + //! draws the element and its children + virtual void draw() override; + + //! sets another skin independent font. if this is set to zero, the button uses the font of the skin. + virtual void setOverrideFont(gui::IGUIFont* font=0) override; + + //! Gets the override font (if any) + virtual gui::IGUIFont* getOverrideFont() const override; + + //! Get the font which is used right now for drawing + virtual gui::IGUIFont* getActiveFont() const override; + + //! Sets another color for the button text. + virtual void setOverrideColor(video::SColor color) override; + + //! Gets the override color + virtual video::SColor getOverrideColor() const override; + + //! Gets the currently used text color + virtual video::SColor getActiveColor() const override; + + //! Sets if the button text should use the override color or the color in the gui skin. + virtual void enableOverrideColor(bool enable) override; + + //! Checks if an override color is enabled + virtual bool isOverrideColorEnabled(void) const override; + + // PATCH + //! Sets an image which should be displayed on the button when it is in the given state. + virtual void setImage(gui::EGUI_BUTTON_IMAGE_STATE state, + video::ITexture* image=nullptr, + const core::rect& sourceRect=core::rect(0,0,0,0)) override; + + //! Sets an image which should be displayed on the button when it is in normal state. + virtual void setImage(video::ITexture* image=nullptr) override; + + //! Sets an image which should be displayed on the button when it is in normal state. + virtual void setImage(video::ITexture* image, const core::rect& pos) override; + + //! Sets an image which should be displayed on the button when it is in pressed state. + virtual void setPressedImage(video::ITexture* image=nullptr) override; + + //! Sets an image which should be displayed on the button when it is in pressed state. + virtual void setPressedImage(video::ITexture* image, const core::rect& pos) override; + + //! Sets the text displayed by the button + virtual void setText(const wchar_t* text) override; + // END PATCH + + //! Sets the sprite bank used by the button + virtual void setSpriteBank(gui::IGUISpriteBank* bank=0) override; + + //! Sets the animated sprite for a specific button state + /** \param index: Number of the sprite within the sprite bank, use -1 for no sprite + \param state: State of the button to set the sprite for + \param index: The sprite number from the current sprite bank + \param color: The color of the sprite + */ + virtual void setSprite(gui::EGUI_BUTTON_STATE state, s32 index, + video::SColor color=video::SColor(255,255,255,255), + bool loop=false, bool scale=false) override; + + //! Get the sprite-index for the given state or -1 when no sprite is set + virtual s32 getSpriteIndex(gui::EGUI_BUTTON_STATE state) const override; + + //! Get the sprite color for the given state. Color is only used when a sprite is set. + virtual video::SColor getSpriteColor(gui::EGUI_BUTTON_STATE state) const override; + + //! Returns if the sprite in the given state does loop + virtual bool getSpriteLoop(gui::EGUI_BUTTON_STATE state) const override; + + //! Returns if the sprite in the given state is scaled + virtual bool getSpriteScale(gui::EGUI_BUTTON_STATE state) const override; + + //! Sets if the button should behave like a push button. Which means it + //! can be in two states: Normal or Pressed. With a click on the button, + //! the user can change the state of the button. + virtual void setIsPushButton(bool isPushButton=true) override; + + //! Checks whether the button is a push button + virtual bool isPushButton() const override; + + //! Sets the pressed state of the button if this is a pushbutton + virtual void setPressed(bool pressed=true) override; + + //! Returns if the button is currently pressed + virtual bool isPressed() const override; + + // PATCH + //! Returns if this element (or one of its direct children) is hovered + bool isHovered() const; + + //! Returns if this element (or one of its direct children) is focused + bool isFocused() const; + // END PATCH + + //! Sets if the button should use the skin to draw its border + virtual void setDrawBorder(bool border=true) override; + + //! Checks if the button face and border are being drawn + virtual bool isDrawingBorder() const override; + + //! Sets if the alpha channel should be used for drawing images on the button (default is false) + virtual void setUseAlphaChannel(bool useAlphaChannel=true) override; + + //! Checks if the alpha channel should be used for drawing images on the button + virtual bool isAlphaChannelUsed() const override; + + //! Sets if the button should scale the button images to fit + virtual void setScaleImage(bool scaleImage=true) override; + + //! Checks whether the button scales the used images + virtual bool isScalingImage() const override; + + //! Get if the shift key was pressed in last EGET_BUTTON_CLICKED event + virtual bool getClickShiftState() const override + { + return ClickShiftState; + } + + //! Get if the control key was pressed in last EGET_BUTTON_CLICKED event + virtual bool getClickControlState() const override + { + return ClickControlState; + } + + void setColor(video::SColor color); + // PATCH + //! Set element properties from a StyleSpec corresponding to the button state + void setFromState(); + + //! Set element properties from a StyleSpec + virtual void setFromStyle(const StyleSpec& style); + + //! Set the styles used for each state + void setStyles(const std::array& styles); + // END PATCH + + + //! Do not drop returned handle + static GUIButton* addButton(gui::IGUIEnvironment *environment, + const core::rect& rectangle, ISimpleTextureSource *tsrc, + IGUIElement* parent, s32 id, const wchar_t* text, + const wchar_t *tooltiptext=L""); + +protected: + void drawSprite(gui::EGUI_BUTTON_STATE state, u32 startTime, const core::position2di& center); + gui::EGUI_BUTTON_IMAGE_STATE getImageState(bool pressed) const; + + ISimpleTextureSource *getTextureSource() { return TSrc; } + + struct ButtonImage + { + ButtonImage() = default; + + ButtonImage(const ButtonImage& other) + { + *this = other; + } + + ~ButtonImage() + { + if ( Texture ) + Texture->drop(); + } + + ButtonImage& operator=(const ButtonImage& other) + { + if ( this == &other ) + return *this; + + if (other.Texture) + other.Texture->grab(); + if ( Texture ) + Texture->drop(); + Texture = other.Texture; + SourceRect = other.SourceRect; + return *this; + } + + bool operator==(const ButtonImage& other) const + { + return Texture == other.Texture && SourceRect == other.SourceRect; + } + + + video::ITexture* Texture = nullptr; + core::rect SourceRect = core::rect(0,0,0,0); + }; + + gui::EGUI_BUTTON_IMAGE_STATE getImageState(bool pressed, const ButtonImage* images) const; + +private: + + struct ButtonSprite + { + bool operator==(const ButtonSprite &other) const + { + return Index == other.Index && Color == other.Color && Loop == other.Loop && Scale == other.Scale; + } + + s32 Index = -1; + video::SColor Color; + bool Loop = false; + bool Scale = false; + }; + + ButtonSprite ButtonSprites[gui::EGBS_COUNT]; + gui::IGUISpriteBank* SpriteBank = nullptr; + + ButtonImage ButtonImages[gui::EGBIS_COUNT]; + + std::array Styles; + + gui::IGUIFont* OverrideFont = nullptr; + + bool OverrideColorEnabled = false; + video::SColor OverrideColor = video::SColor(101,255,255,255); + + u32 ClickTime = 0; + u32 HoverTime = 0; + u32 FocusTime = 0; + + bool ClickShiftState = false; + bool ClickControlState = false; + + bool IsPushButton = false; + bool Pressed = false; + bool UseAlphaChannel = false; + bool DrawBorder = true; + bool ScaleImage = false; + + video::SColor Colors[4]; + // PATCH + bool WasHovered = false; + bool WasFocused = false; + ISimpleTextureSource *TSrc; + + gui::IGUIStaticText *StaticText; + + core::rect BgMiddle; + core::rect Padding; + core::vector2d ContentOffset; + video::SColor BgColor = video::SColor(0xFF,0xFF,0xFF,0xFF); + // END PATCH +}; diff --git a/src/gui/guiSkin.cpp b/src/gui/guiSkin.cpp index 5462a2fec..0ecc80f02 100644 --- a/src/gui/guiSkin.cpp +++ b/src/gui/guiSkin.cpp @@ -1,1037 +1,1037 @@ -// 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; 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 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: - - 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 +// 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 + +#ifndef __GUI_SKIN_H_INCLUDED__ +#define __GUI_SKIN_H_INCLUDED__ + +#include "IGUISkin.h" +#include "irrString.h" +#include +#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 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: + + 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 From d4b10db998ebeb689b3d27368e30952a42169d03 Mon Sep 17 00:00:00 2001 From: cx384 Date: Sat, 23 Mar 2024 17:56:54 +0100 Subject: [PATCH 15/34] Add LF config to .gitattributes --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitattributes b/.gitattributes index 2e62a4e59..06b76c6c8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,5 @@ +# Forces all files which git considers text files to use LF line endings +* text=auto eol=lf + *.cpp diff=cpp *.h diff=cpp From eb8785a20990a7b35e82fcfcbe4d3c046fabe8c5 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Tue, 2 Apr 2024 21:25:32 +0200 Subject: [PATCH 16/34] Fix segfault if SDL GL context fails --- irr/src/CIrrDeviceSDL.cpp | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/irr/src/CIrrDeviceSDL.cpp b/irr/src/CIrrDeviceSDL.cpp index 17dc033d4..2609058a6 100644 --- a/irr/src/CIrrDeviceSDL.cpp +++ b/irr/src/CIrrDeviceSDL.cpp @@ -321,19 +321,21 @@ CIrrDeviceSDL::CIrrDeviceSDL(const SIrrlichtCreationParameters ¶m) : //! destructor CIrrDeviceSDL::~CIrrDeviceSDL() { - if (--SDLDeviceInstances == 0) { #if defined(_IRR_COMPILE_WITH_JOYSTICK_EVENTS_) - const u32 numJoysticks = Joysticks.size(); - for (u32 i = 0; i < numJoysticks; ++i) - SDL_JoystickClose(Joysticks[i]); + const u32 numJoysticks = Joysticks.size(); + for (u32 i = 0; i < numJoysticks; ++i) + SDL_JoystickClose(Joysticks[i]); #endif - if (Window) { - SDL_GL_MakeCurrent(Window, NULL); - SDL_GL_DeleteContext(Context); - SDL_DestroyWindow(Window); - } - SDL_Quit(); + if (Window && Context) { + SDL_GL_MakeCurrent(Window, NULL); + SDL_GL_DeleteContext(Context); + } + if (Window) { + SDL_DestroyWindow(Window); + } + if (--SDLDeviceInstances == 0) { + SDL_Quit(); os::Printer::log("Quit SDL", ELL_INFORMATION); } } From 57a737c417aef1c3ca038384af750050713fd8d4 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Tue, 2 Apr 2024 21:25:46 +0200 Subject: [PATCH 17/34] Improve logging of driver probing --- src/client/renderingengine.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/client/renderingengine.cpp b/src/client/renderingengine.cpp index debfe8e21..51f5e1e92 100644 --- a/src/client/renderingengine.cpp +++ b/src/client/renderingengine.cpp @@ -164,13 +164,19 @@ static std::optional chooseVideoDriver() return std::nullopt; } +static inline auto getVideoDriverName(video::E_DRIVER_TYPE driver) +{ + return RenderingEngine::getVideoDriverInfo(driver).friendly_name; +} + static irr::IrrlichtDevice *createDevice(SIrrlichtCreationParameters params, std::optional requested_driver) { if (requested_driver) { params.DriverType = *requested_driver; + verbosestream << "Trying video driver " << getVideoDriverName(params.DriverType) << std::endl; if (auto *device = createDeviceEx(params)) return device; - errorstream << "Failed to initialize the " << RenderingEngine::getVideoDriverInfo(*requested_driver).friendly_name << " video driver" << std::endl; + errorstream << "Failed to initialize the " << getVideoDriverName(params.DriverType) << " video driver" << std::endl; } sanity_check(requested_driver != video::EDT_NULL); @@ -179,6 +185,7 @@ static irr::IrrlichtDevice *createDevice(SIrrlichtCreationParameters params, std if (fallback_driver == video::EDT_NULL || fallback_driver == requested_driver) continue; params.DriverType = fallback_driver; + verbosestream << "Trying video driver " << getVideoDriverName(params.DriverType) << std::endl; if (auto *device = createDeviceEx(params)) return device; } @@ -232,7 +239,7 @@ RenderingEngine::RenderingEngine(IEventReceiver *receiver) m_device = createDevice(params, driverType); driver = m_device->getVideoDriver(); - infostream << "Using the " << RenderingEngine::getVideoDriverInfo(driver->getDriverType()).friendly_name << " video driver" << std::endl; + verbosestream << "Using the " << getVideoDriverName(driver->getDriverType()) << " video driver" << std::endl; // This changes the minimum allowed number of vertices in a VBO. Default is 500. driver->setMinHardwareBufferVertexCount(4); From ff88ed7c7516c6a0174344a3c1a522bfe7cca7a5 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Tue, 2 Apr 2024 21:26:55 +0200 Subject: [PATCH 18/34] Add symlink for Irrlicht shaders --- client/shaders/Irrlicht | 1 + 1 file changed, 1 insertion(+) create mode 120000 client/shaders/Irrlicht diff --git a/client/shaders/Irrlicht b/client/shaders/Irrlicht new file mode 120000 index 000000000..9349d3073 --- /dev/null +++ b/client/shaders/Irrlicht @@ -0,0 +1 @@ +../../irr/media/Shaders \ No newline at end of file From f8bff346f41005098f54b0ba4835a72ebb84f536 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Wed, 3 Apr 2024 13:56:49 +0200 Subject: [PATCH 19/34] Batched rendering of particles (#14489) Co-authored-by: x2048 Co-authored-by: Desour --- src/client/particles.cpp | 461 +++++++++++++++++++++++++-------------- src/client/particles.h | 145 +++++++----- src/tileanimation.h | 2 +- 3 files changed, 395 insertions(+), 213 deletions(-) diff --git a/src/client/particles.cpp b/src/client/particles.cpp index 14384f3b8..638505846 100644 --- a/src/client/particles.cpp +++ b/src/client/particles.cpp @@ -19,6 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "particles.h" #include +#include #include "client.h" #include "collision.h" #include "client/content_cao.h" @@ -26,21 +27,27 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "client/renderingengine.h" #include "util/numeric.h" #include "light.h" +#include "localplayer.h" #include "environment.h" #include "clientmap.h" #include "mapnode.h" #include "nodedef.h" #include "client.h" #include "settings.h" +#include "profiler.h" + +ClientParticleTexture::ClientParticleTexture(const ServerParticleTexture& p, ITextureSource *tsrc) +{ + tex = p; + // note: getTextureForMesh not needed here because we don't use texture filtering + ref = tsrc->getTexture(p.string); +} /* Particle */ Particle::Particle( - IGameDef *gamedef, - LocalPlayer *player, - ClientEnvironment *env, const ParticleParameters &p, const ClientParticleTexRef &texture, v2f texpos, @@ -49,14 +56,10 @@ Particle::Particle( ParticleSpawner *parent, std::unique_ptr owned_texture ) : - scene::ISceneNode(((Client *)gamedef)->getSceneManager()->getRootSceneNode(), - ((Client *)gamedef)->getSceneManager()), - m_expiration(p.expirationtime), - m_env(env), - m_gamedef(gamedef), - m_collisionbox(aabb3f(v3f(-p.size / 2.0f), v3f(p.size / 2.0f))), + m_base_color(color), + m_texture(texture), m_texpos(texpos), m_texsize(texsize), @@ -64,102 +67,30 @@ Particle::Particle( m_velocity(p.vel), m_acceleration(p.acc), m_p(p), - m_player(player), - - m_base_color(color), - m_color(color), m_parent(parent), m_owned_texture(std::move(owned_texture)) { - // Set material - { - // translate blend modes to GL blend functions - video::E_BLEND_FACTOR bfsrc, bfdst; - video::E_BLEND_OPERATION blendop; - const auto blendmode = texture.tex != nullptr - ? texture.tex->blendmode - : ParticleParamTypes::BlendMode::alpha; +} - switch (blendmode) { - case ParticleParamTypes::BlendMode::add: - bfsrc = video::EBF_SRC_ALPHA; - bfdst = video::EBF_DST_ALPHA; - blendop = video::EBO_ADD; - break; +Particle::~Particle() +{ + if (m_buffer) + m_buffer->release(m_index); +} - case ParticleParamTypes::BlendMode::sub: - bfsrc = video::EBF_SRC_ALPHA; - bfdst = video::EBF_DST_ALPHA; - blendop = video::EBO_REVSUBTRACT; - break; - - case ParticleParamTypes::BlendMode::screen: - bfsrc = video::EBF_ONE; - bfdst = video::EBF_ONE_MINUS_SRC_COLOR; - blendop = video::EBO_ADD; - break; - - default: // includes ParticleParamTypes::BlendMode::alpha - bfsrc = video::EBF_SRC_ALPHA; - bfdst = video::EBF_ONE_MINUS_SRC_ALPHA; - blendop = video::EBO_ADD; - break; - } - - // Texture - m_material.Lighting = false; - m_material.BackfaceCulling = false; - m_material.FogEnable = true; - m_material.forEachTexture([] (auto &tex) { - tex.MinFilter = video::ETMINF_NEAREST_MIPMAP_NEAREST; - tex.MagFilter = video::ETMAGF_NEAREST; - }); - - // correctly render layered transparent particles -- see #10398 - m_material.ZWriteEnable = video::EZW_AUTO; - - // enable alpha blending and set blend mode - m_material.MaterialType = video::EMT_ONETEXTURE_BLEND; - m_material.MaterialTypeParam = video::pack_textureBlendFunc( - bfsrc, bfdst, - video::EMFN_MODULATE_1X, - video::EAS_TEXTURE | video::EAS_VERTEX_COLOR); - m_material.BlendOperation = blendop; - m_material.setTexture(0, m_texture.ref); +bool Particle::attachToBuffer(ParticleBuffer *buffer) +{ + auto index_opt = buffer->allocate(); + if (index_opt.has_value()) { + m_index = index_opt.value(); + m_buffer = buffer; + return true; } - - // Irrlicht stuff - this->setAutomaticCulling(scene::EAC_OFF); - - // Init lighting - updateLight(); - - // Init model - updateVertices(); + return false; } -void Particle::OnRegisterSceneNode() -{ - if (IsVisible) - SceneManager->registerNodeForRendering(this, scene::ESNRP_TRANSPARENT_EFFECT); - - ISceneNode::OnRegisterSceneNode(); -} - -void Particle::render() -{ - video::IVideoDriver *driver = SceneManager->getVideoDriver(); - driver->setMaterial(m_material); - driver->setTransform(video::ETS_WORLD, AbsoluteTransformation); - - u16 indices[] = {0,1,2, 2,3,0}; - driver->drawVertexPrimitiveList(m_vertices, 4, - indices, 2, video::EVT_STANDARD, - scene::EPT_TRIANGLES, video::EIT_16BIT); -} - -void Particle::step(float dtime) +void Particle::step(float dtime, ClientEnvironment *env) { m_time += dtime; @@ -169,10 +100,10 @@ void Particle::step(float dtime) m_velocity = av*vecSign(m_velocity) + v3f(m_p.jitter.pickWithin())*dtime; if (m_p.collisiondetection) { - aabb3f box = m_collisionbox; + aabb3f box(v3f(-m_p.size / 2.0f), v3f(m_p.size / 2.0f)); v3f p_pos = m_pos * BS; v3f p_velocity = m_velocity * BS; - collisionMoveResult r = collisionMoveSimple(m_env, m_gamedef, BS * 0.5f, + collisionMoveResult r = collisionMoveSimple(env, env->getGameDef(), BS * 0.5f, box, 0.0f, dtime, &p_pos, &p_velocity, m_acceleration * BS, nullptr, m_p.object_collision); @@ -215,7 +146,7 @@ void Particle::step(float dtime) m_animation_time += dtime; int frame_length_i = 0; m_p.animation.determineParams( - m_material.getTexture(0)->getSize(), + m_texture.ref->getSize(), NULL, &frame_length_i, NULL); float frame_length = frame_length_i / 1000.0; while (m_animation_time > frame_length) { @@ -225,23 +156,19 @@ void Particle::step(float dtime) } // animate particle alpha in accordance with settings + float alpha = 1.f; if (m_texture.tex != nullptr) - m_alpha = m_texture.tex -> alpha.blend(m_time / (m_expiration+0.1f)); - else - m_alpha = 1.f; + alpha = m_texture.tex -> alpha.blend(m_time / (m_expiration+0.1f)); // Update lighting - updateLight(); + auto col = updateLight(env); + col.setAlpha(255 * alpha); // Update model - updateVertices(); - - // Update position -- see #10398 - v3s16 camera_offset = m_env->getCameraOffset(); - setPosition(m_pos*BS - intToFloat(camera_offset, BS)); + updateVertices(env, col); } -void Particle::updateLight() +video::SColor Particle::updateLight(ClientEnvironment *env) { u8 light = 0; bool pos_ok; @@ -251,32 +178,37 @@ void Particle::updateLight() floor(m_pos.Y+0.5), floor(m_pos.Z+0.5) ); - MapNode n = m_env->getClientMap().getNode(p, &pos_ok); + MapNode n = env->getClientMap().getNode(p, &pos_ok); if (pos_ok) - light = n.getLightBlend(m_env->getDayNightRatio(), - m_gamedef->ndef()->getLightingFlags(n)); + light = n.getLightBlend(env->getDayNightRatio(), + env->getGameDef()->ndef()->getLightingFlags(n)); else - light = blend_light(m_env->getDayNightRatio(), LIGHT_SUN, 0); + light = blend_light(env->getDayNightRatio(), LIGHT_SUN, 0); u8 m_light = decode_light(light + m_p.glow); - m_color.set(m_alpha*255, + return video::SColor(255, m_light * m_base_color.getRed() / 255, m_light * m_base_color.getGreen() / 255, m_light * m_base_color.getBlue() / 255); } -void Particle::updateVertices() +void Particle::updateVertices(ClientEnvironment *env, video::SColor color) { f32 tx0, tx1, ty0, ty1; v2f scale; + if (!m_buffer) + return; + + video::S3DVertex *vertices = m_buffer->getVertices(m_index); + if (m_texture.tex != nullptr) scale = m_texture.tex -> scale.blend(m_time / (m_expiration+0.1)); else scale = v2f(1.f, 1.f); if (m_p.animation.type != TAT_NONE) { - const v2u32 texsize = m_material.getTexture(0)->getSize(); + const v2u32 texsize = m_texture.ref->getSize(); v2f texcoord, framesize_f; v2u32 framesize; texcoord = m_p.animation.getTextureCoords(texsize, m_animation_frame); @@ -297,31 +229,30 @@ void Particle::updateVertices() auto half = m_p.size * .5f, hx = half * scale.X, hy = half * scale.Y; - m_vertices[0] = video::S3DVertex(-hx, -hy, - 0, 0, 0, 0, m_color, tx0, ty1); - m_vertices[1] = video::S3DVertex(hx, -hy, - 0, 0, 0, 0, m_color, tx1, ty1); - m_vertices[2] = video::S3DVertex(hx, hy, - 0, 0, 0, 0, m_color, tx1, ty0); - m_vertices[3] = video::S3DVertex(-hx, hy, - 0, 0, 0, 0, m_color, tx0, ty0); + vertices[0] = video::S3DVertex(-hx, -hy, + 0, 0, 0, 0, color, tx0, ty1); + vertices[1] = video::S3DVertex(hx, -hy, + 0, 0, 0, 0, color, tx1, ty1); + vertices[2] = video::S3DVertex(hx, hy, + 0, 0, 0, 0, color, tx1, ty0); + vertices[3] = video::S3DVertex(-hx, hy, + 0, 0, 0, 0, color, tx0, ty0); + // Update position -- see #10398 + auto *player = env->getLocalPlayer(); + v3s16 camera_offset = env->getCameraOffset(); - // see #10398 - // v3s16 camera_offset = m_env->getCameraOffset(); - // particle position is now handled by step() - m_box.reset(v3f()); - - for (video::S3DVertex &vertex : m_vertices) { + for (u16 i = 0; i < 4; i++) { + video::S3DVertex &vertex = vertices[i]; if (m_p.vertical) { - v3f ppos = m_player->getPosition()/BS; + v3f ppos = player->getPosition() / BS; vertex.Pos.rotateXZBy(std::atan2(ppos.Z - m_pos.Z, ppos.X - m_pos.X) / core::DEGTORAD + 90); } else { - vertex.Pos.rotateYZBy(m_player->getPitch()); - vertex.Pos.rotateXZBy(m_player->getYaw()); + vertex.Pos.rotateYZBy(player->getPitch()); + vertex.Pos.rotateXZBy(player->getYaw()); } - m_box.addInternalPoint(vertex.Pos); + vertex.Pos += m_pos * BS - intToFloat(camera_offset, BS); } } @@ -330,7 +261,6 @@ void Particle::updateVertices() */ ParticleSpawner::ParticleSpawner( - IGameDef *gamedef, LocalPlayer *player, const ParticleSpawnerParameters ¶ms, u16 attached_id, @@ -340,7 +270,6 @@ ParticleSpawner::ParticleSpawner( m_active(0), m_particlemanager(p_manager), m_time(0.0f), - m_gamedef(gamedef), m_player(player), p(params), m_texpool(std::move(texpool)), @@ -565,9 +494,6 @@ void ParticleSpawner::spawnParticle(ClientEnvironment *env, float radius, ++m_active; m_particlemanager->addParticle(std::make_unique( - m_gamedef, - m_player, - env, pp, texture, texpos, @@ -624,6 +550,109 @@ void ParticleSpawner::step(float dtime, ClientEnvironment *env) } } +/* + ParticleBuffer +*/ + +ParticleBuffer::ParticleBuffer(ClientEnvironment *env, const video::SMaterial &material) + : scene::ISceneNode( + env->getGameDef()->getSceneManager()->getRootSceneNode(), + env->getGameDef()->getSceneManager()), + m_mesh_buffer(make_irr()) +{ + m_mesh_buffer->getMaterial() = material; +} + +static constexpr u16 quad_indices[] = { 0, 1, 2, 2, 3, 0 }; + +std::optional ParticleBuffer::allocate() +{ + u16 index; + + m_usage_timer = 0; + + if (!m_free_list.empty()) { + index = m_free_list.back(); + m_free_list.pop_back(); + auto *vertices = static_cast(m_mesh_buffer->getVertices()); + u16 *indices = m_mesh_buffer->getIndices(); + // reset vertices, because it is only written in Particle::step() + for (u16 i = 0; i < 4; i++) + vertices[4 * index + i] = video::S3DVertex(); + for (u16 i = 0; i < 6; i++) + indices[6 * index + i] = 4 * index + quad_indices[i]; + return index; + } + + if (m_count >= MAX_PARTICLES_PER_BUFFER) + return std::nullopt; + + // append new vertices + // note: Our buffer never gets smaller, but ParticleManager will delete + // us after a while. + std::array vertices {}; + m_mesh_buffer->append(&vertices.front(), 4, quad_indices, 6); + index = m_count++; + return index; +} + +void ParticleBuffer::release(u16 index) +{ + assert(index < m_count); + u16 *indices = m_mesh_buffer->getIndices(); + for (u16 i = 0; i < 6; i++) + indices[6 * index + i] = 0; + m_free_list.push_back(index); +} + +video::S3DVertex *ParticleBuffer::getVertices(u16 index) +{ + if (index >= m_count) + return nullptr; + m_bounding_box_dirty = true; + return &(static_cast(m_mesh_buffer->getVertices())[4 * index]); +} + +void ParticleBuffer::OnRegisterSceneNode() +{ + if (IsVisible) + SceneManager->registerNodeForRendering(this, scene::ESNRP_TRANSPARENT_EFFECT); + scene::ISceneNode::OnRegisterSceneNode(); +} + +const core::aabbox3df &ParticleBuffer::getBoundingBox() const +{ + if (!m_bounding_box_dirty) + return m_mesh_buffer->BoundingBox; + + core::aabbox3df box; + for (u16 i = 0; i < m_count; i++) { + // check if this index is used + static_assert(quad_indices[1] != 0); + if (m_mesh_buffer->getIndices()[6 * i + 1] == 0) + continue; + + for (u16 j = 0; j < 4; j++) + box.addInternalPoint(m_mesh_buffer->getPosition(i * 4 + j)); + } + + m_mesh_buffer->BoundingBox = box; + m_bounding_box_dirty = false; + return m_mesh_buffer->BoundingBox; +} + +void ParticleBuffer::render() +{ + video::IVideoDriver *driver = SceneManager->getVideoDriver(); + + if (isEmpty()) + return; + + driver->setTransform(video::ETS_WORLD, core::matrix4()); + driver->setMaterial(m_mesh_buffer->getMaterial()); + driver->drawMeshBuffer(m_mesh_buffer.get()); +} + /* ParticleManager */ @@ -639,8 +668,9 @@ ParticleManager::~ParticleManager() void ParticleManager::step(float dtime) { - stepParticles (dtime); - stepSpawners (dtime); + stepParticles(dtime); + stepSpawners(dtime); + stepBuffers(dtime); } void ParticleManager::stepSpawners(float dtime) @@ -684,35 +714,59 @@ void ParticleManager::stepParticles(float dtime) assert(parent->hasActive()); parent->decrActive(); } - // remove scene node - p.remove(); // delete m_particles[i] = std::move(m_particles.back()); m_particles.pop_back(); } else { - p.step(dtime); + p.step(dtime, m_env); ++i; } } } +void ParticleManager::stepBuffers(float dtime) +{ + constexpr float INTERVAL = 0.5f; + if (!m_buffer_gc.step(dtime, INTERVAL)) + return; + + MutexAutoLock lock(m_particle_list_lock); + + // remove buffers that have been unused for 5 seconds + size_t alloc = 0; + for (size_t i = 0; i < m_particle_buffers.size(); ) { + auto &buf = m_particle_buffers[i]; + buf->m_usage_timer += INTERVAL; + if (buf->isEmpty() && buf->m_usage_timer > 5.0f) { + // delete and swap with last + buf->remove(); + buf = std::move(m_particle_buffers.back()); + m_particle_buffers.pop_back(); + } else { + i++; + alloc += buf->m_count; + } + } + + g_profiler->avg("ParticleManager: particle buffer count [#]", m_particle_buffers.size()); + if (!m_particle_buffers.empty()) + g_profiler->avg("ParticleManager: buffer allocated size [#]", alloc); +} + void ParticleManager::clearAll() { MutexAutoLock lock(m_spawner_list_lock); MutexAutoLock lock2(m_particle_list_lock); - // clear particle spawners m_particle_spawners.clear(); m_dying_particle_spawners.clear(); - // clear particles - for (std::unique_ptr &p : m_particles) { - // remove scene node - p->remove(); - // delete - p.reset(); - } m_particles.clear(); + + // have to remove from scene first because it keeps a reference + for (auto &it : m_particle_buffers) + it->remove(); + m_particle_buffers.clear(); } void ParticleManager::handleParticleEvent(ClientEvent *event, Client *client, @@ -744,7 +798,6 @@ void ParticleManager::handleParticleEvent(ClientEvent *event, Client *client, addParticleSpawner(event->add_particlespawner.id, std::make_unique( - client, player, p, event->add_particlespawner.attached_id, @@ -785,7 +838,7 @@ void ParticleManager::handleParticleEvent(ClientEvent *event, Client *client, p.size = oldsize; if (texture.ref) { - addParticle(std::make_unique(client, player, m_env, + addParticle(std::make_unique( p, texture, texpos, texsize, color, nullptr, std::move(texstore))); } @@ -885,9 +938,6 @@ void ParticleManager::addNodeParticle(IGameDef *gamedef, ); addParticle(std::make_unique( - gamedef, - player, - m_env, p, ClientParticleTexRef(ref), texpos, @@ -902,13 +952,104 @@ void ParticleManager::reserveParticleSpace(size_t max_estimate) m_particles.reserve(m_particles.size() + max_estimate); } -void ParticleManager::addParticle(std::unique_ptr toadd) +video::SMaterial ParticleManager::getMaterialForParticle(const ClientParticleTexRef &texture) +{ + // translate blend modes to GL blend functions + video::E_BLEND_FACTOR bfsrc, bfdst; + video::E_BLEND_OPERATION blendop; + const auto blendmode = texture.tex ? texture.tex->blendmode : + ParticleParamTypes::BlendMode::alpha; + + switch (blendmode) { + case ParticleParamTypes::BlendMode::add: + bfsrc = video::EBF_SRC_ALPHA; + bfdst = video::EBF_DST_ALPHA; + blendop = video::EBO_ADD; + break; + + case ParticleParamTypes::BlendMode::sub: + bfsrc = video::EBF_SRC_ALPHA; + bfdst = video::EBF_DST_ALPHA; + blendop = video::EBO_REVSUBTRACT; + break; + + case ParticleParamTypes::BlendMode::screen: + bfsrc = video::EBF_ONE; + bfdst = video::EBF_ONE_MINUS_SRC_COLOR; + blendop = video::EBO_ADD; + break; + + default: // includes ParticleParamTypes::BlendMode::alpha + bfsrc = video::EBF_SRC_ALPHA; + bfdst = video::EBF_ONE_MINUS_SRC_ALPHA; + blendop = video::EBO_ADD; + break; + } + + video::SMaterial material; + + // Texture + material.Lighting = false; + material.BackfaceCulling = false; + material.FogEnable = true; + material.forEachTexture([] (auto &tex) { + tex.MinFilter = video::ETMINF_NEAREST_MIPMAP_NEAREST; + tex.MagFilter = video::ETMAGF_NEAREST; + }); + + // We don't have working transparency sorting. Disable Z-Write for + // correct results for clipped-alpha at least. + material.ZWriteEnable = video::EZW_OFF; + + // enable alpha blending and set blend mode + material.MaterialType = video::EMT_ONETEXTURE_BLEND; + material.MaterialTypeParam = video::pack_textureBlendFunc( + bfsrc, bfdst, + video::EMFN_MODULATE_1X, + video::EAS_TEXTURE | video::EAS_VERTEX_COLOR); + material.BlendOperation = blendop; + assert(texture.ref); + material.setTexture(0, texture.ref); + + return material; +} + +bool ParticleManager::addParticle(std::unique_ptr toadd) { MutexAutoLock lock(m_particle_list_lock); - m_particles.push_back(std::move(toadd)); -} + auto material = getMaterialForParticle(toadd->getTextureRef()); + ParticleBuffer *found = nullptr; + // simple shortcut when multiple particles of the same type get added + if (!m_particles.empty()) { + auto &last = m_particles.back(); + if (last->getBuffer() && last->getBuffer()->getMaterial(0) == material) + found = last->getBuffer(); + } + // search fitting buffer + if (!found) { + for (auto &buffer : m_particle_buffers) { + if (buffer->getMaterial(0) == material) { + found = buffer.get(); + break; + } + } + } + // or create a new one + if (!found) { + auto tmp = make_irr(m_env, material); + found = tmp.get(); + m_particle_buffers.push_back(std::move(tmp)); + } + + if (!toadd->attachToBuffer(found)) { + infostream << "ParticleManager: buffer full, dropping particle" << std::endl; + return false; + } + m_particles.push_back(std::move(toadd)); + return true; +} void ParticleManager::addParticleSpawner(u64 id, std::unique_ptr toadd) { diff --git a/src/client/particles.h b/src/client/particles.h index 2c8ceafa4..92ea8fb67 100644 --- a/src/client/particles.h +++ b/src/client/particles.h @@ -19,9 +19,10 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once -#include +#include +#include #include "irrlichttypes_extrabloated.h" -#include "localplayer.h" +#include "irr_ptr.h" #include "../particles.h" struct ClientEvent; @@ -29,6 +30,10 @@ class ParticleManager; class ClientEnvironment; struct MapNode; struct ContentFeatures; +class LocalPlayer; +class ITextureSource; +class IGameDef; +class Client; struct ClientParticleTexture { @@ -38,9 +43,7 @@ struct ClientParticleTexture video::ITexture *ref = nullptr; ClientParticleTexture() = default; - ClientParticleTexture(const ServerParticleTexture& p, ITextureSource *t): - tex(p), - ref(t->getTextureForMesh(p.string)) {}; + ClientParticleTexture(const ServerParticleTexture& p, ITextureSource *tsrc); }; struct ClientParticleTexRef @@ -61,14 +64,12 @@ struct ClientParticleTexRef }; class ParticleSpawner; +class ParticleBuffer; -class Particle : public scene::ISceneNode +class Particle { public: Particle( - IGameDef *gamedef, - LocalPlayer *player, - ClientEnvironment *env, const ParticleParameters &p, const ClientParticleTexRef &texture, v2f texpos, @@ -78,61 +79,46 @@ public: std::unique_ptr owned_texture = nullptr ); - virtual const aabb3f &getBoundingBox() const - { - return m_box; - } + ~Particle(); - virtual u32 getMaterialCount() const - { - return 1; - } + DISABLE_CLASS_COPY(Particle) - virtual video::SMaterial& getMaterial(u32 i) - { - return m_material; - } + void step(float dtime, ClientEnvironment *env); - virtual void OnRegisterSceneNode(); - virtual void render(); - - void step(float dtime); - - bool isExpired () + bool isExpired () const { return m_expiration < m_time; } - ParticleSpawner *getParent() { return m_parent; } + ParticleSpawner *getParent() const { return m_parent; } + + const ClientParticleTexRef &getTextureRef() const { return m_texture; } + + ParticleBuffer *getBuffer() const { return m_buffer; } + bool attachToBuffer(ParticleBuffer *buffer); private: - void updateLight(); - void updateVertices(); - void setVertexAlpha(float a); + video::SColor updateLight(ClientEnvironment *env); + void updateVertices(ClientEnvironment *env, video::SColor color); + + ParticleBuffer *m_buffer = nullptr; + u16 m_index; // index in m_buffer - video::S3DVertex m_vertices[4]; float m_time = 0.0f; float m_expiration; - ClientEnvironment *m_env; - IGameDef *m_gamedef; - aabb3f m_box; - aabb3f m_collisionbox; + // Color without lighting + video::SColor m_base_color; + ClientParticleTexRef m_texture; - video::SMaterial m_material; v2f m_texpos; v2f m_texsize; v3f m_pos; v3f m_velocity; v3f m_acceleration; - const ParticleParameters m_p; - LocalPlayer *m_player; - //! Color without lighting - video::SColor m_base_color; - //! Final rendered color - video::SColor m_color; + const ParticleParameters m_p; + float m_animation_time = 0.0f; int m_animation_frame = 0; - float m_alpha = 0.0f; ParticleSpawner *m_parent = nullptr; // Used if not spawned from a particlespawner @@ -142,8 +128,7 @@ private: class ParticleSpawner { public: - ParticleSpawner(IGameDef *gamedef, - LocalPlayer *player, + ParticleSpawner(LocalPlayer *player, const ParticleSpawnerParameters ¶ms, u16 attached_id, std::vector &&texpool, @@ -164,7 +149,6 @@ private: size_t m_active; ParticleManager *m_particlemanager; float m_time; - IGameDef *m_gamedef; LocalPlayer *m_player; ParticleSpawnerParameters p; std::vector m_texpool; @@ -172,12 +156,61 @@ private: u16 m_attached_id; }; +class ParticleBuffer : public scene::ISceneNode +{ + friend class ParticleManager; +public: + ParticleBuffer(ClientEnvironment *env, const video::SMaterial &material); + + // for pointer stability + DISABLE_CLASS_COPY(ParticleBuffer) + + /// Reserves one more slot for a particle (4 vertices, 6 indices) + /// @return particle index within buffer + std::optional allocate(); + /// Frees the particle at `index` + void release(u16 index); + + /// @return video::S3DVertex[4] + video::S3DVertex *getVertices(u16 index); + + inline bool isEmpty() const { + return m_free_list.size() == m_count; + } + + virtual video::SMaterial &getMaterial(u32 num) override { + return m_mesh_buffer->getMaterial(); + } + virtual u32 getMaterialCount() const override { + return 1; + } + + virtual const core::aabbox3df &getBoundingBox() const override; + + virtual void render() override; + + virtual void OnRegisterSceneNode() override; + + // we have 16-bit indices + static constexpr u16 MAX_PARTICLES_PER_BUFFER = 16000; + +private: + irr_ptr m_mesh_buffer; + // unused (e.g. expired) particle indices for re-use + std::vector m_free_list; + // for automatic deletion when unused for a while. is reset on allocate(). + float m_usage_timer = 0; + // total count of contained particles + u16 m_count = 0; + mutable bool m_bounding_box_dirty = true; +}; + /** * Class doing particle as well as their spawners handling */ class ParticleManager { -friend class ParticleSpawner; + friend class ParticleSpawner; public: ParticleManager(ClientEnvironment* env); DISABLE_CLASS_COPY(ParticleManager) @@ -213,7 +246,9 @@ protected: ParticleParameters &p, video::ITexture **texture, v2f &texpos, v2f &texsize, video::SColor *color, u8 tilenum = 0); - void addParticle(std::unique_ptr toadd); + static video::SMaterial getMaterialForParticle(const ClientParticleTexRef &texture); + + bool addParticle(std::unique_ptr toadd); private: void addParticleSpawner(u64 id, std::unique_ptr toadd); @@ -221,17 +256,23 @@ private: void stepParticles(float dtime); void stepSpawners(float dtime); + void stepBuffers(float dtime); void clearAll(); std::vector> m_particles; std::unordered_map> m_particle_spawners; std::vector> m_dying_particle_spawners; - // Start the particle spawner ids generated from here after u32_max. lower values are - // for server sent spawners. - u64 m_next_particle_spawner_id = U32_MAX + 1; + std::vector> m_particle_buffers; + + // Start the particle spawner ids generated from here after u32_max. + // lower values are for server sent spawners. + u64 m_next_particle_spawner_id = static_cast(U32_MAX) + 1; ClientEnvironment *m_env; + + IntervalLimiter m_buffer_gc; + std::mutex m_particle_list_lock; std::mutex m_spawner_list_lock; }; diff --git a/src/tileanimation.h b/src/tileanimation.h index e7cf1a088..db258e240 100644 --- a/src/tileanimation.h +++ b/src/tileanimation.h @@ -31,7 +31,7 @@ enum TileAnimationType : u8 struct TileAnimationParams { - enum TileAnimationType type; + enum TileAnimationType type = TileAnimationType::TAT_NONE; union { // struct { From 0ea1ec31fc19ea5026db9b068bb3a606a8505d0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20M=C3=BCller?= <34514239+appgurueu@users.noreply.github.com> Date: Wed, 3 Apr 2024 14:24:15 +0200 Subject: [PATCH 20/34] Misc. doc things (#14509) * Improve `[sheet` docs * Clarify that `self` is a luaentity in `self.name` * Clarify `textures = {itemname}` deprecation * Document global callback table names --- doc/lua_api.md | 53 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/doc/lua_api.md b/doc/lua_api.md index 329f17b01..cca231c73 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -659,8 +659,9 @@ The mask is applied using binary AND. #### `[sheet:x:,` -Retrieves a tile at position x,y from the base image -which it assumes to be a tilesheet with dimensions w,h. +Retrieves a tile at position x, y (in tiles, 0-indexed) +from the base image, which it assumes to be a tilesheet +with dimensions w, h (in tiles). #### `[colorize::` @@ -7313,8 +7314,41 @@ Global tables All callbacks registered with [Global callback registration functions] are added to corresponding `minetest.registered_*` tables. +For historical reasons, the use of an -s suffix in these names is inconsistent. - +* `minetest.registered_on_chat_messages` +* `minetest.registered_on_chatcommands` +* `minetest.registered_globalsteps` +* `minetest.registered_on_punchnodes` +* `minetest.registered_on_placenodes` +* `minetest.registered_on_dignodes` +* `minetest.registered_on_generateds` +* `minetest.registered_on_newplayers` +* `minetest.registered_on_dieplayers` +* `minetest.registered_on_respawnplayers` +* `minetest.registered_on_prejoinplayers` +* `minetest.registered_on_joinplayers` +* `minetest.registered_on_leaveplayers` +* `minetest.registered_on_player_receive_fields` +* `minetest.registered_on_cheats` +* `minetest.registered_on_crafts` +* `minetest.registered_craft_predicts` +* `minetest.registered_on_item_eats` +* `minetest.registered_on_item_pickups` +* `minetest.registered_on_punchplayers` +* `minetest.registered_on_authplayers` +* `minetest.registered_on_player_inventory_actions` +* `minetest.registered_allow_player_inventory_actions` +* `minetest.registered_on_rightclickplayers` +* `minetest.registered_on_mods_loaded` +* `minetest.registered_on_shutdown` +* `minetest.registered_on_protection_violation` +* `minetest.registered_on_priv_grant` +* `minetest.registered_on_priv_revoke` +* `minetest.registered_can_bypass_userlimit` +* `minetest.registered_on_modchannel_message` +* `minetest.registered_on_liquid_transformed` +* `minetest.registered_on_mapblocks_changed` Class reference =============== @@ -7944,8 +7978,12 @@ child will follow movement and rotation of that bone. * Fourth column: subject looking to the right * Fifth column: subject viewed from above * Sixth column: subject viewed from below -* `get_entity_name()` (**Deprecated**: Will be removed in a future version, use the field `self.name` instead) -* `get_luaentity()`: returns the object's associated luaentity table +* `get_luaentity()`: + * 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. #### Player only (no-op for other objects) @@ -8664,7 +8702,8 @@ Player properties need to be saved manually. -- "mesh" uses the defined mesh model. -- "wielditem" is used for dropped items. -- (see builtin/game/item_entity.lua). - -- For this use 'wield_item = itemname' (Deprecated: 'textures = {itemname}'). + -- For this use 'wield_item = itemname'. + -- Setting 'textures = {itemname}' has the same effect, but is deprecated. -- If the item has a 'wield_image' the object will be an extrusion of -- that, otherwise: -- If 'itemname' is a cubic node or nodebox the object will appear @@ -8691,8 +8730,8 @@ Player properties need to be saved manually. -- "cube" uses 6 textures just like a node, but all 6 must be defined. -- "sprite" uses 1 texture. -- "upright_sprite" uses 2 textures: {front, back}. - -- "wielditem" expects 'textures = {itemname}' (see 'visual' above). -- "mesh" requires one texture for each mesh buffer/material (in order) + -- Deprecated usage of "wielditem" expects 'textures = {itemname}' (see 'visual' above). colors = {}, -- Number of required colors depends on visual From a9a0f1e12950f099b9fb753c02f993e43e183a2d Mon Sep 17 00:00:00 2001 From: sfan5 Date: Fri, 5 Apr 2024 13:00:10 +0200 Subject: [PATCH 21/34] Fix GLES shader precision issues (#14516) --- client/shaders/cloud_shader/opengl_fragment.glsl | 4 ++-- client/shaders/cloud_shader/opengl_vertex.glsl | 4 ++-- client/shaders/nodes_shader/opengl_fragment.glsl | 10 +++------- client/shaders/nodes_shader/opengl_vertex.glsl | 5 ++--- client/shaders/object_shader/opengl_fragment.glsl | 7 +++---- client/shaders/object_shader/opengl_vertex.glsl | 7 ++----- client/shaders/stars_shader/opengl_fragment.glsl | 2 +- src/client/game.cpp | 8 +------- 8 files changed, 16 insertions(+), 31 deletions(-) diff --git a/client/shaders/cloud_shader/opengl_fragment.glsl b/client/shaders/cloud_shader/opengl_fragment.glsl index fe416d121..d47173ce0 100644 --- a/client/shaders/cloud_shader/opengl_fragment.glsl +++ b/client/shaders/cloud_shader/opengl_fragment.glsl @@ -1,7 +1,7 @@ -uniform vec4 fogColor; +uniform lowp vec4 fogColor; uniform float fogDistance; uniform float fogShadingParameter; -varying vec3 eyeVec; +varying highp vec3 eyeVec; varying lowp vec4 varColor; diff --git a/client/shaders/cloud_shader/opengl_vertex.glsl b/client/shaders/cloud_shader/opengl_vertex.glsl index 4bc6eb65e..3f2e7d9b3 100644 --- a/client/shaders/cloud_shader/opengl_vertex.glsl +++ b/client/shaders/cloud_shader/opengl_vertex.glsl @@ -1,8 +1,8 @@ -uniform vec4 emissiveColor; +uniform lowp vec4 emissiveColor; varying lowp vec4 varColor; -varying vec3 eyeVec; +varying highp vec3 eyeVec; void main(void) { diff --git a/client/shaders/nodes_shader/opengl_fragment.glsl b/client/shaders/nodes_shader/opengl_fragment.glsl index 0e75efedf..46977b147 100644 --- a/client/shaders/nodes_shader/opengl_fragment.glsl +++ b/client/shaders/nodes_shader/opengl_fragment.glsl @@ -1,13 +1,12 @@ uniform sampler2D baseTexture; uniform vec3 dayLight; -uniform vec4 fogColor; +uniform lowp vec4 fogColor; uniform float fogDistance; uniform float fogShadingParameter; -uniform vec3 eyePosition; // The cameraOffset is the current center of the visible world. -uniform vec3 cameraOffset; +uniform highp vec3 cameraOffset; uniform float animationTimer; #ifdef ENABLE_DYNAMIC_SHADOWS // shadow texture @@ -44,11 +43,8 @@ varying mediump vec2 varTexCoord; #else centroid varying vec2 varTexCoord; #endif -varying vec3 eyeVec; +varying highp vec3 eyeVec; varying float nightRatio; -varying vec3 tsEyeVec; -varying vec3 lightVec; -varying vec3 tsLightVec; #ifdef ENABLE_DYNAMIC_SHADOWS diff --git a/client/shaders/nodes_shader/opengl_vertex.glsl b/client/shaders/nodes_shader/opengl_vertex.glsl index 813a5f76b..d96164d76 100644 --- a/client/shaders/nodes_shader/opengl_vertex.glsl +++ b/client/shaders/nodes_shader/opengl_vertex.glsl @@ -1,10 +1,9 @@ uniform mat4 mWorld; // Color of the light emitted by the sun. uniform vec3 dayLight; -uniform vec3 eyePosition; // The cameraOffset is the current center of the visible world. -uniform vec3 cameraOffset; +uniform highp vec3 cameraOffset; uniform float animationTimer; varying vec3 vNormal; @@ -44,7 +43,7 @@ centroid varying vec2 varTexCoord; varying float area_enable_parallax; -varying vec3 eyeVec; +varying highp vec3 eyeVec; varying float nightRatio; // Color of the light emitted by the light sources. const vec3 artificialLight = vec3(1.04, 1.04, 1.04); diff --git a/client/shaders/object_shader/opengl_fragment.glsl b/client/shaders/object_shader/opengl_fragment.glsl index 61e00485b..2b8af3fa9 100644 --- a/client/shaders/object_shader/opengl_fragment.glsl +++ b/client/shaders/object_shader/opengl_fragment.glsl @@ -1,13 +1,12 @@ uniform sampler2D baseTexture; uniform vec3 dayLight; -uniform vec4 fogColor; +uniform lowp vec4 fogColor; uniform float fogDistance; uniform float fogShadingParameter; -uniform vec3 eyePosition; // The cameraOffset is the current center of the visible world. -uniform vec3 cameraOffset; +uniform highp vec3 cameraOffset; uniform float animationTimer; #ifdef ENABLE_DYNAMIC_SHADOWS // shadow texture @@ -44,7 +43,7 @@ varying mediump vec2 varTexCoord; #else centroid varying vec2 varTexCoord; #endif -varying vec3 eyeVec; +varying highp vec3 eyeVec; varying float nightRatio; varying float vIDiff; diff --git a/client/shaders/object_shader/opengl_vertex.glsl b/client/shaders/object_shader/opengl_vertex.glsl index 6a5f500b6..d5a434da5 100644 --- a/client/shaders/object_shader/opengl_vertex.glsl +++ b/client/shaders/object_shader/opengl_vertex.glsl @@ -1,10 +1,7 @@ uniform mat4 mWorld; uniform vec3 dayLight; -uniform vec3 eyePosition; uniform float animationTimer; -uniform vec4 emissiveColor; -uniform vec3 cameraOffset; - +uniform lowp vec4 emissiveColor; varying vec3 vNormal; varying vec3 vPosition; @@ -33,7 +30,7 @@ centroid varying vec2 varTexCoord; varying float perspective_factor; #endif -varying vec3 eyeVec; +varying highp vec3 eyeVec; varying float nightRatio; // Color of the light emitted by the light sources. const vec3 artificialLight = vec3(1.04, 1.04, 1.04); diff --git a/client/shaders/stars_shader/opengl_fragment.glsl b/client/shaders/stars_shader/opengl_fragment.glsl index 209b6dc89..224032fa3 100644 --- a/client/shaders/stars_shader/opengl_fragment.glsl +++ b/client/shaders/stars_shader/opengl_fragment.glsl @@ -1,4 +1,4 @@ -uniform vec4 emissiveColor; +uniform lowp vec4 emissiveColor; void main(void) { diff --git a/src/client/game.cpp b/src/client/game.cpp index 3c3feaa72..f40ac2de2 100644 --- a/src/client/game.cpp +++ b/src/client/game.cpp @@ -381,11 +381,9 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter CachedPixelShaderSetting m_animation_timer_delta_pixel{"animationTimerDelta"}; CachedPixelShaderSetting m_day_light{"dayLight"}; - CachedPixelShaderSetting m_eye_position_pixel{"eyePosition"}; - CachedVertexShaderSetting m_eye_position_vertex{"eyePosition"}; CachedPixelShaderSetting m_minimap_yaw{"yawVec"}; CachedPixelShaderSetting m_camera_offset_pixel{"cameraOffset"}; - CachedPixelShaderSetting m_camera_offset_vertex{"cameraOffset"}; + CachedVertexShaderSetting m_camera_offset_vertex{"cameraOffset"}; CachedPixelShaderSetting m_texture0{"texture0"}; CachedPixelShaderSetting m_texture1{"texture1"}; CachedPixelShaderSetting m_texture2{"texture2"}; @@ -483,10 +481,6 @@ public: m_animation_timer_delta_vertex.set(&animation_timer_delta_f, services); m_animation_timer_delta_pixel.set(&animation_timer_delta_f, services); - v3f epos = m_client->getEnv().getLocalPlayer()->getEyePosition(); - m_eye_position_pixel.set(epos, services); - m_eye_position_vertex.set(epos, services); - if (m_client->getMinimap()) { v3f minimap_yaw = m_client->getMinimap()->getYawVec(); m_minimap_yaw.set(minimap_yaw, services); From b2982a6f14fd9d016e5750044b8908592ecbd756 Mon Sep 17 00:00:00 2001 From: Gregor Parzefall Date: Tue, 2 Apr 2024 09:39:44 +0200 Subject: [PATCH 22/34] Fix all cached media being loaded at once on the main thread --- src/client/client.cpp | 5 +++++ src/client/client.h | 1 + src/client/clientmedia.cpp | 13 +++++++++++++ 3 files changed, 19 insertions(+) diff --git a/src/client/client.cpp b/src/client/client.cpp index d6781a965..789a37c36 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -1795,6 +1795,11 @@ float Client::mediaReceiveProgress() return 1.0; // downloader only exists when not yet done } +void Client::drawLoadScreen(const std::wstring &text, float dtime, int percent) { + m_rendering_engine->run(); + m_rendering_engine->draw_load_screen(text, guienv, m_tsrc, dtime, percent); +} + struct TextureUpdateArgs { gui::IGUIEnvironment *guienv; u64 last_time_ms; diff --git a/src/client/client.h b/src/client/client.h index 42049791f..b2ff9a0da 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -356,6 +356,7 @@ public: float mediaReceiveProgress(); + void drawLoadScreen(const std::wstring &text, float dtime, int percent); void afterContentReceived(); void showUpdateProgressTexture(void *args, u32 progress, u32 max_progress); diff --git a/src/client/clientmedia.cpp b/src/client/clientmedia.cpp index 764fac422..45fb1c6aa 100644 --- a/src/client/clientmedia.cpp +++ b/src/client/clientmedia.cpp @@ -18,6 +18,7 @@ with this program; if not, write to the Free Software Foundation, Inc., */ #include "clientmedia.h" +#include "gettext.h" #include "httpfetch.h" #include "client.h" #include "filecache.h" @@ -184,6 +185,11 @@ void ClientMediaDownloader::step(Client *client) void ClientMediaDownloader::initialStep(Client *client) { + std::wstring loading_text = wstrgettext("Media..."); + // Tradeoff between responsiveness during media loading and media loading speed + const u64 chunk_time_ms = 33; + u64 last_time = porting::getTimeMs(); + // Check media cache m_uncached_count = m_files.size(); for (auto &file_it : m_files) { @@ -195,6 +201,13 @@ void ClientMediaDownloader::initialStep(Client *client) filestatus->received = true; m_uncached_count--; } + + u64 cur_time = porting::getTimeMs(); + u64 dtime = porting::getDeltaMs(last_time, cur_time); + if (dtime >= chunk_time_ms) { + client->drawLoadScreen(loading_text, dtime / 1000.0f, 30); + last_time = cur_time; + } } assert(m_uncached_received_count == 0); From fd8e02195ea1c44f93a18f18e1e850c71ff0d33b Mon Sep 17 00:00:00 2001 From: Gregor Parzefall Date: Tue, 2 Apr 2024 14:57:25 +0200 Subject: [PATCH 23/34] Fix local server startup and shutdown blocking the main thread Co-authored-by: sfan5 --- src/client/game.cpp | 57 +++++++++++++++++++++++++++++++------ src/debug.h | 1 - src/threading/lambda.h | 64 ++++++++++++++++++++++++++++++++++++++++++ src/threading/thread.h | 1 + 4 files changed, 113 insertions(+), 10 deletions(-) create mode 100644 src/threading/lambda.h diff --git a/src/client/game.cpp b/src/client/game.cpp index f40ac2de2..55ec480b5 100644 --- a/src/client/game.cpp +++ b/src/client/game.cpp @@ -66,6 +66,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "settings.h" #include "shader.h" #include "sky.h" +#include "threading/lambda.h" #include "translation.h" #include "util/basic_macros.h" #include "util/directiontables.h" @@ -1014,12 +1015,6 @@ Game::Game() : Game::~Game() { - delete client; - delete soundmaker; - sound_manager.reset(); - - delete server; // deleted first to stop all server threads - delete hud; delete camera; delete quicktune; @@ -1268,6 +1263,28 @@ void Game::shutdown() sleep_ms(100); } } + + delete client; + delete soundmaker; + sound_manager.reset(); + + auto stop_thread = runInThread([=] { + delete server; + }, "ServerStop"); + + FpsControl fps_control; + fps_control.reset(); + + while (stop_thread->isRunning()) { + m_rendering_engine->run(); + f32 dtime; + fps_control.limit(device, &dtime); + showOverlayMessage(N_("Shutting down..."), dtime, 0, false); + } + + stop_thread->rethrow(); + + // to be continued in Game::~Game } @@ -1373,11 +1390,33 @@ bool Game::createSingleplayerServer(const std::string &map_dir, server = new Server(map_dir, gamespec, simple_singleplayer_mode, bind_addr, false, nullptr, error_message); - server->start(); - copyServerClientCache(); + auto start_thread = runInThread([=] { + server->start(); + copyServerClientCache(); + }, "ServerStart"); - return true; + input->clear(); + bool success = true; + + FpsControl fps_control; + fps_control.reset(); + + while (start_thread->isRunning()) { + if (!m_rendering_engine->run() || input->cancelPressed()) + success = false; + f32 dtime; + fps_control.limit(device, &dtime); + + if (success) + showOverlayMessage(N_("Creating server..."), dtime, 5); + else + showOverlayMessage(N_("Shutting down..."), dtime, 0, false); + } + + start_thread->rethrow(); + + return success; } void Game::copyServerClientCache() diff --git a/src/debug.h b/src/debug.h index aeea81d47..80497f2b3 100644 --- a/src/debug.h +++ b/src/debug.h @@ -19,7 +19,6 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once -#include #include #include #include "gettime.h" diff --git a/src/threading/lambda.h b/src/threading/lambda.h new file mode 100644 index 000000000..146693457 --- /dev/null +++ b/src/threading/lambda.h @@ -0,0 +1,64 @@ +// Minetest +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include +#include +#include "debug.h" +#include "threading/thread.h" + +/** + * Class returned by `runInThread`. + * + * Provides the usual thread methods along with `rethrow()`. +*/ +class LambdaThread : public Thread +{ + friend std::unique_ptr runInThread( + const std::function &, const std::string &); +public: + /// Re-throw a caught exception, if any. Can only be called after thread exit. + void rethrow() + { + sanity_check(!isRunning()); + if (m_exptr) + std::rethrow_exception(m_exptr); + } + +private: + // hide methods + LambdaThread(const std::string &name="") : Thread(name) {} + using Thread::start; + + std::function m_fn; + std::exception_ptr m_exptr; + + void *run() + { + try { + m_fn(); + } catch(...) { + m_exptr = std::current_exception(); + } + return nullptr; + }; +}; + +/** + * Run a lambda in a separate thread. + * + * Exceptions will be caught. + * @param fn function to run + * @param thread_name name for thread + * @return thread object of type `LambdaThread` +*/ +std::unique_ptr runInThread(const std::function &fn, + const std::string &thread_name = "") +{ + std::unique_ptr t(new LambdaThread(thread_name)); + t->m_fn = fn; + t->start(); + return t; +} diff --git a/src/threading/thread.h b/src/threading/thread.h index 2d0641b43..f915be2b3 100644 --- a/src/threading/thread.h +++ b/src/threading/thread.h @@ -59,6 +59,7 @@ public: Thread(const std::string &name=""); virtual ~Thread(); DISABLE_CLASS_COPY(Thread) + // Note: class cannot be moved since other references exist /* * Begins execution of a new thread at the pure virtual method Thread::run(). From 4e1679d2a25304ee5fb4a746fea398d220edeb5e Mon Sep 17 00:00:00 2001 From: sfan5 Date: Fri, 5 Apr 2024 13:00:34 +0200 Subject: [PATCH 24/34] Keep empty servers at bottom of list (#14511) --- builtin/common/misc_helpers.lua | 3 +++ builtin/mainmenu/serverlistmgr.lua | 37 +++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/builtin/common/misc_helpers.lua b/builtin/common/misc_helpers.lua index 90ac2ae4e..424eb26d2 100644 --- a/builtin/common/misc_helpers.lua +++ b/builtin/common/misc_helpers.lua @@ -490,6 +490,9 @@ end function table.insert_all(t, other) + if table.move then -- LuaJIT + return table.move(other, 1, #other, #t + 1, t) + end for i=1, #other do t[#t + 1] = other[i] end diff --git a/builtin/mainmenu/serverlistmgr.lua b/builtin/mainmenu/serverlistmgr.lua index 997768c15..601b42110 100644 --- a/builtin/mainmenu/serverlistmgr.lua +++ b/builtin/mainmenu/serverlistmgr.lua @@ -80,7 +80,8 @@ local WEIGHT_SORT = 2 -- how much the estimated latency contributes to the final ranking local WEIGHT_LATENCY = 1 -local function order_server_list(list) +--- @param list of servers, will be modified. +local function order_server_list_internal(list) -- calculate the scores local s1 = Normalizer:new() local s2 = Normalizer:new() @@ -99,22 +100,36 @@ local function order_server_list(list) s1 = s1:calc() s2 = s2:calc() - -- make a shallow copy and pre-calculate ordering - local res, order = {}, {} - for i = 1, #list do - local fav = list[i] - res[i] = fav - - local n = s1[fav] * WEIGHT_SORT + s2[fav] * WEIGHT_LATENCY - order[fav] = n + -- pre-calculate ordering + local order = {} + for _, fav in ipairs(list) do + order[fav] = s1[fav] * WEIGHT_SORT + s2[fav] * WEIGHT_LATENCY end -- now sort the list - table.sort(res, function(fav1, fav2) + table.sort(list, function(fav1, fav2) return order[fav1] > order[fav2] end) +end - return res +local function order_server_list(list) + -- split the list into two parts and sort them separately, to keep empty + -- servers at the bottom. + local nonempty, empty = {}, {} + + for _, fav in ipairs(list) do + if (fav.clients or 0) > 0 then + table.insert(nonempty, fav) + else + table.insert(empty, fav) + end + end + + order_server_list_internal(nonempty) + order_server_list_internal(empty) + + table.insert_all(nonempty, empty) + return nonempty end local public_downloading = false From 2d8e4df7bc72e6f8855efbebff6da5a2d00c81c4 Mon Sep 17 00:00:00 2001 From: 1F616EMO~nya Date: Fri, 5 Apr 2024 19:00:50 +0800 Subject: [PATCH 25/34] Allow optional actor ObjectRef value in node interaction calls (#14505) --- builtin/game/features.lua | 1 + doc/lua_api.md | 12 ++++++--- src/script/lua_api/l_env.cpp | 52 ++++++++++++++++++++++++++++-------- src/script/lua_api/l_env.h | 6 ++--- 4 files changed, 54 insertions(+), 17 deletions(-) diff --git a/builtin/game/features.lua b/builtin/game/features.lua index 874d3e885..286c3f146 100644 --- a/builtin/game/features.lua +++ b/builtin/game/features.lua @@ -39,6 +39,7 @@ core.features = { dynamic_add_media_filepath = true, lsystem_decoration_type = true, item_meta_range = true, + node_interaction_actor = true, } function core.has_feature(arg) diff --git a/doc/lua_api.md b/doc/lua_api.md index cca231c73..796a0f39e 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -5432,6 +5432,9 @@ Utilities lsystem_decoration_type = true, -- Overrideable pointing range using the itemstack meta key `"range"` (5.9.0) item_meta_range = true, + -- Allow passing an optional "actor" ObjectRef to the following functions: + -- minetest.place_node, minetest.dig_node, minetest.punch_node (5.9.0) + node_interaction_actor = true, } ``` @@ -6062,13 +6065,16 @@ Environment access * Returns a number between `0` and `15` * Currently it's the same as `math.floor(param1 / 16)`, except that it ensures compatibility. -* `minetest.place_node(pos, node)` +* `minetest.place_node(pos, node[, placer])` * Place node with the same effects that a player would cause -* `minetest.dig_node(pos)` + * `placer`: The ObjectRef that places the node (optional) +* `minetest.dig_node(pos[, digger])` * Dig node with the same effects that a player would cause + * `digger`: The ObjectRef that digs the node (optional) * Returns `true` if successful, `false` on failure (e.g. protected location) -* `minetest.punch_node(pos)` +* `minetest.punch_node(pos[, puncher])` * Punch node with the same effects that a player would cause + * `puncher`: The ObjectRef that punches the node (optional) * `minetest.spawn_falling_node(pos)` * Change node into falling node * Returns `true` and the ObjectRef of the spawned entity if successful, `false` on failure diff --git a/src/script/lua_api/l_env.cpp b/src/script/lua_api/l_env.cpp index b348a0f47..06dc27b2d 100644 --- a/src/script/lua_api/l_env.cpp +++ b/src/script/lua_api/l_env.cpp @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "lua_api/l_nodetimer.h" #include "lua_api/l_noise.h" #include "lua_api/l_vmanip.h" +#include "lua_api/l_object.h" #include "common/c_converter.h" #include "common/c_content.h" #include "scripting_server.h" @@ -416,7 +417,7 @@ int ModApiEnv::l_get_natural_light(lua_State *L) return 1; } -// place_node(pos, node) +// place_node(pos, node, [placer]) // pos = {x=num, y=num, z=num} int ModApiEnv::l_place_node(lua_State *L) { @@ -436,6 +437,7 @@ int ModApiEnv::l_place_node(lua_State *L) lua_pushboolean(L, false); return 1; } + // Create item to place std::optional item = ItemStack(ndef->get(n).name, 1, 0, idef); // Make pointed position @@ -443,13 +445,22 @@ int ModApiEnv::l_place_node(lua_State *L) pointed.type = POINTEDTHING_NODE; pointed.node_abovesurface = pos; pointed.node_undersurface = pos + v3s16(0,-1,0); - // Place it with a NULL placer (appears in Lua as nil) - bool success = scriptIfaceItem->item_OnPlace(item, nullptr, pointed); + + ServerActiveObject *placer = nullptr; + + if (!lua_isnoneornil(L, 3)) { + ObjectRef *ref = checkObject(L, 3); + placer = ObjectRef::getobject(ref); + } + + // Place it with a nullptr placer (appears in Lua as nil) + // or the given ObjectRef + bool success = scriptIfaceItem->item_OnPlace(item, placer, pointed); lua_pushboolean(L, success); return 1; } -// dig_node(pos) +// dig_node(pos, [digger]) // pos = {x=num, y=num, z=num} int ModApiEnv::l_dig_node(lua_State *L) { @@ -465,14 +476,23 @@ int ModApiEnv::l_dig_node(lua_State *L) lua_pushboolean(L, false); return 1; } - // Dig it out with a NULL digger (appears in Lua as a - // non-functional ObjectRef) - bool success = scriptIfaceNode->node_on_dig(pos, n, NULL); + + ServerActiveObject *digger = nullptr; + + if (!lua_isnoneornil(L, 2)) { + ObjectRef *ref = checkObject(L, 2); + digger = ObjectRef::getobject(ref); + } + + // Dig it out with a nullptr digger + // (appears in Lua as a non-functional ObjectRef) + // or the given ObjectRef + bool success = scriptIfaceNode->node_on_dig(pos, n, digger); lua_pushboolean(L, success); return 1; } -// punch_node(pos) +// punch_node(pos, [puncher]) // pos = {x=num, y=num, z=num} int ModApiEnv::l_punch_node(lua_State *L) { @@ -488,9 +508,19 @@ int ModApiEnv::l_punch_node(lua_State *L) lua_pushboolean(L, false); return 1; } - // Punch it with a NULL puncher (appears in Lua as a non-functional - // ObjectRef) - bool success = scriptIfaceNode->node_on_punch(pos, n, NULL, PointedThing()); + + ServerActiveObject *puncher = nullptr; + + if (!lua_isnoneornil(L, 2)) { + ObjectRef *ref = checkObject(L, 2); + puncher = ObjectRef::getobject(ref); + } + + // Punch it with a nullptr puncher + // (appears in Lua as a non-functional ObjectRef) + // or the given ObjectRef + // TODO: custom PointedThing (requires a converter function) + bool success = scriptIfaceNode->node_on_punch(pos, n, puncher, PointedThing()); lua_pushboolean(L, success); return 1; } diff --git a/src/script/lua_api/l_env.h b/src/script/lua_api/l_env.h index f657424d0..2ed0eb114 100644 --- a/src/script/lua_api/l_env.h +++ b/src/script/lua_api/l_env.h @@ -89,15 +89,15 @@ private: // timeofday: nil = current time, 0 = night, 0.5 = day static int l_get_natural_light(lua_State *L); - // place_node(pos, node) + // place_node(pos, node, [placer]) // pos = {x=num, y=num, z=num} static int l_place_node(lua_State *L); - // dig_node(pos) + // dig_node(pos, [digger]) // pos = {x=num, y=num, z=num} static int l_dig_node(lua_State *L); - // punch_node(pos) + // punch_node(pos, [puncher]) // pos = {x=num, y=num, z=num} static int l_punch_node(lua_State *L); From 9bee6d899b4587e5b740af4243b3c6b2ae160276 Mon Sep 17 00:00:00 2001 From: grorp Date: Fri, 5 Apr 2024 17:07:07 +0200 Subject: [PATCH 26/34] Fix invisible crosshair with effective HUD scaling < 1 --- src/client/hud.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/hud.cpp b/src/client/hud.cpp index 5a6f5d5c2..0c1b3e81f 100644 --- a/src/client/hud.cpp +++ b/src/client/hud.cpp @@ -801,7 +801,7 @@ void Hud::drawCrosshair() core::dimension2di orig_size(tex->getOriginalSize()); // Integer scaling to avoid artifacts, floor instead of round since too // small looks better than too large in this case. - core::dimension2di scaled_size = orig_size * std::floor(m_scale_factor); + core::dimension2di scaled_size = orig_size * std::max(std::floor(m_scale_factor), 1.0f); core::rect src_rect(orig_size); core::position2d pos(m_displaycenter.X - scaled_size.Width / 2, From ef0c19477c16a71452f0ecf3b5517d13992ef8c4 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Fri, 5 Apr 2024 14:11:49 +0200 Subject: [PATCH 27/34] Fix HAVE_RECENT_LJ false-positives --- src/CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bf849493a..0f06d3f4d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -698,11 +698,13 @@ include(CheckCSourceCompiles) set(CMAKE_REQUIRED_INCLUDES ${LUA_INCLUDE_DIR}) if(USE_LUAJIT) - set(CMAKE_REQUIRED_LIBRARIES ${LUA_LIBRARY}) + # libm usually required if statically linking + set(CMAKE_REQUIRED_LIBRARIES ${LUA_LIBRARY} m) # LuaJIT provides exactly zero ways to determine how recent it is (the version # is unchanged since 2017), however it happens that string buffers were added # after the changes which we care about so that works as an indicator. # (https://github.com/LuaJIT/LuaJIT/commit/4c6b669 March 2021) + # Note: This is no longer true as of August 2023, but we're keeping the old check. unset(HAVE_RECENT_LJ CACHE) check_symbol_exists(luaopen_string_buffer "lualib.h" HAVE_RECENT_LJ) if(NOT HAVE_RECENT_LJ) From f87994edc7d698e5c159455ef50ef5070a925762 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Fri, 5 Apr 2024 15:34:52 +0200 Subject: [PATCH 28/34] Use better randomseed for Lua too see e985b7a0bf5ddf4999330a5a1a878654f68b254e This solves a concrete issue with async workers generating the same random numbers (as discovered in #14518). --- builtin/init.lua | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/builtin/init.lua b/builtin/init.lua index cf4e8704e..49df70971 100644 --- a/builtin/init.lua +++ b/builtin/init.lua @@ -26,7 +26,15 @@ do core.print = nil -- don't pollute our namespace end end -math.randomseed(os.time()) + +do + -- Note that PUC Lua just calls srand() which is already initialized by C++, + -- but we don't want to rely on this implementation detail. + local seed = 1048576 * (os.time() % 1048576) + seed = seed + core.get_us_time() % 1048576 + math.randomseed(seed) +end + minetest = core -- Load other files From 7e4462e0acd4461e002e3d90157bdfe6ec41322c Mon Sep 17 00:00:00 2001 From: sfan5 Date: Fri, 5 Apr 2024 15:55:54 +0200 Subject: [PATCH 29/34] Better handling of temporary folders --- builtin/mainmenu/content/dlg_contentstore.lua | 5 +---- doc/menu_lua_api.md | 4 ++-- src/filesys.cpp | 19 +++++++++++++++++++ src/filesys.h | 12 +++++++++--- src/script/lua_api/l_mainmenu.cpp | 2 +- src/unittest/test.cpp | 9 ++------- 6 files changed, 34 insertions(+), 17 deletions(-) diff --git a/builtin/mainmenu/content/dlg_contentstore.lua b/builtin/mainmenu/content/dlg_contentstore.lua index 1d6b30cf6..f42b3d4e5 100644 --- a/builtin/mainmenu/content/dlg_contentstore.lua +++ b/builtin/mainmenu/content/dlg_contentstore.lua @@ -98,15 +98,12 @@ local function download_and_extract(param) local tempfolder = core.get_temp_path() if tempfolder ~= "" then - tempfolder = tempfolder .. DIR_DELIM .. "MT_" .. math.random(1, 1024000) if not core.extract_zip(filename, tempfolder) then tempfolder = nil end - else - tempfolder = nil end os.remove(filename) - if not tempfolder then + if not tempfolder or tempfolder == "" then return { msg = fgettext_ne("Failed to extract \"$1\" (unsupported file type or broken archive)", package.title), } diff --git a/doc/menu_lua_api.md b/doc/menu_lua_api.md index 979ef868f..cb1f07a90 100644 --- a/doc/menu_lua_api.md +++ b/doc/menu_lua_api.md @@ -93,8 +93,8 @@ Filesystem registered in the core (possible in async calls) * `core.get_cache_path()` -> path of cache * `core.get_temp_path([param])` (possible in async calls) - * `param`=true: returns path to a temporary file - otherwise: returns path to the temporary folder + * `param`=true: returns path to a newly created temporary file + * otherwise: returns path to a newly created temporary folder HTTP Requests diff --git a/src/filesys.cpp b/src/filesys.cpp index 9f493d7cf..1ae7f57aa 100644 --- a/src/filesys.cpp +++ b/src/filesys.cpp @@ -223,6 +223,16 @@ std::string CreateTempFile() return path; } +std::string CreateTempDir() +{ + std::string path = TempPath() + DIR_DELIM "MT_XXXXXX"; + _mktemp_s(&path[0], path.size() + 1); // modifies path + // will error if it already exists + if (!CreateDirectory(path.c_str(), nullptr)) + return ""; + return path; +} + bool CopyFileContents(const std::string &source, const std::string &target) { BOOL ok = CopyFileEx(source.c_str(), target.c_str(), nullptr, nullptr, @@ -446,6 +456,15 @@ std::string CreateTempFile() return path; } +std::string CreateTempDir() +{ + std::string path = TempPath() + DIR_DELIM "MT_XXXXXX"; + auto r = mkdtemp(&path[0]); // modifies path + if (!r) + return ""; + return path; +} + namespace { struct FileDeleter { void operator()(FILE *stream) { diff --git a/src/filesys.h b/src/filesys.h index 2a6476107..3edc3eef6 100644 --- a/src/filesys.h +++ b/src/filesys.h @@ -75,13 +75,19 @@ bool RecursiveDelete(const std::string &path); bool DeleteSingleFileOrEmptyDirectory(const std::string &path); -// Returns path to temp directory, can return "" on error +/// Returns path to temp directory. +/// You probably don't want to use this directly, see `CreateTempFile` or `CreateTempDir`. +/// @return path or "" on error std::string TempPath(); -// Returns path to securely-created temporary file (will already exist when this function returns) -// can return "" on error +/// Returns path to securely-created temporary file (will already exist when this function returns). +/// @return path or "" on error std::string CreateTempFile(); +/// Returns path to securely-created temporary directory (will already exist when this function returns). +/// @return path or "" on error +std::string CreateTempDir(); + /* Returns a list of subdirectories, including the path itself, but excluding hidden directories (whose names start with . or _) */ diff --git a/src/script/lua_api/l_mainmenu.cpp b/src/script/lua_api/l_mainmenu.cpp index fdb9e3740..a5913e807 100644 --- a/src/script/lua_api/l_mainmenu.cpp +++ b/src/script/lua_api/l_mainmenu.cpp @@ -729,7 +729,7 @@ int ModApiMainMenu::l_get_cache_path(lua_State *L) int ModApiMainMenu::l_get_temp_path(lua_State *L) { if (lua_isnoneornil(L, 1) || !lua_toboolean(L, 1)) - lua_pushstring(L, fs::TempPath().c_str()); + lua_pushstring(L, fs::CreateTempDir().c_str()); else lua_pushstring(L, fs::CreateTempFile().c_str()); return 1; diff --git a/src/unittest/test.cpp b/src/unittest/test.cpp index 1fd9001bf..e2d19b3e1 100644 --- a/src/unittest/test.cpp +++ b/src/unittest/test.cpp @@ -324,13 +324,8 @@ std::string TestBase::getTestTempDirectory() if (!m_test_dir.empty()) return m_test_dir; - char buf[32]; - porting::mt_snprintf(buf, sizeof(buf), "%08X", myrand()); - - m_test_dir = fs::TempPath() + DIR_DELIM "mttest_" + buf; - if (!fs::CreateDir(m_test_dir)) - UASSERT(false); - + m_test_dir = fs::CreateTempDir(); + UASSERT(!m_test_dir.empty()); return m_test_dir; } From 1d673ce07521f06626f03aebe4bdb00c671df3d0 Mon Sep 17 00:00:00 2001 From: DS Date: Sun, 7 Apr 2024 22:06:13 +0200 Subject: [PATCH 30/34] Print filenames in irrlicht png warnings (#14525) Makes warnings like this more informative: `WARNING[Main]: Irrlicht: PNG warning: iCCP: known incorrect sRGB profile` --- irr/src/CImageLoaderPNG.cpp | 12 +++++++++--- irr/src/CImageWriterPNG.cpp | 12 +++++++++--- irr/src/CNullDriver.cpp | 5 +++++ src/client/client.cpp | 2 +- src/client/imagesource.cpp | 2 +- 5 files changed, 25 insertions(+), 8 deletions(-) diff --git a/irr/src/CImageLoaderPNG.cpp b/irr/src/CImageLoaderPNG.cpp index 42cfc51d2..69a283890 100644 --- a/irr/src/CImageLoaderPNG.cpp +++ b/irr/src/CImageLoaderPNG.cpp @@ -18,14 +18,20 @@ namespace video // PNG function for error handling static void png_cpexcept_error(png_structp png_ptr, png_const_charp msg) { - os::Printer::log("PNG fatal error", msg, ELL_ERROR); + io::IReadFile *file = reinterpret_cast(png_get_error_ptr(png_ptr)); + std::string logmsg = std::string("PNG fatal error for ") + + file->getFileName().c_str() + ": " + msg; + os::Printer::log(logmsg.c_str(), ELL_ERROR); longjmp(png_jmpbuf(png_ptr), 1); } // PNG function for warning handling static void png_cpexcept_warn(png_structp png_ptr, png_const_charp msg) { - os::Printer::log("PNG warning", msg, ELL_WARNING); + io::IReadFile *file = reinterpret_cast(png_get_error_ptr(png_ptr)); + std::string logmsg = std::string("PNG warning for ") + + file->getFileName().c_str() + ": " + msg; + os::Printer::log(logmsg.c_str(), ELL_WARNING); } // PNG function for file reading @@ -88,7 +94,7 @@ IImage *CImageLoaderPng::loadImage(io::IReadFile *file) const // Allocate the png read struct png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, - NULL, (png_error_ptr)png_cpexcept_error, (png_error_ptr)png_cpexcept_warn); + file, (png_error_ptr)png_cpexcept_error, (png_error_ptr)png_cpexcept_warn); if (!png_ptr) { os::Printer::log("LOAD PNG: Internal PNG create read struct failure", file->getFileName(), ELL_ERROR); return 0; diff --git a/irr/src/CImageWriterPNG.cpp b/irr/src/CImageWriterPNG.cpp index 1c8a840ca..7bd3065c6 100644 --- a/irr/src/CImageWriterPNG.cpp +++ b/irr/src/CImageWriterPNG.cpp @@ -25,14 +25,20 @@ IImageWriter *createImageWriterPNG() // PNG function for error handling static void png_cpexcept_error(png_structp png_ptr, png_const_charp msg) { - os::Printer::log("PNG fatal error", msg, ELL_ERROR); + io::IWriteFile *file = reinterpret_cast(png_get_error_ptr(png_ptr)); + std::string logmsg = std::string("PNG fatal error for ") + + file->getFileName().c_str() + ": " + msg; + os::Printer::log(logmsg.c_str(), ELL_ERROR); longjmp(png_jmpbuf(png_ptr), 1); } // PNG function for warning handling static void png_cpexcept_warning(png_structp png_ptr, png_const_charp msg) { - os::Printer::log("PNG warning", msg, ELL_WARNING); + io::IWriteFile *file = reinterpret_cast(png_get_error_ptr(png_ptr)); + std::string logmsg = std::string("PNG warning for ") + + file->getFileName().c_str() + ": " + msg; + os::Printer::log(logmsg.c_str(), ELL_WARNING); } // PNG function for file writing @@ -66,7 +72,7 @@ bool CImageWriterPNG::writeImage(io::IWriteFile *file, IImage *image, u32 param) // Allocate the png write struct png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, - NULL, (png_error_ptr)png_cpexcept_error, (png_error_ptr)png_cpexcept_warning); + file, (png_error_ptr)png_cpexcept_error, (png_error_ptr)png_cpexcept_warning); if (!png_ptr) { os::Printer::log("PNGWriter: Internal PNG create write struct failure", file->getFileName(), ELL_ERROR); return false; diff --git a/irr/src/CNullDriver.cpp b/irr/src/CNullDriver.cpp index 347569e6a..92450e5d7 100644 --- a/irr/src/CNullDriver.cpp +++ b/irr/src/CNullDriver.cpp @@ -983,6 +983,11 @@ IImage *CNullDriver::createImageFromFile(io::IReadFile *file) continue; file->seek(0); // reset file position which might have changed due to previous loadImage calls + // avoid warnings if extension is wrong + if (!SurfaceLoader[i]->isALoadableFileFormat(file)) + continue; + + file->seek(0); if (IImage *image = SurfaceLoader[i]->loadImage(file)) return image; } diff --git a/src/client/client.cpp b/src/client/client.cpp index 789a37c36..ca3f91e78 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -799,7 +799,7 @@ bool Client::loadMedia(const std::string &data, const std::string &filename, video::IVideoDriver *vdrv = m_rendering_engine->get_video_driver(); io::IReadFile *rfile = irrfs->createMemoryReadFile( - data.c_str(), data.size(), "_tempreadfile"); + data.c_str(), data.size(), filename.c_str()); FATAL_ERROR_IF(!rfile, "Could not create irrlicht memory file."); diff --git a/src/client/imagesource.cpp b/src/client/imagesource.cpp index d401e98ed..4bcfb9804 100644 --- a/src/client/imagesource.cpp +++ b/src/client/imagesource.cpp @@ -1704,7 +1704,7 @@ bool ImageSource::generateImagePart(std::string_view part_of_name, auto *device = RenderingEngine::get_raw_device(); auto *fs = device->getFileSystem(); auto *vd = device->getVideoDriver(); - auto *memfile = fs->createMemoryReadFile(png.data(), png.size(), "__temp_png"); + auto *memfile = fs->createMemoryReadFile(png.data(), png.size(), "[png_tmpfile"); video::IImage* pngimg = vd->createImageFromFile(memfile); memfile->drop(); From e12db0c1828e84693d2fca2eaf2292006767d1f4 Mon Sep 17 00:00:00 2001 From: DS Date: Sun, 7 Apr 2024 22:06:34 +0200 Subject: [PATCH 31/34] Sounds: Queue more than two buffers if pitch is high (#14515) Pitch changes playback speed. So always enqueuing 2 buffers did not suffice (and it was unnecessary complicated). --- src/client/sound/playing_sound.cpp | 79 +++++++++++++++++++----------- src/client/sound/playing_sound.h | 4 +- src/client/sound/sound_constants.h | 12 +---- 3 files changed, 55 insertions(+), 40 deletions(-) diff --git a/src/client/sound/playing_sound.cpp b/src/client/sound/playing_sound.cpp index 3cc41fc50..622dbdef8 100644 --- a/src/client/sound/playing_sound.cpp +++ b/src/client/sound/playing_sound.cpp @@ -26,6 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "al_extensions.h" #include "debug.h" +#include "sound_constants.h" #include #include @@ -77,33 +78,27 @@ PlayingSound::PlayingSound(ALuint source_id, std::shared_ptr dat warn_if_al_error("when creating non-streaming sound"); } else { - // Start with 2 buffers - ALuint buf_ids[2]; + // Start with first buffer - // If m_next_sample_pos >= len_samples (happens only if not looped), one - // or both of buf_ids will be 0. Queuing 0 is a NOP. + // If m_next_sample_pos >= len_samples (happens only if not looped), buf0 + // will be 0. Queuing 0 is a NOP. auto [buf0, buf0_end, offset_in_buf0] = m_data->getOrLoadBufferAt(m_next_sample_pos); - buf_ids[0] = buf0; m_next_sample_pos = buf0_end; - if (m_looping && m_next_sample_pos == len_samples) - m_next_sample_pos = 0; - - auto [buf1, buf1_end, offset_in_buf1] = m_data->getOrLoadBufferAt(m_next_sample_pos); - buf_ids[1] = buf1; - m_next_sample_pos = buf1_end; - assert(offset_in_buf1 == 0); - - alSourceQueueBuffers(m_source_id, 2, buf_ids); + alSourceQueueBuffers(m_source_id, 1, &buf0); alSourcei(m_source_id, AL_SAMPLE_OFFSET, offset_in_buf0); - // We can't use AL_LOOPING because more buffers are queued later - // looping is therefore done manually + // We can't use AL_LOOPING because more buffers are queued later. + // Looping is therefore done manually. + // Sound is not dead if queue runs empty prematurely m_stopped_means_dead = false; warn_if_al_error("when creating streaming sound"); + + // Enqueue more buffers + stepStream(true); } // Set initial pos, volume, pitch @@ -129,23 +124,44 @@ PlayingSound::PlayingSound(ALuint source_id, std::shared_ptr dat setPitch(pitch); } -bool PlayingSound::stepStream() +bool PlayingSound::stepStream(bool playback_speed_changed) { if (isDead()) return false; - // unqueue finished buffers - ALint num_unqueued_bufs = 0; - alGetSourcei(m_source_id, AL_BUFFERS_PROCESSED, &num_unqueued_bufs); - if (num_unqueued_bufs == 0) - return true; - // We always have 2 buffers enqueued at most - SANITY_CHECK(num_unqueued_bufs <= 2); - ALuint unqueued_buffer_ids[2]; - alSourceUnqueueBuffers(m_source_id, num_unqueued_bufs, unqueued_buffer_ids); + // Unqueue finished buffers + ALint num_processed_bufs = 0; + alGetSourcei(m_source_id, AL_BUFFERS_PROCESSED, &num_processed_bufs); + if (num_processed_bufs == 0 && !playback_speed_changed) + return true; // Nothing to do + if (num_processed_bufs > 0) { + ALint num_to_unqueue = num_processed_bufs; + ALuint unqueued_buffer_ids[8]; + while (num_to_unqueue > 8) { + alSourceUnqueueBuffers(m_source_id, 8, unqueued_buffer_ids); + num_to_unqueue -= 8; + } + alSourceUnqueueBuffers(m_source_id, num_to_unqueue, unqueued_buffer_ids); + } - // Fill up again - for (ALint i = 0; i < num_unqueued_bufs; ++i) { + // Find out how many buffers we want to enqueue + f32 pitch = 1.0f; + alGetSourcef(m_source_id, AL_PITCH, &pitch); + ALint num_queued_bufs = 0; + alGetSourcei(m_source_id, AL_BUFFERS_QUEUED, &num_queued_bufs); + // Min. length of untouched buffers + const f32 playback_left = MIN_STREAM_BUFFER_LENGTH * std::max(0, num_queued_bufs - 1); + // Max. time until next stepStream() call, see also [Streaming of sounds] in + // sound_constants.h. + // Multiplied by pitch because pitch makes playback faster than real time. + // (Does not account for doppler effect, if we had that.) + // +0.1 seconds to accommodate hickups. + const f32 playback_until_next_check = (2.0f * STREAM_BIGSTEP_TIME + 0.1f) * pitch; + const f32 playback_to_fill_up = std::max(0.0f, playback_until_next_check - playback_left); + const int num_bufs_to_enqueue = std::ceil(playback_to_fill_up / MIN_STREAM_BUFFER_LENGTH); + + // Fill up + for (int i = 0; i < num_bufs_to_enqueue; ++i) { if (m_next_sample_pos == m_data->m_decode_info.length_samples) { // Reached end if (m_looping) { @@ -256,4 +272,11 @@ f32 PlayingSound::getGain() noexcept return gain; } +void PlayingSound::setPitch(f32 pitch) +{ + alSourcef(m_source_id, AL_PITCH, pitch); + if (isStreaming()) + stepStream(true); +} + } // namespace sound diff --git a/src/client/sound/playing_sound.h b/src/client/sound/playing_sound.h index dcc680e09..0f4c18fcb 100644 --- a/src/client/sound/playing_sound.h +++ b/src/client/sound/playing_sound.h @@ -63,7 +63,7 @@ public: DISABLE_CLASS_COPY(PlayingSound) // return false means streaming finished - bool stepStream(); + bool stepStream(bool playback_speed_changed = false); // retruns true if it wasn't fading already bool fade(f32 step, f32 target_gain) noexcept; @@ -77,7 +77,7 @@ public: f32 getGain() noexcept; - void setPitch(f32 pitch) noexcept { alSourcef(m_source_id, AL_PITCH, pitch); } + void setPitch(f32 pitch); bool isStreaming() const noexcept { return m_data->isStreaming(); } diff --git a/src/client/sound/sound_constants.h b/src/client/sound/sound_constants.h index d55575320..08aa2329b 100644 --- a/src/client/sound/sound_constants.h +++ b/src/client/sound/sound_constants.h @@ -89,14 +89,8 @@ with this program; if not, write to the Free Software Foundation, Inc., * In the worst case, a sound is stepped at the start of one bigstep and in the * end of the next bigstep. So between two stepStream()-calls lie at most * 2 * STREAM_BIGSTEP_TIME seconds. - * As there are always 2 sound buffers enqueued, at least one untouched full buffer - * is still available after the first stepStream(). - * If we take a MIN_STREAM_BUFFER_LENGTH > 2 * STREAM_BIGSTEP_TIME, we can hence - * not run into an empty queue. - * - * The MIN_STREAM_BUFFER_LENGTH needs to be a little bigger because of dtime jitter, - * other sounds that may have taken long to stepStream(), and sounds being played - * faster due to Doppler effect. + * We ensure that there are always enough untouched full buffers left such that + * we do not run into an empty queue in this time period, see stepStream(). * */ @@ -115,8 +109,6 @@ constexpr f32 STREAM_BIGSTEP_TIME = 0.3f; // step duration for the OpenALSoundManager thread, in seconds constexpr f32 SOUNDTHREAD_DTIME = 0.016f; -static_assert(MIN_STREAM_BUFFER_LENGTH > STREAM_BIGSTEP_TIME * 2.0f, - "See [Streaming of sounds]."); static_assert(SOUND_DURATION_MAX_SINGLE >= MIN_STREAM_BUFFER_LENGTH * 2.0f, "There's no benefit in streaming if we can't queue more than 2 buffers."); From fca60e2a415514aa9e9cae9c66775acddf2f496a Mon Sep 17 00:00:00 2001 From: Gregor Parzefall Date: Wed, 27 Mar 2024 18:45:36 +0100 Subject: [PATCH 32/34] Add SDL2 Java code --- .../main/java/org/libsdl/app/HIDDevice.java | 22 + .../app/HIDDeviceBLESteamController.java | 650 +++++ .../java/org/libsdl/app/HIDDeviceManager.java | 691 ++++++ .../java/org/libsdl/app/HIDDeviceUSB.java | 309 +++ .../app/src/main/java/org/libsdl/app/SDL.java | 86 + .../main/java/org/libsdl/app/SDLActivity.java | 2117 +++++++++++++++++ .../java/org/libsdl/app/SDLAudioManager.java | 514 ++++ .../org/libsdl/app/SDLControllerManager.java | 854 +++++++ .../main/java/org/libsdl/app/SDLSurface.java | 405 ++++ 9 files changed, 5648 insertions(+) create mode 100644 android/app/src/main/java/org/libsdl/app/HIDDevice.java create mode 100644 android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java create mode 100644 android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java create mode 100644 android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java create mode 100644 android/app/src/main/java/org/libsdl/app/SDL.java create mode 100644 android/app/src/main/java/org/libsdl/app/SDLActivity.java create mode 100644 android/app/src/main/java/org/libsdl/app/SDLAudioManager.java create mode 100644 android/app/src/main/java/org/libsdl/app/SDLControllerManager.java create mode 100644 android/app/src/main/java/org/libsdl/app/SDLSurface.java diff --git a/android/app/src/main/java/org/libsdl/app/HIDDevice.java b/android/app/src/main/java/org/libsdl/app/HIDDevice.java new file mode 100644 index 000000000..955df5d14 --- /dev/null +++ b/android/app/src/main/java/org/libsdl/app/HIDDevice.java @@ -0,0 +1,22 @@ +package org.libsdl.app; + +import android.hardware.usb.UsbDevice; + +interface HIDDevice +{ + public int getId(); + public int getVendorId(); + public int getProductId(); + public String getSerialNumber(); + public int getVersion(); + public String getManufacturerName(); + public String getProductName(); + public UsbDevice getDevice(); + public boolean open(); + public int sendFeatureReport(byte[] report); + public int sendOutputReport(byte[] report); + public boolean getFeatureReport(byte[] report); + public void setFrozen(boolean frozen); + public void close(); + public void shutdown(); +} diff --git a/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java b/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java new file mode 100644 index 000000000..ee5521fd5 --- /dev/null +++ b/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java @@ -0,0 +1,650 @@ +package org.libsdl.app; + +import android.content.Context; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothGattService; +import android.hardware.usb.UsbDevice; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.os.*; + +//import com.android.internal.util.HexDump; + +import java.lang.Runnable; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.UUID; + +class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice { + + private static final String TAG = "hidapi"; + private HIDDeviceManager mManager; + private BluetoothDevice mDevice; + private int mDeviceId; + private BluetoothGatt mGatt; + private boolean mIsRegistered = false; + private boolean mIsConnected = false; + private boolean mIsChromebook = false; + private boolean mIsReconnecting = false; + private boolean mFrozen = false; + private LinkedList mOperations; + GattOperation mCurrentOperation = null; + private Handler mHandler; + + private static final int TRANSPORT_AUTO = 0; + private static final int TRANSPORT_BREDR = 1; + private static final int TRANSPORT_LE = 2; + + private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000; + + static public final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3"); + static public final UUID inputCharacteristic = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3"); + static public final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3"); + static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 }; + + static class GattOperation { + private enum Operation { + CHR_READ, + CHR_WRITE, + ENABLE_NOTIFICATION + } + + Operation mOp; + UUID mUuid; + byte[] mValue; + BluetoothGatt mGatt; + boolean mResult = true; + + private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) { + mGatt = gatt; + mOp = operation; + mUuid = uuid; + } + + private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) { + mGatt = gatt; + mOp = operation; + mUuid = uuid; + mValue = value; + } + + public void run() { + // This is executed in main thread + BluetoothGattCharacteristic chr; + + switch (mOp) { + case CHR_READ: + chr = getCharacteristic(mUuid); + //Log.v(TAG, "Reading characteristic " + chr.getUuid()); + if (!mGatt.readCharacteristic(chr)) { + Log.e(TAG, "Unable to read characteristic " + mUuid.toString()); + mResult = false; + break; + } + mResult = true; + break; + case CHR_WRITE: + chr = getCharacteristic(mUuid); + //Log.v(TAG, "Writing characteristic " + chr.getUuid() + " value=" + HexDump.toHexString(value)); + chr.setValue(mValue); + if (!mGatt.writeCharacteristic(chr)) { + Log.e(TAG, "Unable to write characteristic " + mUuid.toString()); + mResult = false; + break; + } + mResult = true; + break; + case ENABLE_NOTIFICATION: + chr = getCharacteristic(mUuid); + //Log.v(TAG, "Writing descriptor of " + chr.getUuid()); + if (chr != null) { + BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")); + if (cccd != null) { + int properties = chr.getProperties(); + byte[] value; + if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == BluetoothGattCharacteristic.PROPERTY_NOTIFY) { + value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE; + } else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) == BluetoothGattCharacteristic.PROPERTY_INDICATE) { + value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE; + } else { + Log.e(TAG, "Unable to start notifications on input characteristic"); + mResult = false; + return; + } + + mGatt.setCharacteristicNotification(chr, true); + cccd.setValue(value); + if (!mGatt.writeDescriptor(cccd)) { + Log.e(TAG, "Unable to write descriptor " + mUuid.toString()); + mResult = false; + return; + } + mResult = true; + } + } + } + } + + public boolean finish() { + return mResult; + } + + private BluetoothGattCharacteristic getCharacteristic(UUID uuid) { + BluetoothGattService valveService = mGatt.getService(steamControllerService); + if (valveService == null) + return null; + return valveService.getCharacteristic(uuid); + } + + static public GattOperation readCharacteristic(BluetoothGatt gatt, UUID uuid) { + return new GattOperation(gatt, Operation.CHR_READ, uuid); + } + + static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, byte[] value) { + return new GattOperation(gatt, Operation.CHR_WRITE, uuid, value); + } + + static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) { + return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid); + } + } + + public HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) { + mManager = manager; + mDevice = device; + mDeviceId = mManager.getDeviceIDForIdentifier(getIdentifier()); + mIsRegistered = false; + mIsChromebook = mManager.getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); + mOperations = new LinkedList(); + mHandler = new Handler(Looper.getMainLooper()); + + mGatt = connectGatt(); + // final HIDDeviceBLESteamController finalThis = this; + // mHandler.postDelayed(new Runnable() { + // @Override + // public void run() { + // finalThis.checkConnectionForChromebookIssue(); + // } + // }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL); + } + + public String getIdentifier() { + return String.format("SteamController.%s", mDevice.getAddress()); + } + + public BluetoothGatt getGatt() { + return mGatt; + } + + // Because on Chromebooks we show up as a dual-mode device, it will attempt to connect TRANSPORT_AUTO, which will use TRANSPORT_BREDR instead + // of TRANSPORT_LE. Let's force ourselves to connect low energy. + private BluetoothGatt connectGatt(boolean managed) { + if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) { + try { + return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE); + } catch (Exception e) { + return mDevice.connectGatt(mManager.getContext(), managed, this); + } + } else { + return mDevice.connectGatt(mManager.getContext(), managed, this); + } + } + + private BluetoothGatt connectGatt() { + return connectGatt(false); + } + + protected int getConnectionState() { + + Context context = mManager.getContext(); + if (context == null) { + // We are lacking any context to get our Bluetooth information. We'll just assume disconnected. + return BluetoothProfile.STATE_DISCONNECTED; + } + + BluetoothManager btManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE); + if (btManager == null) { + // This device doesn't support Bluetooth. We should never be here, because how did + // we instantiate a device to start with? + return BluetoothProfile.STATE_DISCONNECTED; + } + + return btManager.getConnectionState(mDevice, BluetoothProfile.GATT); + } + + public void reconnect() { + + if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) { + mGatt.disconnect(); + mGatt = connectGatt(); + } + + } + + protected void checkConnectionForChromebookIssue() { + if (!mIsChromebook) { + // We only do this on Chromebooks, because otherwise it's really annoying to just attempt + // over and over. + return; + } + + int connectionState = getConnectionState(); + + switch (connectionState) { + case BluetoothProfile.STATE_CONNECTED: + if (!mIsConnected) { + // We are in the Bad Chromebook Place. We can force a disconnect + // to try to recover. + Log.v(TAG, "Chromebook: We are in a very bad state; the controller shows as connected in the underlying Bluetooth layer, but we never received a callback. Forcing a reconnect."); + mIsReconnecting = true; + mGatt.disconnect(); + mGatt = connectGatt(false); + break; + } + else if (!isRegistered()) { + if (mGatt.getServices().size() > 0) { + Log.v(TAG, "Chromebook: We are connected to a controller, but never got our registration. Trying to recover."); + probeService(this); + } + else { + Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services. Trying to recover."); + mIsReconnecting = true; + mGatt.disconnect(); + mGatt = connectGatt(false); + break; + } + } + else { + Log.v(TAG, "Chromebook: We are connected, and registered. Everything's good!"); + return; + } + break; + + case BluetoothProfile.STATE_DISCONNECTED: + Log.v(TAG, "Chromebook: We have either been disconnected, or the Chromebook BtGatt.ContextMap bug has bitten us. Attempting a disconnect/reconnect, but we may not be able to recover."); + + mIsReconnecting = true; + mGatt.disconnect(); + mGatt = connectGatt(false); + break; + + case BluetoothProfile.STATE_CONNECTING: + Log.v(TAG, "Chromebook: We're still trying to connect. Waiting a bit longer."); + break; + } + + final HIDDeviceBLESteamController finalThis = this; + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + finalThis.checkConnectionForChromebookIssue(); + } + }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL); + } + + private boolean isRegistered() { + return mIsRegistered; + } + + private void setRegistered() { + mIsRegistered = true; + } + + private boolean probeService(HIDDeviceBLESteamController controller) { + + if (isRegistered()) { + return true; + } + + if (!mIsConnected) { + return false; + } + + Log.v(TAG, "probeService controller=" + controller); + + for (BluetoothGattService service : mGatt.getServices()) { + if (service.getUuid().equals(steamControllerService)) { + Log.v(TAG, "Found Valve steam controller service " + service.getUuid()); + + for (BluetoothGattCharacteristic chr : service.getCharacteristics()) { + if (chr.getUuid().equals(inputCharacteristic)) { + Log.v(TAG, "Found input characteristic"); + // Start notifications + BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")); + if (cccd != null) { + enableNotification(chr.getUuid()); + } + } + } + return true; + } + } + + if ((mGatt.getServices().size() == 0) && mIsChromebook && !mIsReconnecting) { + Log.e(TAG, "Chromebook: Discovered services were empty; this almost certainly means the BtGatt.ContextMap bug has bitten us."); + mIsConnected = false; + mIsReconnecting = true; + mGatt.disconnect(); + mGatt = connectGatt(false); + } + + return false; + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void finishCurrentGattOperation() { + GattOperation op = null; + synchronized (mOperations) { + if (mCurrentOperation != null) { + op = mCurrentOperation; + mCurrentOperation = null; + } + } + if (op != null) { + boolean result = op.finish(); // TODO: Maybe in main thread as well? + + // Our operation failed, let's add it back to the beginning of our queue. + if (!result) { + mOperations.addFirst(op); + } + } + executeNextGattOperation(); + } + + private void executeNextGattOperation() { + synchronized (mOperations) { + if (mCurrentOperation != null) + return; + + if (mOperations.isEmpty()) + return; + + mCurrentOperation = mOperations.removeFirst(); + } + + // Run in main thread + mHandler.post(new Runnable() { + @Override + public void run() { + synchronized (mOperations) { + if (mCurrentOperation == null) { + Log.e(TAG, "Current operation null in executor?"); + return; + } + + mCurrentOperation.run(); + // now wait for the GATT callback and when it comes, finish this operation + } + } + }); + } + + private void queueGattOperation(GattOperation op) { + synchronized (mOperations) { + mOperations.add(op); + } + executeNextGattOperation(); + } + + private void enableNotification(UUID chrUuid) { + GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid); + queueGattOperation(op); + } + + public void writeCharacteristic(UUID uuid, byte[] value) { + GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value); + queueGattOperation(op); + } + + public void readCharacteristic(UUID uuid) { + GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid); + queueGattOperation(op); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////// + ////////////// BluetoothGattCallback overridden methods + ////////////////////////////////////////////////////////////////////////////////////////////////////// + + public void onConnectionStateChange(BluetoothGatt g, int status, int newState) { + //Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState); + mIsReconnecting = false; + if (newState == 2) { + mIsConnected = true; + // Run directly, without GattOperation + if (!isRegistered()) { + mHandler.post(new Runnable() { + @Override + public void run() { + mGatt.discoverServices(); + } + }); + } + } + else if (newState == 0) { + mIsConnected = false; + } + + // Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent. + } + + public void onServicesDiscovered(BluetoothGatt gatt, int status) { + //Log.v(TAG, "onServicesDiscovered status=" + status); + if (status == 0) { + if (gatt.getServices().size() == 0) { + Log.v(TAG, "onServicesDiscovered returned zero services; something has gone horribly wrong down in Android's Bluetooth stack."); + mIsReconnecting = true; + mIsConnected = false; + gatt.disconnect(); + mGatt = connectGatt(false); + } + else { + probeService(this); + } + } + } + + public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + //Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid()); + + if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) { + mManager.HIDDeviceFeatureReport(getId(), characteristic.getValue()); + } + + finishCurrentGattOperation(); + } + + public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + //Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid()); + + if (characteristic.getUuid().equals(reportCharacteristic)) { + // Only register controller with the native side once it has been fully configured + if (!isRegistered()) { + Log.v(TAG, "Registering Steam Controller with ID: " + getId()); + mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0); + setRegistered(); + } + } + + finishCurrentGattOperation(); + } + + public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + // Enable this for verbose logging of controller input reports + //Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue())); + + if (characteristic.getUuid().equals(inputCharacteristic) && !mFrozen) { + mManager.HIDDeviceInputReport(getId(), characteristic.getValue()); + } + } + + public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + //Log.v(TAG, "onDescriptorRead status=" + status); + } + + public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + BluetoothGattCharacteristic chr = descriptor.getCharacteristic(); + //Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid()); + + if (chr.getUuid().equals(inputCharacteristic)) { + boolean hasWrittenInputDescriptor = true; + BluetoothGattCharacteristic reportChr = chr.getService().getCharacteristic(reportCharacteristic); + if (reportChr != null) { + Log.v(TAG, "Writing report characteristic to enter valve mode"); + reportChr.setValue(enterValveMode); + gatt.writeCharacteristic(reportChr); + } + } + + finishCurrentGattOperation(); + } + + public void onReliableWriteCompleted(BluetoothGatt gatt, int status) { + //Log.v(TAG, "onReliableWriteCompleted status=" + status); + } + + public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { + //Log.v(TAG, "onReadRemoteRssi status=" + status); + } + + public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { + //Log.v(TAG, "onMtuChanged status=" + status); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////// + //////// Public API + ////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public int getId() { + return mDeviceId; + } + + @Override + public int getVendorId() { + // Valve Corporation + final int VALVE_USB_VID = 0x28DE; + return VALVE_USB_VID; + } + + @Override + public int getProductId() { + // We don't have an easy way to query from the Bluetooth device, but we know what it is + final int D0G_BLE2_PID = 0x1106; + return D0G_BLE2_PID; + } + + @Override + public String getSerialNumber() { + // This will be read later via feature report by Steam + return "12345"; + } + + @Override + public int getVersion() { + return 0; + } + + @Override + public String getManufacturerName() { + return "Valve Corporation"; + } + + @Override + public String getProductName() { + return "Steam Controller"; + } + + @Override + public UsbDevice getDevice() { + return null; + } + + @Override + public boolean open() { + return true; + } + + @Override + public int sendFeatureReport(byte[] report) { + if (!isRegistered()) { + Log.e(TAG, "Attempted sendFeatureReport before Steam Controller is registered!"); + if (mIsConnected) { + probeService(this); + } + return -1; + } + + // We need to skip the first byte, as that doesn't go over the air + byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1); + //Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(actual_report)); + writeCharacteristic(reportCharacteristic, actual_report); + return report.length; + } + + @Override + public int sendOutputReport(byte[] report) { + if (!isRegistered()) { + Log.e(TAG, "Attempted sendOutputReport before Steam Controller is registered!"); + if (mIsConnected) { + probeService(this); + } + return -1; + } + + //Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(report)); + writeCharacteristic(reportCharacteristic, report); + return report.length; + } + + @Override + public boolean getFeatureReport(byte[] report) { + if (!isRegistered()) { + Log.e(TAG, "Attempted getFeatureReport before Steam Controller is registered!"); + if (mIsConnected) { + probeService(this); + } + return false; + } + + //Log.v(TAG, "getFeatureReport"); + readCharacteristic(reportCharacteristic); + return true; + } + + @Override + public void close() { + } + + @Override + public void setFrozen(boolean frozen) { + mFrozen = frozen; + } + + @Override + public void shutdown() { + close(); + + BluetoothGatt g = mGatt; + if (g != null) { + g.disconnect(); + g.close(); + mGatt = null; + } + mManager = null; + mIsRegistered = false; + mIsConnected = false; + mOperations.clear(); + } + +} + diff --git a/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java b/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java new file mode 100644 index 000000000..e7281fdf2 --- /dev/null +++ b/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java @@ -0,0 +1,691 @@ +package org.libsdl.app; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothProfile; +import android.os.Build; +import android.util.Log; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.hardware.usb.*; +import android.os.Handler; +import android.os.Looper; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + +public class HIDDeviceManager { + private static final String TAG = "hidapi"; + private static final String ACTION_USB_PERMISSION = "org.libsdl.app.USB_PERMISSION"; + + private static HIDDeviceManager sManager; + private static int sManagerRefCount = 0; + + public static HIDDeviceManager acquire(Context context) { + if (sManagerRefCount == 0) { + sManager = new HIDDeviceManager(context); + } + ++sManagerRefCount; + return sManager; + } + + public static void release(HIDDeviceManager manager) { + if (manager == sManager) { + --sManagerRefCount; + if (sManagerRefCount == 0) { + sManager.close(); + sManager = null; + } + } + } + + private Context mContext; + private HashMap mDevicesById = new HashMap(); + private HashMap mBluetoothDevices = new HashMap(); + private int mNextDeviceId = 0; + private SharedPreferences mSharedPreferences = null; + private boolean mIsChromebook = false; + private UsbManager mUsbManager; + private Handler mHandler; + private BluetoothManager mBluetoothManager; + private List mLastBluetoothDevices; + + private final BroadcastReceiver mUsbBroadcast = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) { + UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + handleUsbDeviceAttached(usbDevice); + } else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) { + UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + handleUsbDeviceDetached(usbDevice); + } else if (action.equals(HIDDeviceManager.ACTION_USB_PERMISSION)) { + UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + handleUsbDevicePermission(usbDevice, intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)); + } + } + }; + + private final BroadcastReceiver mBluetoothBroadcast = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + // Bluetooth device was connected. If it was a Steam Controller, handle it + if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) { + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + Log.d(TAG, "Bluetooth device connected: " + device); + + if (isSteamController(device)) { + connectBluetoothDevice(device); + } + } + + // Bluetooth device was disconnected, remove from controller manager (if any) + if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) { + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + Log.d(TAG, "Bluetooth device disconnected: " + device); + + disconnectBluetoothDevice(device); + } + } + }; + + private HIDDeviceManager(final Context context) { + mContext = context; + + HIDDeviceRegisterCallback(); + + mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE); + mIsChromebook = mContext.getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); + +// if (shouldClear) { +// SharedPreferences.Editor spedit = mSharedPreferences.edit(); +// spedit.clear(); +// spedit.commit(); +// } +// else + { + mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0); + } + } + + public Context getContext() { + return mContext; + } + + public int getDeviceIDForIdentifier(String identifier) { + SharedPreferences.Editor spedit = mSharedPreferences.edit(); + + int result = mSharedPreferences.getInt(identifier, 0); + if (result == 0) { + result = mNextDeviceId++; + spedit.putInt("next_device_id", mNextDeviceId); + } + + spedit.putInt(identifier, result); + spedit.commit(); + return result; + } + + private void initializeUSB() { + mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE); + if (mUsbManager == null) { + return; + } + + /* + // Logging + for (UsbDevice device : mUsbManager.getDeviceList().values()) { + Log.i(TAG,"Path: " + device.getDeviceName()); + Log.i(TAG,"Manufacturer: " + device.getManufacturerName()); + Log.i(TAG,"Product: " + device.getProductName()); + Log.i(TAG,"ID: " + device.getDeviceId()); + Log.i(TAG,"Class: " + device.getDeviceClass()); + Log.i(TAG,"Protocol: " + device.getDeviceProtocol()); + Log.i(TAG,"Vendor ID " + device.getVendorId()); + Log.i(TAG,"Product ID: " + device.getProductId()); + Log.i(TAG,"Interface count: " + device.getInterfaceCount()); + Log.i(TAG,"---------------------------------------"); + + // Get interface details + for (int index = 0; index < device.getInterfaceCount(); index++) { + UsbInterface mUsbInterface = device.getInterface(index); + Log.i(TAG," ***** *****"); + Log.i(TAG," Interface index: " + index); + Log.i(TAG," Interface ID: " + mUsbInterface.getId()); + Log.i(TAG," Interface class: " + mUsbInterface.getInterfaceClass()); + Log.i(TAG," Interface subclass: " + mUsbInterface.getInterfaceSubclass()); + Log.i(TAG," Interface protocol: " + mUsbInterface.getInterfaceProtocol()); + Log.i(TAG," Endpoint count: " + mUsbInterface.getEndpointCount()); + + // Get endpoint details + for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++) + { + UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi); + Log.i(TAG," ++++ ++++ ++++"); + Log.i(TAG," Endpoint index: " + epi); + Log.i(TAG," Attributes: " + mEndpoint.getAttributes()); + Log.i(TAG," Direction: " + mEndpoint.getDirection()); + Log.i(TAG," Number: " + mEndpoint.getEndpointNumber()); + Log.i(TAG," Interval: " + mEndpoint.getInterval()); + Log.i(TAG," Packet size: " + mEndpoint.getMaxPacketSize()); + Log.i(TAG," Type: " + mEndpoint.getType()); + } + } + } + Log.i(TAG," No more devices connected."); + */ + + // Register for USB broadcasts and permission completions + IntentFilter filter = new IntentFilter(); + filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); + filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); + filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION); + mContext.registerReceiver(mUsbBroadcast, filter); + + for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) { + handleUsbDeviceAttached(usbDevice); + } + } + + UsbManager getUSBManager() { + return mUsbManager; + } + + private void shutdownUSB() { + try { + mContext.unregisterReceiver(mUsbBroadcast); + } catch (Exception e) { + // We may not have registered, that's okay + } + } + + private boolean isHIDDeviceInterface(UsbDevice usbDevice, UsbInterface usbInterface) { + if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) { + return true; + } + if (isXbox360Controller(usbDevice, usbInterface) || isXboxOneController(usbDevice, usbInterface)) { + return true; + } + return false; + } + + private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterface) { + final int XB360_IFACE_SUBCLASS = 93; + final int XB360_IFACE_PROTOCOL = 1; // Wired + final int XB360W_IFACE_PROTOCOL = 129; // Wireless + final int[] SUPPORTED_VENDORS = { + 0x0079, // GPD Win 2 + 0x044f, // Thrustmaster + 0x045e, // Microsoft + 0x046d, // Logitech + 0x056e, // Elecom + 0x06a3, // Saitek + 0x0738, // Mad Catz + 0x07ff, // Mad Catz + 0x0e6f, // PDP + 0x0f0d, // Hori + 0x1038, // SteelSeries + 0x11c9, // Nacon + 0x12ab, // Unknown + 0x1430, // RedOctane + 0x146b, // BigBen + 0x1532, // Razer Sabertooth + 0x15e4, // Numark + 0x162e, // Joytech + 0x1689, // Razer Onza + 0x1949, // Lab126, Inc. + 0x1bad, // Harmonix + 0x20d6, // PowerA + 0x24c6, // PowerA + 0x2c22, // Qanba + 0x2dc8, // 8BitDo + 0x9886, // ASTRO Gaming + }; + + if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC && + usbInterface.getInterfaceSubclass() == XB360_IFACE_SUBCLASS && + (usbInterface.getInterfaceProtocol() == XB360_IFACE_PROTOCOL || + usbInterface.getInterfaceProtocol() == XB360W_IFACE_PROTOCOL)) { + int vendor_id = usbDevice.getVendorId(); + for (int supportedVid : SUPPORTED_VENDORS) { + if (vendor_id == supportedVid) { + return true; + } + } + } + return false; + } + + private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterface) { + final int XB1_IFACE_SUBCLASS = 71; + final int XB1_IFACE_PROTOCOL = 208; + final int[] SUPPORTED_VENDORS = { + 0x03f0, // HP + 0x044f, // Thrustmaster + 0x045e, // Microsoft + 0x0738, // Mad Catz + 0x0e6f, // PDP + 0x0f0d, // Hori + 0x10f5, // Turtle Beach + 0x1532, // Razer Wildcat + 0x20d6, // PowerA + 0x24c6, // PowerA + 0x2dc8, // 8BitDo + 0x2e24, // Hyperkin + 0x3537, // GameSir + }; + + if (usbInterface.getId() == 0 && + usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC && + usbInterface.getInterfaceSubclass() == XB1_IFACE_SUBCLASS && + usbInterface.getInterfaceProtocol() == XB1_IFACE_PROTOCOL) { + int vendor_id = usbDevice.getVendorId(); + for (int supportedVid : SUPPORTED_VENDORS) { + if (vendor_id == supportedVid) { + return true; + } + } + } + return false; + } + + private void handleUsbDeviceAttached(UsbDevice usbDevice) { + connectHIDDeviceUSB(usbDevice); + } + + private void handleUsbDeviceDetached(UsbDevice usbDevice) { + List devices = new ArrayList(); + for (HIDDevice device : mDevicesById.values()) { + if (usbDevice.equals(device.getDevice())) { + devices.add(device.getId()); + } + } + for (int id : devices) { + HIDDevice device = mDevicesById.get(id); + mDevicesById.remove(id); + device.shutdown(); + HIDDeviceDisconnected(id); + } + } + + private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_granted) { + for (HIDDevice device : mDevicesById.values()) { + if (usbDevice.equals(device.getDevice())) { + boolean opened = false; + if (permission_granted) { + opened = device.open(); + } + HIDDeviceOpenResult(device.getId(), opened); + } + } + } + + private void connectHIDDeviceUSB(UsbDevice usbDevice) { + synchronized (this) { + int interface_mask = 0; + for (int interface_index = 0; interface_index < usbDevice.getInterfaceCount(); interface_index++) { + UsbInterface usbInterface = usbDevice.getInterface(interface_index); + if (isHIDDeviceInterface(usbDevice, usbInterface)) { + // Check to see if we've already added this interface + // This happens with the Xbox Series X controller which has a duplicate interface 0, which is inactive + int interface_id = usbInterface.getId(); + if ((interface_mask & (1 << interface_id)) != 0) { + continue; + } + interface_mask |= (1 << interface_id); + + HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index); + int id = device.getId(); + mDevicesById.put(id, device); + HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol()); + } + } + } + } + + private void initializeBluetooth() { + Log.d(TAG, "Initializing Bluetooth"); + + if (Build.VERSION.SDK_INT >= 31 /* Android 12 */ && + mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH_CONNECT, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) { + Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH_CONNECT"); + return; + } + + if (Build.VERSION.SDK_INT <= 30 /* Android 11.0 (R) */ && + mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) { + Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH"); + return; + } + + if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) || (Build.VERSION.SDK_INT < 18 /* Android 4.3 (JELLY_BEAN_MR2) */)) { + Log.d(TAG, "Couldn't initialize Bluetooth, this version of Android does not support Bluetooth LE"); + return; + } + + // Find bonded bluetooth controllers and create SteamControllers for them + mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE); + if (mBluetoothManager == null) { + // This device doesn't support Bluetooth. + return; + } + + BluetoothAdapter btAdapter = mBluetoothManager.getAdapter(); + if (btAdapter == null) { + // This device has Bluetooth support in the codebase, but has no available adapters. + return; + } + + // Get our bonded devices. + for (BluetoothDevice device : btAdapter.getBondedDevices()) { + + Log.d(TAG, "Bluetooth device available: " + device); + if (isSteamController(device)) { + connectBluetoothDevice(device); + } + + } + + // NOTE: These don't work on Chromebooks, to my undying dismay. + IntentFilter filter = new IntentFilter(); + filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); + filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); + mContext.registerReceiver(mBluetoothBroadcast, filter); + + if (mIsChromebook) { + mHandler = new Handler(Looper.getMainLooper()); + mLastBluetoothDevices = new ArrayList(); + + // final HIDDeviceManager finalThis = this; + // mHandler.postDelayed(new Runnable() { + // @Override + // public void run() { + // finalThis.chromebookConnectionHandler(); + // } + // }, 5000); + } + } + + private void shutdownBluetooth() { + try { + mContext.unregisterReceiver(mBluetoothBroadcast); + } catch (Exception e) { + // We may not have registered, that's okay + } + } + + // Chromebooks do not pass along ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED properly. + // This function provides a sort of dummy version of that, watching for changes in the + // connected devices and attempting to add controllers as things change. + public void chromebookConnectionHandler() { + if (!mIsChromebook) { + return; + } + + ArrayList disconnected = new ArrayList(); + ArrayList connected = new ArrayList(); + + List currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT); + + for (BluetoothDevice bluetoothDevice : currentConnected) { + if (!mLastBluetoothDevices.contains(bluetoothDevice)) { + connected.add(bluetoothDevice); + } + } + for (BluetoothDevice bluetoothDevice : mLastBluetoothDevices) { + if (!currentConnected.contains(bluetoothDevice)) { + disconnected.add(bluetoothDevice); + } + } + + mLastBluetoothDevices = currentConnected; + + for (BluetoothDevice bluetoothDevice : disconnected) { + disconnectBluetoothDevice(bluetoothDevice); + } + for (BluetoothDevice bluetoothDevice : connected) { + connectBluetoothDevice(bluetoothDevice); + } + + final HIDDeviceManager finalThis = this; + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + finalThis.chromebookConnectionHandler(); + } + }, 10000); + } + + public boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) { + Log.v(TAG, "connectBluetoothDevice device=" + bluetoothDevice); + synchronized (this) { + if (mBluetoothDevices.containsKey(bluetoothDevice)) { + Log.v(TAG, "Steam controller with address " + bluetoothDevice + " already exists, attempting reconnect"); + + HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice); + device.reconnect(); + + return false; + } + HIDDeviceBLESteamController device = new HIDDeviceBLESteamController(this, bluetoothDevice); + int id = device.getId(); + mBluetoothDevices.put(bluetoothDevice, device); + mDevicesById.put(id, device); + + // The Steam Controller will mark itself connected once initialization is complete + } + return true; + } + + public void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) { + synchronized (this) { + HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice); + if (device == null) + return; + + int id = device.getId(); + mBluetoothDevices.remove(bluetoothDevice); + mDevicesById.remove(id); + device.shutdown(); + HIDDeviceDisconnected(id); + } + } + + public boolean isSteamController(BluetoothDevice bluetoothDevice) { + // Sanity check. If you pass in a null device, by definition it is never a Steam Controller. + if (bluetoothDevice == null) { + return false; + } + + // If the device has no local name, we really don't want to try an equality check against it. + if (bluetoothDevice.getName() == null) { + return false; + } + + return bluetoothDevice.getName().equals("SteamController") && ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) != 0); + } + + private void close() { + shutdownUSB(); + shutdownBluetooth(); + synchronized (this) { + for (HIDDevice device : mDevicesById.values()) { + device.shutdown(); + } + mDevicesById.clear(); + mBluetoothDevices.clear(); + HIDDeviceReleaseCallback(); + } + } + + public void setFrozen(boolean frozen) { + synchronized (this) { + for (HIDDevice device : mDevicesById.values()) { + device.setFrozen(frozen); + } + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////////////////////////// + + private HIDDevice getDevice(int id) { + synchronized (this) { + HIDDevice result = mDevicesById.get(id); + if (result == null) { + Log.v(TAG, "No device for id: " + id); + Log.v(TAG, "Available devices: " + mDevicesById.keySet()); + } + return result; + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////// + ////////// JNI interface functions + ////////////////////////////////////////////////////////////////////////////////////////////////////// + + public boolean initialize(boolean usb, boolean bluetooth) { + Log.v(TAG, "initialize(" + usb + ", " + bluetooth + ")"); + + if (usb) { + initializeUSB(); + } + if (bluetooth) { + initializeBluetooth(); + } + return true; + } + + public boolean openDevice(int deviceID) { + Log.v(TAG, "openDevice deviceID=" + deviceID); + HIDDevice device = getDevice(deviceID); + if (device == null) { + HIDDeviceDisconnected(deviceID); + return false; + } + + // Look to see if this is a USB device and we have permission to access it + UsbDevice usbDevice = device.getDevice(); + if (usbDevice != null && !mUsbManager.hasPermission(usbDevice)) { + HIDDeviceOpenPending(deviceID); + try { + final int FLAG_MUTABLE = 0x02000000; // PendingIntent.FLAG_MUTABLE, but don't require SDK 31 + int flags; + if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) { + flags = FLAG_MUTABLE; + } else { + flags = 0; + } + mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, new Intent(HIDDeviceManager.ACTION_USB_PERMISSION), flags)); + } catch (Exception e) { + Log.v(TAG, "Couldn't request permission for USB device " + usbDevice); + HIDDeviceOpenResult(deviceID, false); + } + return false; + } + + try { + return device.open(); + } catch (Exception e) { + Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); + } + return false; + } + + public int sendOutputReport(int deviceID, byte[] report) { + try { + //Log.v(TAG, "sendOutputReport deviceID=" + deviceID + " length=" + report.length); + HIDDevice device; + device = getDevice(deviceID); + if (device == null) { + HIDDeviceDisconnected(deviceID); + return -1; + } + + return device.sendOutputReport(report); + } catch (Exception e) { + Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); + } + return -1; + } + + public int sendFeatureReport(int deviceID, byte[] report) { + try { + //Log.v(TAG, "sendFeatureReport deviceID=" + deviceID + " length=" + report.length); + HIDDevice device; + device = getDevice(deviceID); + if (device == null) { + HIDDeviceDisconnected(deviceID); + return -1; + } + + return device.sendFeatureReport(report); + } catch (Exception e) { + Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); + } + return -1; + } + + public boolean getFeatureReport(int deviceID, byte[] report) { + try { + //Log.v(TAG, "getFeatureReport deviceID=" + deviceID); + HIDDevice device; + device = getDevice(deviceID); + if (device == null) { + HIDDeviceDisconnected(deviceID); + return false; + } + + return device.getFeatureReport(report); + } catch (Exception e) { + Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); + } + return false; + } + + public void closeDevice(int deviceID) { + try { + Log.v(TAG, "closeDevice deviceID=" + deviceID); + HIDDevice device; + device = getDevice(deviceID); + if (device == null) { + HIDDeviceDisconnected(deviceID); + return; + } + + device.close(); + } catch (Exception e) { + Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); + } + } + + + ////////////////////////////////////////////////////////////////////////////////////////////////////// + /////////////// Native methods + ////////////////////////////////////////////////////////////////////////////////////////////////////// + + private native void HIDDeviceRegisterCallback(); + private native void HIDDeviceReleaseCallback(); + + native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol); + native void HIDDeviceOpenPending(int deviceID); + native void HIDDeviceOpenResult(int deviceID, boolean opened); + native void HIDDeviceDisconnected(int deviceID); + + native void HIDDeviceInputReport(int deviceID, byte[] report); + native void HIDDeviceFeatureReport(int deviceID, byte[] report); +} diff --git a/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java b/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java new file mode 100644 index 000000000..bfe0cf954 --- /dev/null +++ b/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java @@ -0,0 +1,309 @@ +package org.libsdl.app; + +import android.hardware.usb.*; +import android.os.Build; +import android.util.Log; +import java.util.Arrays; + +class HIDDeviceUSB implements HIDDevice { + + private static final String TAG = "hidapi"; + + protected HIDDeviceManager mManager; + protected UsbDevice mDevice; + protected int mInterfaceIndex; + protected int mInterface; + protected int mDeviceId; + protected UsbDeviceConnection mConnection; + protected UsbEndpoint mInputEndpoint; + protected UsbEndpoint mOutputEndpoint; + protected InputThread mInputThread; + protected boolean mRunning; + protected boolean mFrozen; + + public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_index) { + mManager = manager; + mDevice = usbDevice; + mInterfaceIndex = interface_index; + mInterface = mDevice.getInterface(mInterfaceIndex).getId(); + mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier()); + mRunning = false; + } + + public String getIdentifier() { + return String.format("%s/%x/%x/%d", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId(), mInterfaceIndex); + } + + @Override + public int getId() { + return mDeviceId; + } + + @Override + public int getVendorId() { + return mDevice.getVendorId(); + } + + @Override + public int getProductId() { + return mDevice.getProductId(); + } + + @Override + public String getSerialNumber() { + String result = null; + if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) { + try { + result = mDevice.getSerialNumber(); + } + catch (SecurityException exception) { + //Log.w(TAG, "App permissions mean we cannot get serial number for device " + getDeviceName() + " message: " + exception.getMessage()); + } + } + if (result == null) { + result = ""; + } + return result; + } + + @Override + public int getVersion() { + return 0; + } + + @Override + public String getManufacturerName() { + String result = null; + if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) { + result = mDevice.getManufacturerName(); + } + if (result == null) { + result = String.format("%x", getVendorId()); + } + return result; + } + + @Override + public String getProductName() { + String result = null; + if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) { + result = mDevice.getProductName(); + } + if (result == null) { + result = String.format("%x", getProductId()); + } + return result; + } + + @Override + public UsbDevice getDevice() { + return mDevice; + } + + public String getDeviceName() { + return getManufacturerName() + " " + getProductName() + "(0x" + String.format("%x", getVendorId()) + "/0x" + String.format("%x", getProductId()) + ")"; + } + + @Override + public boolean open() { + mConnection = mManager.getUSBManager().openDevice(mDevice); + if (mConnection == null) { + Log.w(TAG, "Unable to open USB device " + getDeviceName()); + return false; + } + + // Force claim our interface + UsbInterface iface = mDevice.getInterface(mInterfaceIndex); + if (!mConnection.claimInterface(iface, true)) { + Log.w(TAG, "Failed to claim interfaces on USB device " + getDeviceName()); + close(); + return false; + } + + // Find the endpoints + for (int j = 0; j < iface.getEndpointCount(); j++) { + UsbEndpoint endpt = iface.getEndpoint(j); + switch (endpt.getDirection()) { + case UsbConstants.USB_DIR_IN: + if (mInputEndpoint == null) { + mInputEndpoint = endpt; + } + break; + case UsbConstants.USB_DIR_OUT: + if (mOutputEndpoint == null) { + mOutputEndpoint = endpt; + } + break; + } + } + + // Make sure the required endpoints were present + if (mInputEndpoint == null || mOutputEndpoint == null) { + Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName()); + close(); + return false; + } + + // Start listening for input + mRunning = true; + mInputThread = new InputThread(); + mInputThread.start(); + + return true; + } + + @Override + public int sendFeatureReport(byte[] report) { + int res = -1; + int offset = 0; + int length = report.length; + boolean skipped_report_id = false; + byte report_number = report[0]; + + if (report_number == 0x0) { + ++offset; + --length; + skipped_report_id = true; + } + + res = mConnection.controlTransfer( + UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT, + 0x09/*HID set_report*/, + (3/*HID feature*/ << 8) | report_number, + mInterface, + report, offset, length, + 1000/*timeout millis*/); + + if (res < 0) { + Log.w(TAG, "sendFeatureReport() returned " + res + " on device " + getDeviceName()); + return -1; + } + + if (skipped_report_id) { + ++length; + } + return length; + } + + @Override + public int sendOutputReport(byte[] report) { + int r = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000); + if (r != report.length) { + Log.w(TAG, "sendOutputReport() returned " + r + " on device " + getDeviceName()); + } + return r; + } + + @Override + public boolean getFeatureReport(byte[] report) { + int res = -1; + int offset = 0; + int length = report.length; + boolean skipped_report_id = false; + byte report_number = report[0]; + + if (report_number == 0x0) { + /* Offset the return buffer by 1, so that the report ID + will remain in byte 0. */ + ++offset; + --length; + skipped_report_id = true; + } + + res = mConnection.controlTransfer( + UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN, + 0x01/*HID get_report*/, + (3/*HID feature*/ << 8) | report_number, + mInterface, + report, offset, length, + 1000/*timeout millis*/); + + if (res < 0) { + Log.w(TAG, "getFeatureReport() returned " + res + " on device " + getDeviceName()); + return false; + } + + if (skipped_report_id) { + ++res; + ++length; + } + + byte[] data; + if (res == length) { + data = report; + } else { + data = Arrays.copyOfRange(report, 0, res); + } + mManager.HIDDeviceFeatureReport(mDeviceId, data); + + return true; + } + + @Override + public void close() { + mRunning = false; + if (mInputThread != null) { + while (mInputThread.isAlive()) { + mInputThread.interrupt(); + try { + mInputThread.join(); + } catch (InterruptedException e) { + // Keep trying until we're done + } + } + mInputThread = null; + } + if (mConnection != null) { + UsbInterface iface = mDevice.getInterface(mInterfaceIndex); + mConnection.releaseInterface(iface); + mConnection.close(); + mConnection = null; + } + } + + @Override + public void shutdown() { + close(); + mManager = null; + } + + @Override + public void setFrozen(boolean frozen) { + mFrozen = frozen; + } + + protected class InputThread extends Thread { + @Override + public void run() { + int packetSize = mInputEndpoint.getMaxPacketSize(); + byte[] packet = new byte[packetSize]; + while (mRunning) { + int r; + try + { + r = mConnection.bulkTransfer(mInputEndpoint, packet, packetSize, 1000); + } + catch (Exception e) + { + Log.v(TAG, "Exception in UsbDeviceConnection bulktransfer: " + e); + break; + } + if (r < 0) { + // Could be a timeout or an I/O error + } + if (r > 0) { + byte[] data; + if (r == packetSize) { + data = packet; + } else { + data = Arrays.copyOfRange(packet, 0, r); + } + + if (!mFrozen) { + mManager.HIDDeviceInputReport(mDeviceId, data); + } + } + } + } + } +} diff --git a/android/app/src/main/java/org/libsdl/app/SDL.java b/android/app/src/main/java/org/libsdl/app/SDL.java new file mode 100644 index 000000000..44c21c1c7 --- /dev/null +++ b/android/app/src/main/java/org/libsdl/app/SDL.java @@ -0,0 +1,86 @@ +package org.libsdl.app; + +import android.content.Context; + +import java.lang.Class; +import java.lang.reflect.Method; + +/** + SDL library initialization +*/ +public class SDL { + + // This function should be called first and sets up the native code + // so it can call into the Java classes + public static void setupJNI() { + SDLActivity.nativeSetupJNI(); + SDLAudioManager.nativeSetupJNI(); + SDLControllerManager.nativeSetupJNI(); + } + + // This function should be called each time the activity is started + public static void initialize() { + setContext(null); + + SDLActivity.initialize(); + SDLAudioManager.initialize(); + SDLControllerManager.initialize(); + } + + // This function stores the current activity (SDL or not) + public static void setContext(Context context) { + SDLAudioManager.setContext(context); + mContext = context; + } + + public static Context getContext() { + return mContext; + } + + public static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException { + + if (libraryName == null) { + throw new NullPointerException("No library name provided."); + } + + try { + // Let's see if we have ReLinker available in the project. This is necessary for + // some projects that have huge numbers of local libraries bundled, and thus may + // trip a bug in Android's native library loader which ReLinker works around. (If + // loadLibrary works properly, ReLinker will simply use the normal Android method + // internally.) + // + // To use ReLinker, just add it as a dependency. For more information, see + // https://github.com/KeepSafe/ReLinker for ReLinker's repository. + // + Class relinkClass = mContext.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker"); + Class relinkListenerClass = mContext.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener"); + Class contextClass = mContext.getClassLoader().loadClass("android.content.Context"); + Class stringClass = mContext.getClassLoader().loadClass("java.lang.String"); + + // Get a 'force' instance of the ReLinker, so we can ensure libraries are reinstalled if + // they've changed during updates. + Method forceMethod = relinkClass.getDeclaredMethod("force"); + Object relinkInstance = forceMethod.invoke(null); + Class relinkInstanceClass = relinkInstance.getClass(); + + // Actually load the library! + Method loadMethod = relinkInstanceClass.getDeclaredMethod("loadLibrary", contextClass, stringClass, stringClass, relinkListenerClass); + loadMethod.invoke(relinkInstance, mContext, libraryName, null, null); + } + catch (final Throwable e) { + // Fall back + try { + System.loadLibrary(libraryName); + } + catch (final UnsatisfiedLinkError ule) { + throw ule; + } + catch (final SecurityException se) { + throw se; + } + } + } + + protected static Context mContext; +} diff --git a/android/app/src/main/java/org/libsdl/app/SDLActivity.java b/android/app/src/main/java/org/libsdl/app/SDLActivity.java new file mode 100644 index 000000000..fd5a056e3 --- /dev/null +++ b/android/app/src/main/java/org/libsdl/app/SDLActivity.java @@ -0,0 +1,2117 @@ +package org.libsdl.app; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.UiModeManager; +import android.content.ClipboardManager; +import android.content.ClipData; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.hardware.Sensor; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.text.Editable; +import android.text.InputType; +import android.text.Selection; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.SparseArray; +import android.view.Display; +import android.view.Gravity; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.PointerIcon; +import android.view.Surface; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.view.inputmethod.BaseInputConnection; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +import java.util.Hashtable; +import java.util.Locale; + + +/** + SDL Activity +*/ +public class SDLActivity extends Activity implements View.OnSystemUiVisibilityChangeListener { + private static final String TAG = "SDL"; + private static final int SDL_MAJOR_VERSION = 2; + private static final int SDL_MINOR_VERSION = 30; + private static final int SDL_MICRO_VERSION = 1; +/* + // Display InputType.SOURCE/CLASS of events and devices + // + // SDLActivity.debugSource(device.getSources(), "device[" + device.getName() + "]"); + // SDLActivity.debugSource(event.getSource(), "event"); + public static void debugSource(int sources, String prefix) { + int s = sources; + int s_copy = sources; + String cls = ""; + String src = ""; + int tst = 0; + int FLAG_TAINTED = 0x80000000; + + if ((s & InputDevice.SOURCE_CLASS_BUTTON) != 0) cls += " BUTTON"; + if ((s & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) cls += " JOYSTICK"; + if ((s & InputDevice.SOURCE_CLASS_POINTER) != 0) cls += " POINTER"; + if ((s & InputDevice.SOURCE_CLASS_POSITION) != 0) cls += " POSITION"; + if ((s & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) cls += " TRACKBALL"; + + + int s2 = s_copy & ~InputDevice.SOURCE_ANY; // keep class bits + s2 &= ~( InputDevice.SOURCE_CLASS_BUTTON + | InputDevice.SOURCE_CLASS_JOYSTICK + | InputDevice.SOURCE_CLASS_POINTER + | InputDevice.SOURCE_CLASS_POSITION + | InputDevice.SOURCE_CLASS_TRACKBALL); + + if (s2 != 0) cls += "Some_Unkown"; + + s2 = s_copy & InputDevice.SOURCE_ANY; // keep source only, no class; + + if (Build.VERSION.SDK_INT >= 23) { + tst = InputDevice.SOURCE_BLUETOOTH_STYLUS; + if ((s & tst) == tst) src += " BLUETOOTH_STYLUS"; + s2 &= ~tst; + } + + tst = InputDevice.SOURCE_DPAD; + if ((s & tst) == tst) src += " DPAD"; + s2 &= ~tst; + + tst = InputDevice.SOURCE_GAMEPAD; + if ((s & tst) == tst) src += " GAMEPAD"; + s2 &= ~tst; + + if (Build.VERSION.SDK_INT >= 21) { + tst = InputDevice.SOURCE_HDMI; + if ((s & tst) == tst) src += " HDMI"; + s2 &= ~tst; + } + + tst = InputDevice.SOURCE_JOYSTICK; + if ((s & tst) == tst) src += " JOYSTICK"; + s2 &= ~tst; + + tst = InputDevice.SOURCE_KEYBOARD; + if ((s & tst) == tst) src += " KEYBOARD"; + s2 &= ~tst; + + tst = InputDevice.SOURCE_MOUSE; + if ((s & tst) == tst) src += " MOUSE"; + s2 &= ~tst; + + if (Build.VERSION.SDK_INT >= 26) { + tst = InputDevice.SOURCE_MOUSE_RELATIVE; + if ((s & tst) == tst) src += " MOUSE_RELATIVE"; + s2 &= ~tst; + + tst = InputDevice.SOURCE_ROTARY_ENCODER; + if ((s & tst) == tst) src += " ROTARY_ENCODER"; + s2 &= ~tst; + } + tst = InputDevice.SOURCE_STYLUS; + if ((s & tst) == tst) src += " STYLUS"; + s2 &= ~tst; + + tst = InputDevice.SOURCE_TOUCHPAD; + if ((s & tst) == tst) src += " TOUCHPAD"; + s2 &= ~tst; + + tst = InputDevice.SOURCE_TOUCHSCREEN; + if ((s & tst) == tst) src += " TOUCHSCREEN"; + s2 &= ~tst; + + if (Build.VERSION.SDK_INT >= 18) { + tst = InputDevice.SOURCE_TOUCH_NAVIGATION; + if ((s & tst) == tst) src += " TOUCH_NAVIGATION"; + s2 &= ~tst; + } + + tst = InputDevice.SOURCE_TRACKBALL; + if ((s & tst) == tst) src += " TRACKBALL"; + s2 &= ~tst; + + tst = InputDevice.SOURCE_ANY; + if ((s & tst) == tst) src += " ANY"; + s2 &= ~tst; + + if (s == FLAG_TAINTED) src += " FLAG_TAINTED"; + s2 &= ~FLAG_TAINTED; + + if (s2 != 0) src += " Some_Unkown"; + + Log.v(TAG, prefix + "int=" + s_copy + " CLASS={" + cls + " } source(s):" + src); + } +*/ + + public static boolean mIsResumedCalled, mHasFocus; + public static final boolean mHasMultiWindow = (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */); + + // Cursor types + // private static final int SDL_SYSTEM_CURSOR_NONE = -1; + private static final int SDL_SYSTEM_CURSOR_ARROW = 0; + private static final int SDL_SYSTEM_CURSOR_IBEAM = 1; + private static final int SDL_SYSTEM_CURSOR_WAIT = 2; + private static final int SDL_SYSTEM_CURSOR_CROSSHAIR = 3; + private static final int SDL_SYSTEM_CURSOR_WAITARROW = 4; + private static final int SDL_SYSTEM_CURSOR_SIZENWSE = 5; + private static final int SDL_SYSTEM_CURSOR_SIZENESW = 6; + private static final int SDL_SYSTEM_CURSOR_SIZEWE = 7; + private static final int SDL_SYSTEM_CURSOR_SIZENS = 8; + private static final int SDL_SYSTEM_CURSOR_SIZEALL = 9; + private static final int SDL_SYSTEM_CURSOR_NO = 10; + private static final int SDL_SYSTEM_CURSOR_HAND = 11; + + protected static final int SDL_ORIENTATION_UNKNOWN = 0; + protected static final int SDL_ORIENTATION_LANDSCAPE = 1; + protected static final int SDL_ORIENTATION_LANDSCAPE_FLIPPED = 2; + protected static final int SDL_ORIENTATION_PORTRAIT = 3; + protected static final int SDL_ORIENTATION_PORTRAIT_FLIPPED = 4; + + protected static int mCurrentOrientation; + protected static Locale mCurrentLocale; + + // Handle the state of the native layer + public enum NativeState { + INIT, RESUMED, PAUSED + } + + public static NativeState mNextNativeState; + public static NativeState mCurrentNativeState; + + /** If shared libraries (e.g. SDL or the native application) could not be loaded. */ + public static boolean mBrokenLibraries = true; + + // Main components + protected static SDLActivity mSingleton; + protected static SDLSurface mSurface; + protected static DummyEdit mTextEdit; + protected static boolean mScreenKeyboardShown; + protected static ViewGroup mLayout; + protected static SDLClipboardHandler mClipboardHandler; + protected static Hashtable mCursors; + protected static int mLastCursorID; + protected static SDLGenericMotionListener_API12 mMotionListener; + protected static HIDDeviceManager mHIDDeviceManager; + + // This is what SDL runs in. It invokes SDL_main(), eventually + protected static Thread mSDLThread; + + protected static SDLGenericMotionListener_API12 getMotionListener() { + if (mMotionListener == null) { + if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) { + mMotionListener = new SDLGenericMotionListener_API26(); + } else if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { + mMotionListener = new SDLGenericMotionListener_API24(); + } else { + mMotionListener = new SDLGenericMotionListener_API12(); + } + } + + return mMotionListener; + } + + /** + * This method returns the name of the shared object with the application entry point + * It can be overridden by derived classes. + */ + protected String getMainSharedObject() { + String library; + String[] libraries = SDLActivity.mSingleton.getLibraries(); + if (libraries.length > 0) { + library = "lib" + libraries[libraries.length - 1] + ".so"; + } else { + library = "libmain.so"; + } + return getContext().getApplicationInfo().nativeLibraryDir + "/" + library; + } + + /** + * This method returns the name of the application entry point + * It can be overridden by derived classes. + */ + protected String getMainFunction() { + return "SDL_main"; + } + + /** + * This method is called by SDL before loading the native shared libraries. + * It can be overridden to provide names of shared libraries to be loaded. + * The default implementation returns the defaults. It never returns null. + * An array returned by a new implementation must at least contain "SDL2". + * Also keep in mind that the order the libraries are loaded may matter. + * @return names of shared libraries to be loaded (e.g. "SDL2", "main"). + */ + protected String[] getLibraries() { + return new String[] { + "SDL2", + // "SDL2_image", + // "SDL2_mixer", + // "SDL2_net", + // "SDL2_ttf", + "main" + }; + } + + // Load the .so + public void loadLibraries() { + for (String lib : getLibraries()) { + SDL.loadLibrary(lib); + } + } + + /** + * This method is called by SDL before starting the native application thread. + * It can be overridden to provide the arguments after the application name. + * The default implementation returns an empty array. It never returns null. + * @return arguments for the native application. + */ + protected String[] getArguments() { + return new String[0]; + } + + public static void initialize() { + // The static nature of the singleton and Android quirkyness force us to initialize everything here + // Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values + mSingleton = null; + mSurface = null; + mTextEdit = null; + mLayout = null; + mClipboardHandler = null; + mCursors = new Hashtable(); + mLastCursorID = 0; + mSDLThread = null; + mIsResumedCalled = false; + mHasFocus = true; + mNextNativeState = NativeState.INIT; + mCurrentNativeState = NativeState.INIT; + } + + protected SDLSurface createSDLSurface(Context context) { + return new SDLSurface(context); + } + + // Setup + @Override + protected void onCreate(Bundle savedInstanceState) { + Log.v(TAG, "Device: " + Build.DEVICE); + Log.v(TAG, "Model: " + Build.MODEL); + Log.v(TAG, "onCreate()"); + super.onCreate(savedInstanceState); + + try { + Thread.currentThread().setName("SDLActivity"); + } catch (Exception e) { + Log.v(TAG, "modify thread properties failed " + e.toString()); + } + + // Load shared libraries + String errorMsgBrokenLib = ""; + try { + loadLibraries(); + mBrokenLibraries = false; /* success */ + } catch(UnsatisfiedLinkError e) { + System.err.println(e.getMessage()); + mBrokenLibraries = true; + errorMsgBrokenLib = e.getMessage(); + } catch(Exception e) { + System.err.println(e.getMessage()); + mBrokenLibraries = true; + errorMsgBrokenLib = e.getMessage(); + } + + if (!mBrokenLibraries) { + String expected_version = String.valueOf(SDL_MAJOR_VERSION) + "." + + String.valueOf(SDL_MINOR_VERSION) + "." + + String.valueOf(SDL_MICRO_VERSION); + String version = nativeGetVersion(); + if (!version.equals(expected_version)) { + mBrokenLibraries = true; + errorMsgBrokenLib = "SDL C/Java version mismatch (expected " + expected_version + ", got " + version + ")"; + } + } + + if (mBrokenLibraries) { + mSingleton = this; + AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this); + dlgAlert.setMessage("An error occurred while trying to start the application. Please try again and/or reinstall." + + System.getProperty("line.separator") + + System.getProperty("line.separator") + + "Error: " + errorMsgBrokenLib); + dlgAlert.setTitle("SDL Error"); + dlgAlert.setPositiveButton("Exit", + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog,int id) { + // if this button is clicked, close current activity + SDLActivity.mSingleton.finish(); + } + }); + dlgAlert.setCancelable(false); + dlgAlert.create().show(); + + return; + } + + // Set up JNI + SDL.setupJNI(); + + // Initialize state + SDL.initialize(); + + // So we can call stuff from static callbacks + mSingleton = this; + SDL.setContext(this); + + mClipboardHandler = new SDLClipboardHandler(); + + mHIDDeviceManager = HIDDeviceManager.acquire(this); + + // Set up the surface + mSurface = createSDLSurface(this); + + mLayout = new RelativeLayout(this); + mLayout.addView(mSurface); + + // Get our current screen orientation and pass it down. + mCurrentOrientation = SDLActivity.getCurrentOrientation(); + // Only record current orientation + SDLActivity.onNativeOrientationChanged(mCurrentOrientation); + + try { + if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) { + mCurrentLocale = getContext().getResources().getConfiguration().locale; + } else { + mCurrentLocale = getContext().getResources().getConfiguration().getLocales().get(0); + } + } catch(Exception ignored) { + } + + setContentView(mLayout); + + setWindowStyle(false); + + getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this); + + // Get filename from "Open with" of another application + Intent intent = getIntent(); + if (intent != null && intent.getData() != null) { + String filename = intent.getData().getPath(); + if (filename != null) { + Log.v(TAG, "Got filename: " + filename); + SDLActivity.onNativeDropFile(filename); + } + } + } + + protected void pauseNativeThread() { + mNextNativeState = NativeState.PAUSED; + mIsResumedCalled = false; + + if (SDLActivity.mBrokenLibraries) { + return; + } + + SDLActivity.handleNativeState(); + } + + protected void resumeNativeThread() { + mNextNativeState = NativeState.RESUMED; + mIsResumedCalled = true; + + if (SDLActivity.mBrokenLibraries) { + return; + } + + SDLActivity.handleNativeState(); + } + + // Events + @Override + protected void onPause() { + Log.v(TAG, "onPause()"); + super.onPause(); + + if (mHIDDeviceManager != null) { + mHIDDeviceManager.setFrozen(true); + } + if (!mHasMultiWindow) { + pauseNativeThread(); + } + } + + @Override + protected void onResume() { + Log.v(TAG, "onResume()"); + super.onResume(); + + if (mHIDDeviceManager != null) { + mHIDDeviceManager.setFrozen(false); + } + if (!mHasMultiWindow) { + resumeNativeThread(); + } + } + + @Override + protected void onStop() { + Log.v(TAG, "onStop()"); + super.onStop(); + if (mHasMultiWindow) { + pauseNativeThread(); + } + } + + @Override + protected void onStart() { + Log.v(TAG, "onStart()"); + super.onStart(); + if (mHasMultiWindow) { + resumeNativeThread(); + } + } + + public static int getCurrentOrientation() { + int result = SDL_ORIENTATION_UNKNOWN; + + Activity activity = (Activity)getContext(); + if (activity == null) { + return result; + } + Display display = activity.getWindowManager().getDefaultDisplay(); + + switch (display.getRotation()) { + case Surface.ROTATION_0: + result = SDL_ORIENTATION_PORTRAIT; + break; + + case Surface.ROTATION_90: + result = SDL_ORIENTATION_LANDSCAPE; + break; + + case Surface.ROTATION_180: + result = SDL_ORIENTATION_PORTRAIT_FLIPPED; + break; + + case Surface.ROTATION_270: + result = SDL_ORIENTATION_LANDSCAPE_FLIPPED; + break; + } + + return result; + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + Log.v(TAG, "onWindowFocusChanged(): " + hasFocus); + + if (SDLActivity.mBrokenLibraries) { + return; + } + + mHasFocus = hasFocus; + if (hasFocus) { + mNextNativeState = NativeState.RESUMED; + SDLActivity.getMotionListener().reclaimRelativeMouseModeIfNeeded(); + + SDLActivity.handleNativeState(); + nativeFocusChanged(true); + + } else { + nativeFocusChanged(false); + if (!mHasMultiWindow) { + mNextNativeState = NativeState.PAUSED; + SDLActivity.handleNativeState(); + } + } + } + + @Override + public void onLowMemory() { + Log.v(TAG, "onLowMemory()"); + super.onLowMemory(); + + if (SDLActivity.mBrokenLibraries) { + return; + } + + SDLActivity.nativeLowMemory(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + Log.v(TAG, "onConfigurationChanged()"); + super.onConfigurationChanged(newConfig); + + if (SDLActivity.mBrokenLibraries) { + return; + } + + if (mCurrentLocale == null || !mCurrentLocale.equals(newConfig.locale)) { + mCurrentLocale = newConfig.locale; + SDLActivity.onNativeLocaleChanged(); + } + } + + @Override + protected void onDestroy() { + Log.v(TAG, "onDestroy()"); + + if (mHIDDeviceManager != null) { + HIDDeviceManager.release(mHIDDeviceManager); + mHIDDeviceManager = null; + } + + SDLAudioManager.release(this); + + if (SDLActivity.mBrokenLibraries) { + super.onDestroy(); + return; + } + + if (SDLActivity.mSDLThread != null) { + + // Send Quit event to "SDLThread" thread + SDLActivity.nativeSendQuit(); + + // Wait for "SDLThread" thread to end + try { + SDLActivity.mSDLThread.join(); + } catch(Exception e) { + Log.v(TAG, "Problem stopping SDLThread: " + e); + } + } + + SDLActivity.nativeQuit(); + + super.onDestroy(); + } + + @Override + public void onBackPressed() { + // Check if we want to block the back button in case of mouse right click. + // + // If we do, the normal hardware back button will no longer work and people have to use home, + // but the mouse right click will work. + // + boolean trapBack = SDLActivity.nativeGetHintBoolean("SDL_ANDROID_TRAP_BACK_BUTTON", false); + if (trapBack) { + // Exit and let the mouse handler handle this button (if appropriate) + return; + } + + // Default system back button behavior. + if (!isFinishing()) { + super.onBackPressed(); + } + } + + // Called by JNI from SDL. + public static void manualBackButton() { + mSingleton.pressBackButton(); + } + + // Used to get us onto the activity's main thread + public void pressBackButton() { + runOnUiThread(new Runnable() { + @Override + public void run() { + if (!SDLActivity.this.isFinishing()) { + SDLActivity.this.superOnBackPressed(); + } + } + }); + } + + // Used to access the system back behavior. + public void superOnBackPressed() { + super.onBackPressed(); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + + if (SDLActivity.mBrokenLibraries) { + return false; + } + + int keyCode = event.getKeyCode(); + // Ignore certain special keys so they're handled by Android + if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || + keyCode == KeyEvent.KEYCODE_VOLUME_UP || + keyCode == KeyEvent.KEYCODE_CAMERA || + keyCode == KeyEvent.KEYCODE_ZOOM_IN || /* API 11 */ + keyCode == KeyEvent.KEYCODE_ZOOM_OUT /* API 11 */ + ) { + return false; + } + return super.dispatchKeyEvent(event); + } + + /* Transition to next state */ + public static void handleNativeState() { + + if (mNextNativeState == mCurrentNativeState) { + // Already in same state, discard. + return; + } + + // Try a transition to init state + if (mNextNativeState == NativeState.INIT) { + + mCurrentNativeState = mNextNativeState; + return; + } + + // Try a transition to paused state + if (mNextNativeState == NativeState.PAUSED) { + if (mSDLThread != null) { + nativePause(); + } + if (mSurface != null) { + mSurface.handlePause(); + } + mCurrentNativeState = mNextNativeState; + return; + } + + // Try a transition to resumed state + if (mNextNativeState == NativeState.RESUMED) { + if (mSurface.mIsSurfaceReady && mHasFocus && mIsResumedCalled) { + if (mSDLThread == null) { + // This is the entry point to the C app. + // Start up the C app thread and enable sensor input for the first time + // FIXME: Why aren't we enabling sensor input at start? + + mSDLThread = new Thread(new SDLMain(), "SDLThread"); + mSurface.enableSensor(Sensor.TYPE_ACCELEROMETER, true); + mSDLThread.start(); + + // No nativeResume(), don't signal Android_ResumeSem + } else { + nativeResume(); + } + mSurface.handleResume(); + + mCurrentNativeState = mNextNativeState; + } + } + } + + // Messages from the SDLMain thread + static final int COMMAND_CHANGE_TITLE = 1; + static final int COMMAND_CHANGE_WINDOW_STYLE = 2; + static final int COMMAND_TEXTEDIT_HIDE = 3; + static final int COMMAND_SET_KEEP_SCREEN_ON = 5; + + protected static final int COMMAND_USER = 0x8000; + + protected static boolean mFullscreenModeActive; + + /** + * This method is called by SDL if SDL did not handle a message itself. + * This happens if a received message contains an unsupported command. + * Method can be overwritten to handle Messages in a different class. + * @param command the command of the message. + * @param param the parameter of the message. May be null. + * @return if the message was handled in overridden method. + */ + protected boolean onUnhandledMessage(int command, Object param) { + return false; + } + + /** + * A Handler class for Messages from native SDL applications. + * It uses current Activities as target (e.g. for the title). + * static to prevent implicit references to enclosing object. + */ + protected static class SDLCommandHandler extends Handler { + @Override + public void handleMessage(Message msg) { + Context context = SDL.getContext(); + if (context == null) { + Log.e(TAG, "error handling message, getContext() returned null"); + return; + } + switch (msg.arg1) { + case COMMAND_CHANGE_TITLE: + if (context instanceof Activity) { + ((Activity) context).setTitle((String)msg.obj); + } else { + Log.e(TAG, "error handling message, getContext() returned no Activity"); + } + break; + case COMMAND_CHANGE_WINDOW_STYLE: + if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) { + if (context instanceof Activity) { + Window window = ((Activity) context).getWindow(); + if (window != null) { + if ((msg.obj instanceof Integer) && ((Integer) msg.obj != 0)) { + int flags = View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.INVISIBLE; + window.getDecorView().setSystemUiVisibility(flags); + window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); + SDLActivity.mFullscreenModeActive = true; + } else { + int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_VISIBLE; + window.getDecorView().setSystemUiVisibility(flags); + window.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); + window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + SDLActivity.mFullscreenModeActive = false; + } + } + } else { + Log.e(TAG, "error handling message, getContext() returned no Activity"); + } + } + break; + case COMMAND_TEXTEDIT_HIDE: + if (mTextEdit != null) { + // Note: On some devices setting view to GONE creates a flicker in landscape. + // Setting the View's sizes to 0 is similar to GONE but without the flicker. + // The sizes will be set to useful values when the keyboard is shown again. + mTextEdit.setLayoutParams(new RelativeLayout.LayoutParams(0, 0)); + + InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(mTextEdit.getWindowToken(), 0); + + mScreenKeyboardShown = false; + + mSurface.requestFocus(); + } + break; + case COMMAND_SET_KEEP_SCREEN_ON: + { + if (context instanceof Activity) { + Window window = ((Activity) context).getWindow(); + if (window != null) { + if ((msg.obj instanceof Integer) && ((Integer) msg.obj != 0)) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + } + break; + } + default: + if ((context instanceof SDLActivity) && !((SDLActivity) context).onUnhandledMessage(msg.arg1, msg.obj)) { + Log.e(TAG, "error handling message, command is " + msg.arg1); + } + } + } + } + + // Handler for the messages + Handler commandHandler = new SDLCommandHandler(); + + // Send a message from the SDLMain thread + boolean sendCommand(int command, Object data) { + Message msg = commandHandler.obtainMessage(); + msg.arg1 = command; + msg.obj = data; + boolean result = commandHandler.sendMessage(msg); + + if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) { + if (command == COMMAND_CHANGE_WINDOW_STYLE) { + // Ensure we don't return until the resize has actually happened, + // or 500ms have passed. + + boolean bShouldWait = false; + + if (data instanceof Integer) { + // Let's figure out if we're already laid out fullscreen or not. + Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + DisplayMetrics realMetrics = new DisplayMetrics(); + display.getRealMetrics(realMetrics); + + boolean bFullscreenLayout = ((realMetrics.widthPixels == mSurface.getWidth()) && + (realMetrics.heightPixels == mSurface.getHeight())); + + if ((Integer) data == 1) { + // If we aren't laid out fullscreen or actively in fullscreen mode already, we're going + // to change size and should wait for surfaceChanged() before we return, so the size + // is right back in native code. If we're already laid out fullscreen, though, we're + // not going to change size even if we change decor modes, so we shouldn't wait for + // surfaceChanged() -- which may not even happen -- and should return immediately. + bShouldWait = !bFullscreenLayout; + } else { + // If we're laid out fullscreen (even if the status bar and nav bar are present), + // or are actively in fullscreen, we're going to change size and should wait for + // surfaceChanged before we return, so the size is right back in native code. + bShouldWait = bFullscreenLayout; + } + } + + if (bShouldWait && (SDLActivity.getContext() != null)) { + // We'll wait for the surfaceChanged() method, which will notify us + // when called. That way, we know our current size is really the + // size we need, instead of grabbing a size that's still got + // the navigation and/or status bars before they're hidden. + // + // We'll wait for up to half a second, because some devices + // take a surprisingly long time for the surface resize, but + // then we'll just give up and return. + // + synchronized (SDLActivity.getContext()) { + try { + SDLActivity.getContext().wait(500); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } + } + } + + return result; + } + + // C functions we call + public static native String nativeGetVersion(); + public static native int nativeSetupJNI(); + public static native int nativeRunMain(String library, String function, Object arguments); + public static native void nativeLowMemory(); + public static native void nativeSendQuit(); + public static native void nativeQuit(); + public static native void nativePause(); + public static native void nativeResume(); + public static native void nativeFocusChanged(boolean hasFocus); + public static native void onNativeDropFile(String filename); + public static native void nativeSetScreenResolution(int surfaceWidth, int surfaceHeight, int deviceWidth, int deviceHeight, float rate); + public static native void onNativeResize(); + public static native void onNativeKeyDown(int keycode); + public static native void onNativeKeyUp(int keycode); + public static native boolean onNativeSoftReturnKey(); + public static native void onNativeKeyboardFocusLost(); + public static native void onNativeMouse(int button, int action, float x, float y, boolean relative); + public static native void onNativeTouch(int touchDevId, int pointerFingerId, + int action, float x, + float y, float p); + public static native void onNativeAccel(float x, float y, float z); + public static native void onNativeClipboardChanged(); + public static native void onNativeSurfaceCreated(); + public static native void onNativeSurfaceChanged(); + public static native void onNativeSurfaceDestroyed(); + public static native String nativeGetHint(String name); + public static native boolean nativeGetHintBoolean(String name, boolean default_value); + public static native void nativeSetenv(String name, String value); + public static native void onNativeOrientationChanged(int orientation); + public static native void nativeAddTouch(int touchId, String name); + public static native void nativePermissionResult(int requestCode, boolean result); + public static native void onNativeLocaleChanged(); + + /** + * This method is called by SDL using JNI. + */ + public static boolean setActivityTitle(String title) { + // Called from SDLMain() thread and can't directly affect the view + return mSingleton.sendCommand(COMMAND_CHANGE_TITLE, title); + } + + /** + * This method is called by SDL using JNI. + */ + public static void setWindowStyle(boolean fullscreen) { + // Called from SDLMain() thread and can't directly affect the view + mSingleton.sendCommand(COMMAND_CHANGE_WINDOW_STYLE, fullscreen ? 1 : 0); + } + + /** + * This method is called by SDL using JNI. + * This is a static method for JNI convenience, it calls a non-static method + * so that is can be overridden + */ + public static void setOrientation(int w, int h, boolean resizable, String hint) + { + if (mSingleton != null) { + mSingleton.setOrientationBis(w, h, resizable, hint); + } + } + + /** + * This can be overridden + */ + public void setOrientationBis(int w, int h, boolean resizable, String hint) + { + int orientation_landscape = -1; + int orientation_portrait = -1; + + /* If set, hint "explicitly controls which UI orientations are allowed". */ + if (hint.contains("LandscapeRight") && hint.contains("LandscapeLeft")) { + orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; + } else if (hint.contains("LandscapeLeft")) { + orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; + } else if (hint.contains("LandscapeRight")) { + orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; + } + + /* exact match to 'Portrait' to distinguish with PortraitUpsideDown */ + boolean contains_Portrait = hint.contains("Portrait ") || hint.endsWith("Portrait"); + + if (contains_Portrait && hint.contains("PortraitUpsideDown")) { + orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; + } else if (contains_Portrait) { + orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; + } else if (hint.contains("PortraitUpsideDown")) { + orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; + } + + boolean is_landscape_allowed = (orientation_landscape != -1); + boolean is_portrait_allowed = (orientation_portrait != -1); + int req; /* Requested orientation */ + + /* No valid hint, nothing is explicitly allowed */ + if (!is_portrait_allowed && !is_landscape_allowed) { + if (resizable) { + /* All orientations are allowed */ + req = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR; + } else { + /* Fixed window and nothing specified. Get orientation from w/h of created window */ + req = (w > h ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE : ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); + } + } else { + /* At least one orientation is allowed */ + if (resizable) { + if (is_portrait_allowed && is_landscape_allowed) { + /* hint allows both landscape and portrait, promote to full sensor */ + req = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR; + } else { + /* Use the only one allowed "orientation" */ + req = (is_landscape_allowed ? orientation_landscape : orientation_portrait); + } + } else { + /* Fixed window and both orientations are allowed. Choose one. */ + if (is_portrait_allowed && is_landscape_allowed) { + req = (w > h ? orientation_landscape : orientation_portrait); + } else { + /* Use the only one allowed "orientation" */ + req = (is_landscape_allowed ? orientation_landscape : orientation_portrait); + } + } + } + + Log.v(TAG, "setOrientation() requestedOrientation=" + req + " width=" + w +" height="+ h +" resizable=" + resizable + " hint=" + hint); + mSingleton.setRequestedOrientation(req); + } + + /** + * This method is called by SDL using JNI. + */ + public static void minimizeWindow() { + + if (mSingleton == null) { + return; + } + + Intent startMain = new Intent(Intent.ACTION_MAIN); + startMain.addCategory(Intent.CATEGORY_HOME); + startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mSingleton.startActivity(startMain); + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean shouldMinimizeOnFocusLoss() { +/* + if (Build.VERSION.SDK_INT >= 24) { + if (mSingleton == null) { + return true; + } + + if (mSingleton.isInMultiWindowMode()) { + return false; + } + + if (mSingleton.isInPictureInPictureMode()) { + return false; + } + } + + return true; +*/ + return false; + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean isScreenKeyboardShown() + { + if (mTextEdit == null) { + return false; + } + + if (!mScreenKeyboardShown) { + return false; + } + + InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + return imm.isAcceptingText(); + + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean supportsRelativeMouse() + { + // DeX mode in Samsung Experience 9.0 and earlier doesn't support relative mice properly under + // Android 7 APIs, and simply returns no data under Android 8 APIs. + // + // This is fixed in Samsung Experience 9.5, which corresponds to Android 8.1.0, and + // thus SDK version 27. If we are in DeX mode and not API 27 or higher, as a result, + // we should stick to relative mode. + // + if (Build.VERSION.SDK_INT < 27 /* Android 8.1 (O_MR1) */ && isDeXMode()) { + return false; + } + + return SDLActivity.getMotionListener().supportsRelativeMouse(); + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean setRelativeMouseEnabled(boolean enabled) + { + if (enabled && !supportsRelativeMouse()) { + return false; + } + + return SDLActivity.getMotionListener().setRelativeMouseEnabled(enabled); + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean sendMessage(int command, int param) { + if (mSingleton == null) { + return false; + } + return mSingleton.sendCommand(command, param); + } + + /** + * This method is called by SDL using JNI. + */ + public static Context getContext() { + return SDL.getContext(); + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean isAndroidTV() { + UiModeManager uiModeManager = (UiModeManager) getContext().getSystemService(UI_MODE_SERVICE); + if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) { + return true; + } + if (Build.MANUFACTURER.equals("MINIX") && Build.MODEL.equals("NEO-U1")) { + return true; + } + if (Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.equals("X96-W")) { + return true; + } + return Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.startsWith("TV"); + } + + public static double getDiagonal() + { + DisplayMetrics metrics = new DisplayMetrics(); + Activity activity = (Activity)getContext(); + if (activity == null) { + return 0.0; + } + activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); + + double dWidthInches = metrics.widthPixels / (double)metrics.xdpi; + double dHeightInches = metrics.heightPixels / (double)metrics.ydpi; + + return Math.sqrt((dWidthInches * dWidthInches) + (dHeightInches * dHeightInches)); + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean isTablet() { + // If our diagonal size is seven inches or greater, we consider ourselves a tablet. + return (getDiagonal() >= 7.0); + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean isChromebook() { + if (getContext() == null) { + return false; + } + return getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean isDeXMode() { + if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) { + return false; + } + try { + final Configuration config = getContext().getResources().getConfiguration(); + final Class configClass = config.getClass(); + return configClass.getField("SEM_DESKTOP_MODE_ENABLED").getInt(configClass) + == configClass.getField("semDesktopModeEnabled").getInt(config); + } catch(Exception ignored) { + return false; + } + } + + /** + * This method is called by SDL using JNI. + */ + public static DisplayMetrics getDisplayDPI() { + return getContext().getResources().getDisplayMetrics(); + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean getManifestEnvironmentVariables() { + try { + if (getContext() == null) { + return false; + } + + ApplicationInfo applicationInfo = getContext().getPackageManager().getApplicationInfo(getContext().getPackageName(), PackageManager.GET_META_DATA); + Bundle bundle = applicationInfo.metaData; + if (bundle == null) { + return false; + } + String prefix = "SDL_ENV."; + final int trimLength = prefix.length(); + for (String key : bundle.keySet()) { + if (key.startsWith(prefix)) { + String name = key.substring(trimLength); + String value = bundle.get(key).toString(); + nativeSetenv(name, value); + } + } + /* environment variables set! */ + return true; + } catch (Exception e) { + Log.v(TAG, "exception " + e.toString()); + } + return false; + } + + // This method is called by SDLControllerManager's API 26 Generic Motion Handler. + public static View getContentView() { + return mLayout; + } + + static class ShowTextInputTask implements Runnable { + /* + * This is used to regulate the pan&scan method to have some offset from + * the bottom edge of the input region and the top edge of an input + * method (soft keyboard) + */ + static final int HEIGHT_PADDING = 15; + + public int x, y, w, h; + + public ShowTextInputTask(int x, int y, int w, int h) { + this.x = x; + this.y = y; + this.w = w; + this.h = h; + + /* Minimum size of 1 pixel, so it takes focus. */ + if (this.w <= 0) { + this.w = 1; + } + if (this.h + HEIGHT_PADDING <= 0) { + this.h = 1 - HEIGHT_PADDING; + } + } + + @Override + public void run() { + RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(w, h + HEIGHT_PADDING); + params.leftMargin = x; + params.topMargin = y; + + if (mTextEdit == null) { + mTextEdit = new DummyEdit(SDL.getContext()); + + mLayout.addView(mTextEdit, params); + } else { + mTextEdit.setLayoutParams(params); + } + + mTextEdit.setVisibility(View.VISIBLE); + mTextEdit.requestFocus(); + + InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(mTextEdit, 0); + + mScreenKeyboardShown = true; + } + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean showTextInput(int x, int y, int w, int h) { + // Transfer the task to the main thread as a Runnable + return mSingleton.commandHandler.post(new ShowTextInputTask(x, y, w, h)); + } + + public static boolean isTextInputEvent(KeyEvent event) { + + // Key pressed with Ctrl should be sent as SDL_KEYDOWN/SDL_KEYUP and not SDL_TEXTINPUT + if (event.isCtrlPressed()) { + return false; + } + + return event.isPrintingKey() || event.getKeyCode() == KeyEvent.KEYCODE_SPACE; + } + + public static boolean handleKeyEvent(View v, int keyCode, KeyEvent event, InputConnection ic) { + int deviceId = event.getDeviceId(); + int source = event.getSource(); + + if (source == InputDevice.SOURCE_UNKNOWN) { + InputDevice device = InputDevice.getDevice(deviceId); + if (device != null) { + source = device.getSources(); + } + } + +// if (event.getAction() == KeyEvent.ACTION_DOWN) { +// Log.v("SDL", "key down: " + keyCode + ", deviceId = " + deviceId + ", source = " + source); +// } else if (event.getAction() == KeyEvent.ACTION_UP) { +// Log.v("SDL", "key up: " + keyCode + ", deviceId = " + deviceId + ", source = " + source); +// } + + // Dispatch the different events depending on where they come from + // Some SOURCE_JOYSTICK, SOURCE_DPAD or SOURCE_GAMEPAD are also SOURCE_KEYBOARD + // So, we try to process them as JOYSTICK/DPAD/GAMEPAD events first, if that fails we try them as KEYBOARD + // + // Furthermore, it's possible a game controller has SOURCE_KEYBOARD and + // SOURCE_JOYSTICK, while its key events arrive from the keyboard source + // So, retrieve the device itself and check all of its sources + if (SDLControllerManager.isDeviceSDLJoystick(deviceId)) { + // Note that we process events with specific key codes here + if (event.getAction() == KeyEvent.ACTION_DOWN) { + if (SDLControllerManager.onNativePadDown(deviceId, keyCode) == 0) { + return true; + } + } else if (event.getAction() == KeyEvent.ACTION_UP) { + if (SDLControllerManager.onNativePadUp(deviceId, keyCode) == 0) { + return true; + } + } + } + + if ((source & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) { + // on some devices key events are sent for mouse BUTTON_BACK/FORWARD presses + // they are ignored here because sending them as mouse input to SDL is messy + if ((keyCode == KeyEvent.KEYCODE_BACK) || (keyCode == KeyEvent.KEYCODE_FORWARD)) { + switch (event.getAction()) { + case KeyEvent.ACTION_DOWN: + case KeyEvent.ACTION_UP: + // mark the event as handled or it will be handled by system + // handling KEYCODE_BACK by system will call onBackPressed() + return true; + } + } + } + + if (event.getAction() == KeyEvent.ACTION_DOWN) { + if (isTextInputEvent(event)) { + if (ic != null) { + ic.commitText(String.valueOf((char) event.getUnicodeChar()), 1); + } else { + SDLInputConnection.nativeCommitText(String.valueOf((char) event.getUnicodeChar()), 1); + } + } + onNativeKeyDown(keyCode); + return true; + } else if (event.getAction() == KeyEvent.ACTION_UP) { + onNativeKeyUp(keyCode); + return true; + } + + return false; + } + + /** + * This method is called by SDL using JNI. + */ + public static Surface getNativeSurface() { + if (SDLActivity.mSurface == null) { + return null; + } + return SDLActivity.mSurface.getNativeSurface(); + } + + // Input + + /** + * This method is called by SDL using JNI. + */ + public static void initTouch() { + int[] ids = InputDevice.getDeviceIds(); + + for (int id : ids) { + InputDevice device = InputDevice.getDevice(id); + /* Allow SOURCE_TOUCHSCREEN and also Virtual InputDevices because they can send TOUCHSCREEN events */ + if (device != null && ((device.getSources() & InputDevice.SOURCE_TOUCHSCREEN) == InputDevice.SOURCE_TOUCHSCREEN + || device.isVirtual())) { + + int touchDevId = device.getId(); + /* + * Prevent id to be -1, since it's used in SDL internal for synthetic events + * Appears when using Android emulator, eg: + * adb shell input mouse tap 100 100 + * adb shell input touchscreen tap 100 100 + */ + if (touchDevId < 0) { + touchDevId -= 1; + } + nativeAddTouch(touchDevId, device.getName()); + } + } + } + + // Messagebox + + /** Result of current messagebox. Also used for blocking the calling thread. */ + protected final int[] messageboxSelection = new int[1]; + + /** + * This method is called by SDL using JNI. + * Shows the messagebox from UI thread and block calling thread. + * buttonFlags, buttonIds and buttonTexts must have same length. + * @param buttonFlags array containing flags for every button. + * @param buttonIds array containing id for every button. + * @param buttonTexts array containing text for every button. + * @param colors null for default or array of length 5 containing colors. + * @return button id or -1. + */ + public int messageboxShowMessageBox( + final int flags, + final String title, + final String message, + final int[] buttonFlags, + final int[] buttonIds, + final String[] buttonTexts, + final int[] colors) { + + messageboxSelection[0] = -1; + + // sanity checks + + if ((buttonFlags.length != buttonIds.length) && (buttonIds.length != buttonTexts.length)) { + return -1; // implementation broken + } + + // collect arguments for Dialog + + final Bundle args = new Bundle(); + args.putInt("flags", flags); + args.putString("title", title); + args.putString("message", message); + args.putIntArray("buttonFlags", buttonFlags); + args.putIntArray("buttonIds", buttonIds); + args.putStringArray("buttonTexts", buttonTexts); + args.putIntArray("colors", colors); + + // trigger Dialog creation on UI thread + + runOnUiThread(new Runnable() { + @Override + public void run() { + messageboxCreateAndShow(args); + } + }); + + // block the calling thread + + synchronized (messageboxSelection) { + try { + messageboxSelection.wait(); + } catch (InterruptedException ex) { + ex.printStackTrace(); + return -1; + } + } + + // return selected value + + return messageboxSelection[0]; + } + + protected void messageboxCreateAndShow(Bundle args) { + + // TODO set values from "flags" to messagebox dialog + + // get colors + + int[] colors = args.getIntArray("colors"); + int backgroundColor; + int textColor; + int buttonBorderColor; + int buttonBackgroundColor; + int buttonSelectedColor; + if (colors != null) { + int i = -1; + backgroundColor = colors[++i]; + textColor = colors[++i]; + buttonBorderColor = colors[++i]; + buttonBackgroundColor = colors[++i]; + buttonSelectedColor = colors[++i]; + } else { + backgroundColor = Color.TRANSPARENT; + textColor = Color.TRANSPARENT; + buttonBorderColor = Color.TRANSPARENT; + buttonBackgroundColor = Color.TRANSPARENT; + buttonSelectedColor = Color.TRANSPARENT; + } + + // create dialog with title and a listener to wake up calling thread + + final AlertDialog dialog = new AlertDialog.Builder(this).create(); + dialog.setTitle(args.getString("title")); + dialog.setCancelable(false); + dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface unused) { + synchronized (messageboxSelection) { + messageboxSelection.notify(); + } + } + }); + + // create text + + TextView message = new TextView(this); + message.setGravity(Gravity.CENTER); + message.setText(args.getString("message")); + if (textColor != Color.TRANSPARENT) { + message.setTextColor(textColor); + } + + // create buttons + + int[] buttonFlags = args.getIntArray("buttonFlags"); + int[] buttonIds = args.getIntArray("buttonIds"); + String[] buttonTexts = args.getStringArray("buttonTexts"); + + final SparseArray