From a2460df3160f671ae10789d2e49d7f9898de3aa5 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Fri, 30 May 2025 20:14:26 +0200 Subject: [PATCH 01/31] Print Luanti version after ascii art --- src/server.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/server.cpp b/src/server.cpp index a1263ef1dc..e89b9441d2 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -586,14 +586,16 @@ void Server::start() R"(| | |_| | (_| | | | | |_| |)", R"(|_|\__,_|\__,_|_| |_|\__|_|)", }; - if (!m_admin_chat) { // we're not printing to rawstream to avoid it showing up in the logs. // however it would then mess up the ncurses terminal (m_admin_chat), // so we skip it in that case. - for (auto line : art) - std::cerr << line << std::endl; + for (size_t i = 0; i < ARRLEN(art); ++i) + std::cerr << art[i] << (i == ARRLEN(art) - 1 ? "" : "\n"); + // add a "tail" with the engine version + std::cerr << " ___ " << g_version_hash << std::endl; } + actionstream << "World at [" << m_path_world << "]" << std::endl; actionstream << "Server for gameid=\"" << m_gamespec.id << "\" listening on "; From 56a7f0b7cf9943acd7c649fb018d484344e5b9a5 Mon Sep 17 00:00:00 2001 From: grorp Date: Sun, 1 Jun 2025 15:24:14 +0200 Subject: [PATCH 02/31] Don't break when multiple dialogs are shown on startup (#16204) --- builtin/fstk/ui.lua | 2 +- builtin/mainmenu/dlg_rebind_keys.lua | 20 ++++++++++---------- builtin/mainmenu/dlg_reinstall_mtg.lua | 26 +++++++++++++------------- builtin/mainmenu/init.lua | 8 ++++++-- 4 files changed, 30 insertions(+), 26 deletions(-) diff --git a/builtin/fstk/ui.lua b/builtin/fstk/ui.lua index 44d2ab3d89..31b104ed30 100644 --- a/builtin/fstk/ui.lua +++ b/builtin/fstk/ui.lua @@ -118,7 +118,7 @@ function ui.update() if (active_toplevel_ui_elements > 1) then core.log("warning", "more than one active ui ".. - "element, self most likely isn't intended") + "element, this most likely isn't intended") end if (active_toplevel_ui_elements == 0) then diff --git a/builtin/mainmenu/dlg_rebind_keys.lua b/builtin/mainmenu/dlg_rebind_keys.lua index d1b442004d..ec4d1357fd 100644 --- a/builtin/mainmenu/dlg_rebind_keys.lua +++ b/builtin/mainmenu/dlg_rebind_keys.lua @@ -33,13 +33,13 @@ end local function buttonhandler(this, fields) if fields.reconfigure then + local parent = this.parent + close_dialog(this) - local maintab = ui.find_by_name("maintab") - local dlg = create_settings_dlg("controls_keyboard_and_mouse") - dlg:set_parent(maintab) - maintab:hide() + dlg:set_parent(parent) + parent:hide() dlg:show() return true @@ -74,7 +74,7 @@ local function create_rebind_keys_dlg() return dlg end -function migrate_keybindings() +function migrate_keybindings(parent) -- Show migration dialog if the user upgraded from an earlier version -- and this has not yet been shown before, *or* if keys settings had to be changed if core.is_first_run then @@ -95,14 +95,14 @@ function migrate_keybindings() end if not has_migration then - return + return parent end - local maintab = ui.find_by_name("maintab") - local dlg = create_rebind_keys_dlg() - dlg:set_parent(maintab) - maintab:hide() + dlg:set_parent(parent) + parent:hide() dlg:show() ui.update() + + return dlg end diff --git a/builtin/mainmenu/dlg_reinstall_mtg.lua b/builtin/mainmenu/dlg_reinstall_mtg.lua index c167b2656e..6c512ea036 100644 --- a/builtin/mainmenu/dlg_reinstall_mtg.lua +++ b/builtin/mainmenu/dlg_reinstall_mtg.lua @@ -11,7 +11,7 @@ local SETTING_NAME = "no_mtg_notification" -function check_reinstall_mtg() +function check_reinstall_mtg(parent) -- used to be in minetest.conf if core.settings:get_bool(SETTING_NAME) then cache_settings:set_bool(SETTING_NAME, true) @@ -19,14 +19,14 @@ function check_reinstall_mtg() end if cache_settings:get_bool(SETTING_NAME) then - return + return parent end local games = core.get_games() for _, game in ipairs(games) do if game.id == "minetest" then cache_settings:set_bool(SETTING_NAME, true) - return + return parent end end @@ -40,16 +40,16 @@ function check_reinstall_mtg() end if not mtg_world_found then cache_settings:set_bool(SETTING_NAME, true) - return + return parent end - local maintab = ui.find_by_name("maintab") - local dlg = create_reinstall_mtg_dlg() - dlg:set_parent(maintab) - maintab:hide() + dlg:set_parent(parent) + parent:hide() dlg:show() ui.update() + + return dlg end local function get_formspec(dialogdata) @@ -74,22 +74,22 @@ end local function buttonhandler(this, fields) if fields.reinstall then + local parent = this.parent + -- Don't set "no_mtg_notification" here so that the dialog will be shown -- again if downloading MTG fails for whatever reason. this:delete() - local maintab = ui.find_by_name("maintab") - local dlg = create_contentdb_dlg(nil, "minetest/minetest") - dlg:set_parent(maintab) - maintab:hide() + dlg:set_parent(parent) + parent:hide() dlg:show() return true end if fields.dismiss then - cache_settings:set_bool("no_mtg_notification", true) + cache_settings:set_bool(SETTING_NAME, true) this:delete() return true end diff --git a/builtin/mainmenu/init.lua b/builtin/mainmenu/init.lua index 14185a4843..c1bbc0ae42 100644 --- a/builtin/mainmenu/init.lua +++ b/builtin/mainmenu/init.lua @@ -112,8 +112,12 @@ local function init_globals() tv_main:show() ui.update() - check_reinstall_mtg() - migrate_keybindings() + -- synchronous, chain parents to only show one at a time + local parent = tv_main + parent = migrate_keybindings(parent) + check_reinstall_mtg(parent) + + -- asynchronous, will only be shown if we're still on "maintab" check_new_version() end From 0bb87eb1ff891a0f8ede8bb4fcef1967284659ef Mon Sep 17 00:00:00 2001 From: JosiahWI <41302989+JosiahWI@users.noreply.github.com> Date: Sun, 1 Jun 2025 08:24:32 -0500 Subject: [PATCH 03/31] Avoid signal-unsafe operations in POSIX signal handler (#16160) --- src/client/clientlauncher.cpp | 8 ++++---- src/client/game.cpp | 24 +++++++++++++----------- src/client/game.h | 3 ++- src/gui/guiEngine.cpp | 4 +++- src/gui/guiEngine.h | 6 ++++-- src/main.cpp | 8 ++++---- src/porting.cpp | 22 ++++++++++------------ src/porting.h | 3 ++- src/server.cpp | 4 +++- src/server.h | 3 ++- src/terminal_chat_console.h | 10 ++++++---- 11 files changed, 53 insertions(+), 42 deletions(-) diff --git a/src/client/clientlauncher.cpp b/src/client/clientlauncher.cpp index 64f2e8f514..4c8eaf0602 100644 --- a/src/client/clientlauncher.cpp +++ b/src/client/clientlauncher.cpp @@ -147,8 +147,8 @@ bool ClientLauncher::run(GameStartData &start_data, const Settings &cmd_args) /* Menu-game loop */ - bool retval = true; - bool *kill = porting::signal_handler_killstatus(); + bool retval = true; + volatile auto *kill = porting::signal_handler_killstatus(); while (m_rendering_engine->run() && !*kill && !g_gamecallback->shutdown_requested) { @@ -540,9 +540,9 @@ bool ClientLauncher::launch_game(std::string &error_message, void ClientLauncher::main_menu(MainMenuData *menudata) { - bool *kill = porting::signal_handler_killstatus(); + volatile auto *kill = porting::signal_handler_killstatus(); video::IVideoDriver *driver = m_rendering_engine->get_video_driver(); - auto *device = m_rendering_engine->get_raw_device(); + auto *device = m_rendering_engine->get_raw_device(); // Wait until app is in foreground because of #15883 infostream << "Waiting for app to be in foreground" << std::endl; diff --git a/src/client/game.cpp b/src/client/game.cpp index 2c9c4fb771..f9a52c43f5 100644 --- a/src/client/game.cpp +++ b/src/client/game.cpp @@ -65,6 +65,8 @@ #include "client/sound/sound_openal.h" #endif +#include + class NodeDugEvent : public MtEvent { public: @@ -561,7 +563,7 @@ public: Game(); ~Game(); - bool startup(bool *kill, + bool startup(volatile std::sig_atomic_t *kill, InputHandler *input, RenderingEngine *rendering_engine, const GameStartData &game_params, @@ -793,14 +795,14 @@ private: This class does take ownership/responsibily for cleaning up etc of any of these items (e.g. device) */ - IrrlichtDevice *device; - RenderingEngine *m_rendering_engine; - video::IVideoDriver *driver; - scene::ISceneManager *smgr; - bool *kill; - std::string *error_message; - bool *reconnect_requested; - PausedNodesList paused_animated_nodes; + IrrlichtDevice *device; + RenderingEngine *m_rendering_engine; + video::IVideoDriver *driver; + scene::ISceneManager *smgr; + volatile std::sig_atomic_t *kill; + std::string *error_message; + bool *reconnect_requested; + PausedNodesList paused_animated_nodes; bool simple_singleplayer_mode; /* End 'cache' */ @@ -932,7 +934,7 @@ Game::~Game() m_rendering_engine->finalize(); } -bool Game::startup(bool *kill, +bool Game::startup(volatile std::sig_atomic_t *kill, InputHandler *input, RenderingEngine *rendering_engine, const GameStartData &start_data, @@ -4235,7 +4237,7 @@ void Game::readSettings() ****************************************************************************/ /****************************************************************************/ -void the_game(bool *kill, +void the_game(volatile std::sig_atomic_t *kill, InputHandler *input, RenderingEngine *rendering_engine, const GameStartData &start_data, diff --git a/src/client/game.h b/src/client/game.h index 6b838e2bca..95976ac7bb 100644 --- a/src/client/game.h +++ b/src/client/game.h @@ -6,6 +6,7 @@ #include "irrlichttypes.h" #include "config.h" +#include #include #if !IS_CLIENT_BUILD @@ -36,7 +37,7 @@ struct CameraOrientation { #define GAME_FALLBACK_TIMEOUT 1.8f #define GAME_CONNECTION_TIMEOUT 10.0f -void the_game(bool *kill, +void the_game(volatile std::sig_atomic_t *kill, InputHandler *input, RenderingEngine *rendering_engine, const GameStartData &start_data, diff --git a/src/gui/guiEngine.cpp b/src/gui/guiEngine.cpp index 0ec0d7a764..e843b5b388 100644 --- a/src/gui/guiEngine.cpp +++ b/src/gui/guiEngine.cpp @@ -33,6 +33,8 @@ #include "client/sound/sound_openal.h" #endif +#include + /******************************************************************************/ void TextDestGuiEngine::gotText(const StringMap &fields) @@ -104,7 +106,7 @@ GUIEngine::GUIEngine(JoystickController *joystick, RenderingEngine *rendering_engine, IMenuManager *menumgr, MainMenuData *data, - bool &kill) : + volatile std::sig_atomic_t &kill) : m_rendering_engine(rendering_engine), m_parent(parent), m_menumanager(menumgr), diff --git a/src/gui/guiEngine.h b/src/gui/guiEngine.h index f4b22f3c26..4fde215fb4 100644 --- a/src/gui/guiEngine.h +++ b/src/gui/guiEngine.h @@ -14,6 +14,8 @@ #include "util/enriched_string.h" #include "translation.h" +#include + /******************************************************************************/ /* Structs and macros */ /******************************************************************************/ @@ -124,7 +126,7 @@ public: RenderingEngine *rendering_engine, IMenuManager *menumgr, MainMenuData *data, - bool &kill); + volatile std::sig_atomic_t &kill); /** default destructor */ virtual ~GUIEngine(); @@ -193,7 +195,7 @@ private: irr_ptr m_menu; /** reference to kill variable managed by SIGINT handler */ - bool &m_kill; + volatile std::sig_atomic_t &m_kill; /** variable used to abort menu and return back to main game handling */ bool m_startgame = false; diff --git a/src/main.cpp b/src/main.cpp index 95a2f7be46..51069cf193 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1138,7 +1138,7 @@ static bool run_dedicated_server(const GameParams &game_params, const Settings & return false; } ChatInterface iface; - bool &kill = *porting::signal_handler_killstatus(); + volatile auto &kill = *porting::signal_handler_killstatus(); try { // Create server @@ -1181,7 +1181,7 @@ static bool run_dedicated_server(const GameParams &game_params, const Settings & server.start(); // Run server - bool &kill = *porting::signal_handler_killstatus(); + volatile auto &kill = *porting::signal_handler_killstatus(); dedicated_server_loop(server, kill); } catch (const ModError &e) { @@ -1226,7 +1226,7 @@ static bool migrate_map_database(const GameParams &game_params, const Settings & u32 count = 0; u64 last_update_time = 0; - bool &kill = *porting::signal_handler_killstatus(); + volatile auto &kill = *porting::signal_handler_killstatus(); std::vector blocks; old_db->listAllLoadableBlocks(blocks); @@ -1280,7 +1280,7 @@ static bool recompress_map_database(const GameParams &game_params, const Setting u32 count = 0; u64 last_update_time = 0; - bool &kill = *porting::signal_handler_killstatus(); + volatile auto &kill = *porting::signal_handler_killstatus(); const u8 serialize_as_ver = SER_FMT_VER_HIGHEST_WRITE; const s16 map_compression_level = rangelim(g_settings->getS16("map_compression_level_disk"), -1, 9); diff --git a/src/porting.cpp b/src/porting.cpp index 11ff63a88a..711b65db69 100644 --- a/src/porting.cpp +++ b/src/porting.cpp @@ -60,6 +60,7 @@ #include "util/string.h" #include "util/tracy_wrapper.h" #include +#include #include #include #include @@ -81,31 +82,28 @@ namespace porting Signal handler (grabs Ctrl-C on POSIX systems) */ -static bool g_killed = false; +volatile static std::sig_atomic_t g_killed = false; -bool *signal_handler_killstatus() +volatile std::sig_atomic_t *signal_handler_killstatus() { return &g_killed; } #if !defined(_WIN32) // POSIX +#define STDERR_FILENO 2 static void signal_handler(int sig) { if (!g_killed) { if (sig == SIGINT) { - dstream << "INFO: signal_handler(): " - << "Ctrl-C pressed, shutting down." << std::endl; + const char *dbg_text{"INFO: signal_handler(): " + "Ctrl-C pressed, shutting down.\n"}; + write(STDERR_FILENO, dbg_text, strlen(dbg_text)); } else if (sig == SIGTERM) { - dstream << "INFO: signal_handler(): " - << "got SIGTERM, shutting down." << std::endl; + const char *dbg_text{"INFO: signal_handler(): " + "got SIGTERM, shutting down.\n"}; + write(STDERR_FILENO, dbg_text, strlen(dbg_text)); } - - // Comment out for less clutter when testing scripts - /*dstream << "INFO: sigint_handler(): " - << "Printing debug stacks" << std::endl; - debug_stacks_print();*/ - g_killed = true; } else { (void)signal(sig, SIG_DFL); diff --git a/src/porting.h b/src/porting.h index f7d623d335..1a4bb9e7bc 100644 --- a/src/porting.h +++ b/src/porting.h @@ -13,6 +13,7 @@ #endif // Be mindful of what you include here! +#include #include #include "config.h" #include "irrlichttypes.h" // u64 @@ -77,7 +78,7 @@ namespace porting void signal_handler_init(); // Returns a pointer to a bool. // When the bool is true, program should quit. -[[nodiscard]] bool *signal_handler_killstatus(); +[[nodiscard]] volatile std::sig_atomic_t *signal_handler_killstatus(); /* Path of static data directory. diff --git a/src/server.cpp b/src/server.cpp index e89b9441d2..89bba75fbd 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -65,6 +65,8 @@ #include "gettext.h" #include "util/tracy_wrapper.h" +#include + class ClientNotFoundException : public BaseException { public: @@ -4114,7 +4116,7 @@ std::unique_ptr Server::emergePlayer(const char *name, session_t peer return playersao; } -void dedicated_server_loop(Server &server, bool &kill) +void dedicated_server_loop(Server &server, volatile std::sig_atomic_t &kill) { verbosestream<<"dedicated_server_loop()"< +#include #include #include #include @@ -799,4 +800,4 @@ private: Shuts down when kill is set to true. */ -void dedicated_server_loop(Server &server, bool &kill); +void dedicated_server_loop(Server &server, volatile std::sig_atomic_t &kill); diff --git a/src/terminal_chat_console.h b/src/terminal_chat_console.h index f49557d0cc..3324656dc7 100644 --- a/src/terminal_chat_console.h +++ b/src/terminal_chat_console.h @@ -9,6 +9,8 @@ #include "util/container.h" #include "log.h" #include "log_internal.h" + +#include #include #include @@ -45,7 +47,7 @@ public: void setup( ChatInterface *iface, - bool *kill_requested, + volatile std::sig_atomic_t *kill_requested, const std::string &nick) { m_nick = nick; @@ -96,9 +98,9 @@ private: int m_rows; bool m_can_draw_text; - bool *m_kill_requested = nullptr; - ChatBackend m_chat_backend; - ChatInterface *m_chat_interface; + volatile std::sig_atomic_t *m_kill_requested = nullptr; + ChatBackend m_chat_backend; + ChatInterface *m_chat_interface; TermLogOutput m_log_output; From fde6384a099143a4a3033e090d61296b1d15f051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20M=C3=BCller?= <34514239+appgurueu@users.noreply.github.com> Date: Sun, 1 Jun 2025 23:21:35 +0200 Subject: [PATCH 04/31] Fix and clean up skeletal animation (#15722) * Fix attachments lagging behind their parents (#14818) * Fix animation blending (#14817) * Bring back cool guy as another .x smoke test * Add .x mesh loader unittest * Do bounding box & matrix calculation at proper point in time * Remove obsolete `SAnimatedMesh` --- doc/lua_api.md | 3 + .../mods/testentities/models/LICENSE.txt | 8 +- .../models/testentities_cool_guy.png | Bin 0 -> 3673 bytes .../models/testentities_cool_guy.x | 2 + games/devtest/mods/testentities/visuals.lua | 13 + irr/include/IAnimatedMesh.h | 22 +- irr/include/IAnimatedMeshSceneNode.h | 47 +- irr/include/IBoneSceneNode.h | 88 +-- irr/include/IMesh.h | 36 +- irr/include/IMeshManipulator.h | 20 - irr/include/IMeshSceneNode.h | 2 +- irr/include/ISceneNode.h | 14 +- irr/include/SAnimatedMesh.h | 167 ----- irr/include/SMesh.h | 13 +- irr/include/SkinnedMesh.h | 146 ++-- irr/include/Transform.h | 42 ++ irr/include/quaternion.h | 2 +- irr/src/CAnimatedMeshSceneNode.cpp | 337 ++++----- irr/src/CAnimatedMeshSceneNode.h | 44 +- irr/src/CB3DMeshFileLoader.cpp | 34 +- irr/src/CBoneSceneNode.cpp | 86 --- irr/src/CBoneSceneNode.h | 71 +- irr/src/CGLTFMeshFileLoader.cpp | 46 +- irr/src/CMakeLists.txt | 1 - irr/src/CMeshCache.cpp | 8 +- irr/src/CMeshManipulator.cpp | 30 - irr/src/CMeshManipulator.h | 9 - irr/src/CNullDriver.cpp | 2 +- irr/src/COBJMeshFileLoader.cpp | 21 +- irr/src/CSceneManager.cpp | 1 - irr/src/CXMeshFileLoader.cpp | 12 +- irr/src/SkinnedMesh.cpp | 656 +++++++----------- lib/tiniergltf/tiniergltf.hpp | 7 +- src/client/content_cao.cpp | 77 +- src/client/content_cao.h | 2 - src/client/mesh.cpp | 10 +- src/nodedef.cpp | 8 - src/unittest/CMakeLists.txt | 2 + src/unittest/test_irr_gltf_mesh_loader.cpp | 44 +- src/unittest/test_irr_x_mesh_loader.cpp | 111 +++ 40 files changed, 856 insertions(+), 1388 deletions(-) create mode 100644 games/devtest/mods/testentities/models/testentities_cool_guy.png create mode 100755 games/devtest/mods/testentities/models/testentities_cool_guy.x delete mode 100644 irr/include/SAnimatedMesh.h create mode 100644 irr/include/Transform.h delete mode 100644 irr/src/CBoneSceneNode.cpp create mode 100644 src/unittest/test_irr_x_mesh_loader.cpp diff --git a/doc/lua_api.md b/doc/lua_api.md index 3a6da1bdb8..07f70c36fe 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -315,6 +315,9 @@ due to their space savings. Bone weights should be normalized, e.g. using ["normalize all" in Blender](https://docs.blender.org/manual/en/4.2/grease_pencil/modes/weight_paint/weights_menu.html#normalize-all). +Note that nodes using matrix transforms must not be animated. +This also extends to bone overrides, which must not be applied to them. + You can use the [Khronos glTF validator](https://github.com/KhronosGroup/glTF-Validator) to check whether a model is a valid glTF file. diff --git a/games/devtest/mods/testentities/models/LICENSE.txt b/games/devtest/mods/testentities/models/LICENSE.txt index 19ffffc5cc..6ec445db69 100644 --- a/games/devtest/mods/testentities/models/LICENSE.txt +++ b/games/devtest/mods/testentities/models/LICENSE.txt @@ -12,4 +12,10 @@ Jordach (CC BY-SA 3.0): Zeg9 (CC BY-SA 3.0): testentities_lava_flan.x - testentities_lava_flan.png \ No newline at end of file + testentities_lava_flan.png + +"Cool Guy": + +hecks (refer to irr/LICENSE): + testentities_cool_guy.x + testentities_cool_guy.png diff --git a/games/devtest/mods/testentities/models/testentities_cool_guy.png b/games/devtest/mods/testentities/models/testentities_cool_guy.png new file mode 100644 index 0000000000000000000000000000000000000000..84cc12e442b73df3583daed7fe9c5433b86c5749 GIT binary patch literal 3673 zcmd6qXHeA5md5`>90o~}Gl&StAUOz!nB14p%GeZ0aIHU>mG&^jGYd(mA{u>)a*y@H(8g*X6!e4Ffa#$1++=q z70I+EM+qUxQ3{wS2?1e&NHH-IKFw&pN$^{FK#oI>L#2q)x{wmwv!^h)<* z33zDJ8Oo7MbNFs^M`)HEx9d9MDz=?%DPf_*F}AYvf_FukRnd05z+!Sg_4umbrw6~E z7#}H`%+_<)7Qd^QXx@{t@0rS6?GLb-Mew#j%mQh;% z5tQprzF5k*^6rDjv$mOPYbKQZLNk~wCfE#&%mH2qG+ z(1xPlteMPUeF5BPrqXQMqcAMlU4p*v{;6kvF;DUf7Nr~o>!cn)MMb3e+G(f>epDj= z3bh!L%lTu)efVfhU#z7}?4$|vU^X)$q1>+LW2M7Dru@rLOtl*o6;%;q^W(P1#3lGj zN=j{QZJXnzm4>Dg?$ZI-!5l8NxRMwCZqJ^j^jLPUmz6xtp?KX)K`8V|@hJZKP8hpb zD7Hv-S$V-!w?vyHS`s691^+bJ6fImO_j7P~)*IJ6VX0##=`q_oFp$MrTocH4mcEs~ zWq{I=4L;E-YHNE#!Tunpu(XL5_E0QwZ5q{0$ zC^!lXb<%bDf8Dq~k)LY{*k8gD&$;-=2V$b;=&H*Ycikik2{7Cs8-M+l)d-q+! z>ir81leRT;^&EZR70+8?mwnxy=leTpn&Gm<6l}@N=IcS%SLgZKX};1K`VWU2Ifs*n zvR>!r278C$yicJRHDuN@rxu)C*6(4qnB=*h*!wXm>|v|X&B z_Zw!m%uPCH86M^0t$|(nK5KEvR`WVvJ|3Pfr}nU_d`b9fgNUVo3e5iC zIg@x8`=b$833eHHuhH@$b_j{PJmul$t|h?w*Y)8SaRy5^DzmF<{auahLH&fd}nF7sg-B3MV#MOWCO&T_m$|;mQoC zV>DXdOlo)?Z;ZiZDfzH!{SJGLtyVsJXFm}-Tm+k8i(x}qA45`8YS4FWdef2?qKGTo zmKh7zRg#=C>Aws)4eiV}y)YFK|Fya^Q(qH`3wk{?KdB_%K}3I5?z>@tpfLV~7OzSqbq{Uw!e74Vx|&R%YOTIXAx|CI#Wh4s)4(}PIS7%<MOA+02|$40m-c>mJPZ{(9EYLNBMh&PG?^@TVWX8UF17t?y*8sWd*p#5Z~ zoc*g|aPhI--S%ARTb%DSEjnjyWmN)M=+~auSdkhO3Req|c_@i4xmf1IWFZJojk}yQ z!2c~#gqx@@1VL~ofd5W%n#BCNJeP z{~@jd@)<81XsB*}!av?_CD?j0fx$iR9=(OOwsCMGc?6eAK0V3>coCnY8ouSo#f7(e zx2O9>C z*oxZh4RTtMsqX23Etm|FxSv(|?N3>+V+2~R8D%!7KY_k?@f+~2<}v1w7m!5T8Q*<+ zT`{~L`!y38VqVuuz12NWd+O(TvG;osRQ^stCpE!;ruE#cyyx?|Z;5*3kY=#HLg?g% zr?lMEfF;n|)8FwrvF7m6M^K{Eg<&tVsCNkYhA1KE=YZ4H1Sfq<<4E@a6=+gK2phBr zrmJP_gj_H_s@@;c<+^x4QfNO^QHmxWHc{6!vHJZ;fme$~&*rJ}ijod%OJ-*IaYXa8 zJ6Wue$i6|^OhWwUwCvE2O@_Zlm08)hlWaKaMY^q2B@752%F+JgO@cfzz4M21B*0Iv zWb-So8@Ny9smQ&2>vF{enpi7&-*lyDW2^kd{7asO$6_uR;SXc=rrkAT9F=dE@G6>; zC{Dx3RK2DO3HljRCQ>3Z`i?m-_Txuf4v93SQQN%MW4HXb%v5^eyT=tNUAjlZ6^&3L z`TXQO=v@>py>|4)%RG5!OgQCb{F1G@@;xW>&%@`f-P94=2|Dzh8Ul_z-#~TW(@@#p zFF??s`&@0?aez8S-MK@8Qv@D;5{qLi=EBl@=e zc>(k;iZs|OmtLAp{mSOucc*RdLHskl-hS){Yj8Nh1EQRR34!+Vv`m-wPEIX`Uu{3nn?k>i#km6bCvj*~I6?c-Kx8hcs1?$BCyO zaxpNxDZ*gLN3@(W;wizGMN5+!t`3`2$;yE#e&a=9iDa~z@bMf`y~`T-qf+;08fr_g z9_&+|FX2YSF1N`XUQMIyY;6uIQkp9iysZuU_gXIU9+WK|e(>;Tp-Txi;XvWrTBA-p z%L1-0*34%SiPk7ads*ifm6#Z!X(_~()^rR$FLSVD$6cst@ydi3I|6S$2%rd6kYJr2 z#eExcV$3sNoE=jyzqD?OBk188iSr02N*;q7Y2Auw-tl4*c>>V1K+~c{Bd!`NY;4wk z0hL$*&#t0rUgg3eUkBrtRdy z?fYX$m(eZkW z?W?8R9S(8#SD=wCH~9aw1v;bqS7!eS{co-K+u6Sj0{^q$|K={SoHBh|gr|C2!JjW9 OprN9p{PnR_*na>&K!GLz literal 0 HcmV?d00001 diff --git a/games/devtest/mods/testentities/models/testentities_cool_guy.x b/games/devtest/mods/testentities/models/testentities_cool_guy.x new file mode 100755 index 0000000000..e806d8315d --- /dev/null +++ b/games/devtest/mods/testentities/models/testentities_cool_guy.x @@ -0,0 +1,2 @@ +xof 0303txt 0032 +AnimationSet{Animation{{Armature}AnimationKey{0;2;0;4;1,0,0,0;;,29;4;1,0,0,0;;;}AnimationKey{2;2;0;3;0,0,0;;,29;3;0,0,0;;;}}Animation{{Armature_knee_r}AnimationKey{0;16;0;4;0.864183,0.503177,0,0;;,1;4;0.829812,0.558043,0,0;;,3;4;0.708698,0.705512,0,0;;,5;4;0.589108,0.808054,0,0;;,7;4;0.593659,0.804717,0,0;;,9;4;0.748627,0.662991,0,0;;,11;4;0.910305,0.413938,0,0;;,13;4;0.975925,0.218107,0,0;;,15;4;0.981302,0.192476,0,0;;,17;4;0.975476,0.220108,0,0;;,19;4;0.963662,0.267124,0,0;;,21;4;0.945893,0.324478,0,0;;,23;4;0.923816,0.382838,0,0;;,25;4;0.901205,0.433394,0,0;;,27;4;0.883429,0.468566,0,0;;,29;4;0.876305,0.481757,0,0;;;}AnimationKey{2;2;0;3;0,0,1.10139;;,29;3;0,0,1.10139;;;}}Animation{{Armature_elbow_r}AnimationKey{0;16;0;4;0.756295,0.004619,-0.619265,0.210967;;,1;4;0.771977,0.005599,-0.60257,0.202311;;,3;4;0.825501,0.009164,-0.538259,0.169533;;,5;4;0.891859,0.014253,-0.436142,0.119019;;,7;4;0.949154,0.019821,-0.308768,0.058108;;,9;4;0.983251,0.024703,-0.18057,-0.001258;;,11;4;0.995416,0.028143,-0.07812,-0.047458;;,13;4;0.996672,0.02991,-0.020368,-0.073041;;,15;4;0.996672,0.02991,-0.020368,-0.073041;;,17;4;0.995416,0.028143,-0.07812,-0.047458;;,19;4;0.983251,0.024703,-0.18057,-0.001258;;,21;4;0.949154,0.019821,-0.308768,0.058108;;,23;4;0.891859,0.014253,-0.436142,0.119019;;,25;4;0.825501,0.009164,-0.538259,0.169533;;,27;4;0.771977,0.005599,-0.60257,0.202311;;,29;4;0.750682,0.004275,-0.625038,0.213976;;;}AnimationKey{2;2;0;3;0,0,0.754892;;,29;3;0,0,0.754892;;;}}Animation{{Armature_arm_r}AnimationKey{0;16;0;4;0.28219,0.629905,0.723388,-0.017285;;,1;4;0.277641,0.632543,0.722699,-0.022614;;,3;4;0.261375,0.641615,0.719924,-0.041507;;,5;4;0.238321,0.653533,0.715186,-0.067874;;,7;4;0.212026,0.665838,0.708676,-0.097381;;,9;4;0.186345,0.676585,0.701229,-0.125643;;,11;4;0.165298,0.684491,0.694351,-0.14841;;,13;4;0.152894,0.688778,0.68998,-0.161665;;,15;4;0.152894,0.688779,0.68998,-0.161665;;,17;4;0.165298,0.684491,0.694351,-0.14841;;,19;4;0.186345,0.676585,0.701229,-0.125643;;,21;4;0.212026,0.665838,0.708676,-0.097381;;,23;4;0.238321,0.653533,0.715186,-0.067874;;,25;4;0.261375,0.641615,0.719924,-0.041507;;,27;4;0.277641,0.632543,0.722699,-0.022614;;,29;4;0.283802,0.628959,0.723623,-0.015394;;;}AnimationKey{2;2;0;3;-0.545315,0,1;;,29;3;-0.545315,0,1;;;}}Animation{{Armature_knee_l}AnimationKey{0;16;0;4;0.981896,0.189423,0,0;;,1;4;0.9814,0.191974,0,0;;,3;4;0.979127,0.203251,0,0;;,5;4;0.974526,0.224276,0,0;;,7;4;0.96645,0.256853,0,0;;,9;4;0.953088,0.302692,0,0;;,11;4;0.931731,0.36315,0,0;;,13;4;0.898645,0.438676,0,0;;,15;4;0.848226,0.529634,0,0;;,17;4;0.773692,0.633562,0,0;;,19;4;0.689831,0.72397,0,0;;,21;4;0.629304,0.777159,0,0;;,23;4;0.648685,0.761057,0,0;;,25;4;0.812268,0.583284,0,0;;,27;4;0.948066,0.318074,0,0;;,29;4;0.982049,0.188624,0,0;;;}AnimationKey{2;2;0;3;0,0,1.10139;;,29;3;0,0,1.10139;;;}}Animation{{Armature_Bone_007}AnimationKey{0;16;0;4;0.993671,-0.112331,0,0;;,1;4;0.994784,-0.102002,0,0;;,3;4;0.997507,-0.070564,0,0;;,5;4;0.999237,-0.039056,0,0;;,7;4;0.999694,-0.024737,0,0;;,9;4;0.999079,-0.042907,0,0;;,11;4;0.99677,-0.080308,0,0;;,13;4;0.993798,-0.111199,0,0;;,15;4;0.993599,-0.112965,0,0;;,17;4;0.995813,-0.091409,0,0;;,19;4;0.998181,-0.060285,0,0;;,21;4;0.999479,-0.032286,0,0;;,23;4;0.999797,-0.020142,0,0;;,25;4;0.998983,-0.045097,0,0;;,27;4;0.995813,-0.091409,0,0;;,29;4;0.993221,-0.116243,0,0;;;}AnimationKey{2;2;0;3;0,0,1.221802;;,29;3;0,0,1.221802;;;}}Animation{{Armature_elbow_l}AnimationKey{0;16;0;4;0.995195,-0.034868,-0.015799,-0.090119;;,1;4;0.993465,-0.046368,-0.030155,-0.099838;;,3;4;0.983557,-0.0879,-0.082099,-0.134715;;,5;4;0.959324,-0.146904,-0.156177,-0.183648;;,7;4;0.917546,-0.212233,-0.238611,-0.236921;;,9;4;0.864109,-0.271657,-0.314022,-0.284443;;,11;4;0.813172,-0.315829,-0.370387,-0.319087;;,13;4;0.781004,-0.339668,-0.400938,-0.337501;;,15;4;0.781004,-0.339668,-0.400938,-0.337501;;,17;4;0.813172,-0.315829,-0.370387,-0.319087;;,19;4;0.864109,-0.271657,-0.314022,-0.284443;;,21;4;0.917546,-0.212233,-0.238611,-0.236921;;,23;4;0.959324,-0.146904,-0.156177,-0.183648;;,25;4;0.983557,-0.0879,-0.082099,-0.134715;;,27;4;0.993465,-0.046368,-0.030155,-0.099838;;,29;4;0.995701,-0.030812,-0.010739,-0.086685;;;}AnimationKey{2;2;0;3;0,0,0.754892;;,29;3;0,0,0.754892;;;}}Animation{{Armature_body}AnimationKey{0;16;0;4;-0,0,0.601298,0.799025;;,1;4;-0,0,0.608144,0.793827;;,3;4;-0,0,0.627465,0.778645;;,5;4;-0,0,0.643183,0.765712;;,7;4;-0,0,0.643755,0.765231;;,9;4;-0,0,0.631076,0.775721;;,11;4;-0,0,0.613775,0.789481;;,13;4;-0,0,0.6007,0.799474;;,15;4;-0,0,0.601488,0.798882;;,17;4;-0,0,0.619499,0.784997;;,19;4;-0,0,0.643196,0.765702;;,21;4;-0,0,0.660441,0.750878;;,23;4;-0,0,0.659666,0.751559;;,25;4;-0,0,0.638264,0.769817;;,27;4;-0,0,0.611752,0.791049;;,29;4;-0,0,0.598631,0.801025;;;}AnimationKey{2;2;0;3;0,2.580534,0;;,29;3;0,2.571201,0;;;}}Animation{{Armature_leg_l}AnimationKey{0;16;0;4;0.390287,0.920693,0,0;;,1;4;0.362565,0.931959,0,0;;,3;4;0.266163,0.963928,0,0;;,5;4;0.138294,0.990391,0,0;;,7;4;0.012725,0.999919,0,0;;,9;4;-0.090194,0.995924,0,0;;,11;4;-0.162502,0.986708,0,0;;,13;4;-0.201466,0.979496,0,0;;,15;4;-0.185641,0.982618,0,0;;,17;4;-0.013697,0.999906,0,0;;,19;4;0.24238,0.970181,0,0;;,21;4;0.417271,0.908782,0,0;;,23;4;0.439308,0.898336,0,0;;,25;4;0.424255,0.905543,0,0;;,27;4;0.407664,0.913132,0,0;;,29;4;0.400263,0.9164,0,0;;;}AnimationKey{2;2;0;3;0.246294,0,-0.171352;;,29;3;0.246294,0,-0.171351;;;}}Animation{{Armature_leg_r}AnimationKey{0;16;0;4;0.174933,-0.98458,0,0;;,1;4;0.082829,-0.996564,0,0;;,3;4;-0.21147,-0.977384,0,0;;,5;4;-0.442802,-0.89662,0,0;;,7;4;-0.47604,-0.879424,0,0;;,9;4;-0.47279,-0.881175,0,0;;,11;4;-0.459567,-0.888143,0,0;;,13;4;-0.427425,-0.904051,0,0;;,15;4;-0.361724,-0.932285,0,0;;,17;4;-0.251362,-0.967893,0,0;;,19;4;-0.114531,-0.99342,0,0;;,21;4;0.021053,-0.999778,0,0;;,23;4;0.12473,-0.992191,0,0;;,25;4;0.181473,-0.983396,0,0;;,27;4;0.204037,-0.978963,0,0;;,29;4;0.208187,-0.978089,0,0;;;}AnimationKey{2;2;0;3;-0.246294,0,-0.171352;;,29;3;-0.246294,0,-0.171351;;;}}Animation{{Armature_arm_l}AnimationKey{0;16;0;4;0.200754,-0.659656,-0.716264,-0.107316;;,1;4;0.192268,-0.660735,-0.716526,-0.114246;;,3;4;0.161871,-0.663925,-0.716753,-0.138802;;,5;4;0.118745,-0.666682,-0.715211,-0.17294;;,7;4;0.069733,-0.667364,-0.710872,-0.210767;;,9;4;0.022313,-0.665594,-0.704111,-0.246404;;,11;4;-0.016046,-0.662426,-0.696821,-0.274543;;,13;4;-0.038374,-0.659874,-0.691824,-0.290643;;,15;4;-0.038373,-0.659874,-0.691824,-0.290643;;,17;4;-0.016044,-0.662427,-0.696822,-0.274543;;,19;4;0.022312,-0.665594,-0.70411,-0.246404;;,21;4;0.069733,-0.667365,-0.710872,-0.210767;;,23;4;0.118745,-0.666682,-0.715211,-0.17294;;,25;4;0.161871,-0.663925,-0.716753,-0.138802;;,27;4;0.192268,-0.660735,-0.716526,-0.114246;;,29;4;0.203757,-0.659255,-0.716151,-0.104856;;;}AnimationKey{2;2;0;3;0.545315,0,1;;,29;3;0.545315,0,1;;;}}}Frame Root{FrameTransformMatrix{1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1;;}Frame Armature{FrameTransformMatrix{1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1;;}Frame Armature_body{FrameTransformMatrix{-1,0,0,0,0,0,1,0,0,1,0,0,0,2.571201,0,1;;}Frame Armature_arm_r{FrameTransformMatrix{-0.047733,0.997488,-0.05233,0,0.901521,0.020464,-0.432251,0,-0.430095,-0.067809,-0.900233,0,-0.545315,0,1,1;;}Frame Armature_elbow_r{FrameTransformMatrix{0.987983,0.151721,-0.029519,0,-0.153228,0.986478,-0.058162,0,0.020295,0.061987,0.997871,0,0,0,0.754892,1;;}}}Frame Armature_arm_l{FrameTransformMatrix{-0.047732,0.994072,-0.097683,0,0.901521,0.084983,0.424309,0,0.430095,-0.067809,-0.900233,0,0.545315,0,1,1;;}Frame Armature_elbow_l{FrameTransformMatrix{0.984741,0.173286,-0.016044,0,-0.171963,0.983073,0.063221,0,0.026727,-0.059497,0.99787,0,0,0,0.754892,1;;}}}Frame Armature_leg_l{FrameTransformMatrix{1,0,0,0,0,-0.998426,-0.056453,0,0,0.056453,-0.998405,0,0.246294,0,-0.171351,1;;}Frame Armature_knee_l{FrameTransformMatrix{1,0,0,0,0,0.993861,-0.110639,0,0,0.110639,0.993861,0,0,0,1.10139,1;;}}}Frame Armature_leg_r{FrameTransformMatrix{1,0,0,0,0,-0.998426,-0.056453,0,0,0.056453,-0.998405,0,-0.246294,0,-0.171351,1;;}Frame Armature_knee_r{FrameTransformMatrix{1,0,0,0,0,0.993861,-0.110639,0,0,0.110639,0.993861,0,0,0,1.10139,1;;}}}Frame Armature_Bone_007{FrameTransformMatrix{1,0,0,0,0,1,0,0,0,0,1,0,0,0,1.221802,1;;}}}Frame cool_dude{FrameTransformMatrix{-1,0,0,0,0,1,0,0,0,0,-1,0,0,0,0,1;;}Mesh{272;0;2.440814;0.219926;,0;3.688199;0.219926;,0.466212;3.688199;0.219926;,0.466212;2.440814;0.219926;,0.466212;2.440814;0.219926;,0.466212;3.688199;0.219926;,0.466212;3.688199;-0.219926;,0.466212;2.440814;-0.219926;,0;2.440814;0.219926;,0.466212;2.440814;0.219926;,0.466212;2.440814;-0.219926;,0;2.440814;-0.219926;,0.055633;1.27575;-0.190081;,0.055633;2.35741;-0.190081;,0.055633;2.35741;0.190081;,0.055633;1.27575;0.190081;,0.055633;1.27575;0.190081;,0.055633;2.35741;0.190081;,0.43017;2.35741;0.190081;,0.43017;1.27575;0.190081;,0.43017;1.27575;0.190081;,0.43017;2.35741;0.190081;,0.43017;2.35741;-0.190081;,0.43017;1.27575;-0.190081;,0.43017;1.27575;-0.190081;,0.43017;2.35741;-0.190081;,0.055633;2.35741;-0.190081;,0.055633;1.27575;-0.190081;,0.055633;1.27575;0.190081;,0.43017;1.27575;0.190081;,0.43017;1.27575;-0.190081;,0.055633;1.27575;-0.190081;,0.43017;2.35741;0.190081;,0.055633;2.35741;0.190081;,0.055633;2.35741;-0.190081;,0.43017;2.35741;-0.190081;,0.466212;3.688199;0.219926;,0;3.688199;0.219926;,0;3.688199;-0.219926;,0.466212;3.688199;-0.219926;,0.466212;2.440814;-0.219926;,0.466212;3.688199;-0.219926;,0;3.688199;-0.219926;,0;2.440814;-0.219926;,0.769341;2.834949;-0.041122;,0.440953;3.555781;-0.041122;,0.440953;3.555781;0.207294;,0.769341;2.834949;0.207294;,0.769341;2.834949;0.207294;,0.440953;3.555781;0.207294;,0.616273;3.635651;0.207294;,0.944661;2.914819;0.207294;,0.944661;2.914819;0.207294;,0.616273;3.635651;0.207294;,0.616273;3.635651;-0.041122;,0.944661;2.914819;-0.041122;,0.944661;2.914819;-0.041122;,0.616273;3.635651;-0.041122;,0.440953;3.555781;-0.041122;,0.769341;2.834949;-0.041122;,0.769341;2.834949;0.207294;,0.944661;2.914819;0.207294;,0.944661;2.914819;-0.041122;,0.769341;2.834949;-0.041122;,0.616273;3.635651;0.207294;,0.440953;3.555781;0.207294;,0.440953;3.555781;-0.041122;,0.616273;3.635651;-0.041122;,1.104504;2.080977;-0.086788;,0.776116;2.801809;-0.086788;,0.776116;2.801809;0.161627;,1.104504;2.080977;0.161627;,1.104504;2.080977;0.161627;,0.776116;2.801809;0.161627;,0.951436;2.881679;0.161627;,1.279824;2.160847;0.161627;,1.279824;2.160847;0.161627;,0.951436;2.881679;0.161627;,0.951436;2.881679;-0.086788;,1.279824;2.160847;-0.086788;,1.279824;2.160847;-0.086788;,0.951436;2.881679;-0.086788;,0.776116;2.801809;-0.086788;,1.104504;2.080977;-0.086788;,1.104504;2.080977;0.161627;,1.279824;2.160847;0.161627;,1.279824;2.160847;-0.086788;,1.104504;2.080977;-0.086788;,0.951436;2.881679;0.161627;,0.776116;2.801809;0.161627;,0.776116;2.801809;-0.086788;,0.951436;2.881679;-0.086788;,0.055633;0.093601;-0.190081;,0.055633;1.205294;-0.190081;,0.055633;1.205294;0.190081;,0.055633;0.093601;0.190081;,0.055633;0.093601;0.190081;,0.055633;1.205294;0.190081;,0.43017;1.205294;0.190081;,0.43017;0.093601;0.190081;,0.43017;0.093601;0.190081;,0.43017;1.205294;0.190081;,0.43017;1.205294;-0.190081;,0.43017;0.093601;-0.190081;,0.43017;0.093601;-0.190081;,0.43017;1.205294;-0.190081;,0.055633;1.205294;-0.190081;,0.055633;0.093601;-0.190081;,0.055633;0.093601;0.190081;,0.43017;0.093601;0.190081;,0.43017;0.093601;-0.190081;,0.055633;0.093601;-0.190081;,0.43017;1.205294;0.190081;,0.055633;1.205294;0.190081;,0.055633;1.205294;-0.190081;,0.43017;1.205294;-0.190081;,0;3.790919;0.428464;,0;4.579204;0.428464;,0.43344;4.560537;0.409797;,0.43344;3.809586;0.409797;,0.43344;3.809586;0.409797;,0.43344;4.560537;0.409797;,0.43344;4.560537;-0.284975;,0.43344;3.809586;-0.284975;,0;3.790919;0.428464;,0.43344;3.809586;0.409797;,0.43344;3.809586;-0.284975;,0;3.790919;-0.303642;,0.43344;4.560537;0.409797;,0;4.579204;0.428464;,0;4.579204;-0.303642;,0.43344;4.560537;-0.284975;,0.43344;3.809586;-0.284975;,0.43344;4.560537;-0.284975;,0;4.579204;-0.303642;,0;3.790919;-0.303642;,0;2.440814;0.219926;,-0.466212;2.440814;0.219926;,-0.466212;3.688199;0.219926;,0;3.688199;0.219926;,-0.466212;2.440814;0.219926;,-0.466212;2.440814;-0.219926;,-0.466212;3.688199;-0.219926;,-0.466212;3.688199;0.219926;,0;2.440814;0.219926;,0;2.440814;-0.219926;,-0.466212;2.440814;-0.219926;,-0.466212;2.440814;0.219926;,-0.055633;1.27575;-0.190081;,-0.055633;1.27575;0.190081;,-0.055633;2.35741;0.190081;,-0.055633;2.35741;-0.190081;,-0.055633;1.27575;0.190081;,-0.43017;1.27575;0.190081;,-0.43017;2.35741;0.190081;,-0.055633;2.35741;0.190081;,-0.43017;1.27575;0.190081;,-0.43017;1.27575;-0.190081;,-0.43017;2.35741;-0.190081;,-0.43017;2.35741;0.190081;,-0.43017;1.27575;-0.190081;,-0.055633;1.27575;-0.190081;,-0.055633;2.35741;-0.190081;,-0.43017;2.35741;-0.190081;,-0.055633;1.27575;0.190081;,-0.055633;1.27575;-0.190081;,-0.43017;1.27575;-0.190081;,-0.43017;1.27575;0.190081;,-0.43017;2.35741;0.190081;,-0.43017;2.35741;-0.190081;,-0.055633;2.35741;-0.190081;,-0.055633;2.35741;0.190081;,-0.466212;3.688199;0.219926;,-0.466212;3.688199;-0.219926;,0;3.688199;-0.219926;,0;3.688199;0.219926;,-0.466212;2.440814;-0.219926;,0;2.440814;-0.219926;,0;3.688199;-0.219926;,-0.466212;3.688199;-0.219926;,-0.769341;2.834949;-0.041122;,-0.769341;2.834949;0.207294;,-0.440953;3.555781;0.207294;,-0.440953;3.555781;-0.041122;,-0.769341;2.834949;0.207294;,-0.944661;2.914819;0.207294;,-0.616273;3.635651;0.207294;,-0.440953;3.555781;0.207294;,-0.944661;2.914819;0.207294;,-0.944661;2.914819;-0.041122;,-0.616273;3.635651;-0.041122;,-0.616273;3.635651;0.207294;,-0.944661;2.914819;-0.041122;,-0.769341;2.834949;-0.041122;,-0.440953;3.555781;-0.041122;,-0.616273;3.635651;-0.041122;,-0.769341;2.834949;0.207294;,-0.769341;2.834949;-0.041122;,-0.944661;2.914819;-0.041122;,-0.944661;2.914819;0.207294;,-0.616273;3.635651;0.207294;,-0.616273;3.635651;-0.041122;,-0.440953;3.555781;-0.041122;,-0.440953;3.555781;0.207294;,-1.104504;2.080977;-0.086788;,-1.104504;2.080977;0.161627;,-0.776116;2.801809;0.161627;,-0.776116;2.801809;-0.086788;,-1.104504;2.080977;0.161627;,-1.279824;2.160847;0.161627;,-0.951436;2.881679;0.161627;,-0.776116;2.801809;0.161627;,-1.279824;2.160847;0.161627;,-1.279824;2.160847;-0.086788;,-0.951436;2.881679;-0.086788;,-0.951436;2.881679;0.161627;,-1.279824;2.160847;-0.086788;,-1.104504;2.080977;-0.086788;,-0.776116;2.801809;-0.086788;,-0.951436;2.881679;-0.086788;,-1.104504;2.080977;0.161627;,-1.104504;2.080977;-0.086788;,-1.279824;2.160847;-0.086788;,-1.279824;2.160847;0.161627;,-0.951436;2.881679;0.161627;,-0.951436;2.881679;-0.086788;,-0.776116;2.801809;-0.086788;,-0.776116;2.801809;0.161627;,-0.055633;0.093601;-0.190081;,-0.055633;0.093601;0.190081;,-0.055633;1.205294;0.190081;,-0.055633;1.205294;-0.190081;,-0.055633;0.093601;0.190081;,-0.43017;0.093601;0.190081;,-0.43017;1.205294;0.190081;,-0.055633;1.205294;0.190081;,-0.43017;0.093601;0.190081;,-0.43017;0.093601;-0.190081;,-0.43017;1.205294;-0.190081;,-0.43017;1.205294;0.190081;,-0.43017;0.093601;-0.190081;,-0.055633;0.093601;-0.190081;,-0.055633;1.205294;-0.190081;,-0.43017;1.205294;-0.190081;,-0.055633;0.093601;0.190081;,-0.055633;0.093601;-0.190081;,-0.43017;0.093601;-0.190081;,-0.43017;0.093601;0.190081;,-0.43017;1.205294;0.190081;,-0.43017;1.205294;-0.190081;,-0.055633;1.205294;-0.190081;,-0.055633;1.205294;0.190081;,0;3.790919;0.428464;,-0.43344;3.809586;0.409797;,-0.43344;4.560537;0.409797;,0;4.579204;0.428464;,-0.43344;3.809586;0.409797;,-0.43344;3.809586;-0.284975;,-0.43344;4.560537;-0.284975;,-0.43344;4.560537;0.409797;,0;3.790919;0.428464;,0;3.790919;-0.303642;,-0.43344;3.809586;-0.284975;,-0.43344;3.809586;0.409797;,-0.43344;4.560537;0.409797;,-0.43344;4.560537;-0.284975;,0;4.579204;-0.303642;,0;4.579204;0.428464;,-0.43344;3.809586;-0.284975;,0;3.790919;-0.303642;,0;4.579204;-0.303642;,-0.43344;4.560537;-0.284975;;68;4;3,2,1,0;,4;7,6,5,4;,4;11,10,9,8;,4;15,14,13,12;,4;19,18,17,16;,4;23,22,21,20;,4;27,26,25,24;,4;31,30,29,28;,4;35,34,33,32;,4;39,38,37,36;,4;43,42,41,40;,4;47,46,45,44;,4;51,50,49,48;,4;55,54,53,52;,4;59,58,57,56;,4;63,62,61,60;,4;67,66,65,64;,4;71,70,69,68;,4;75,74,73,72;,4;79,78,77,76;,4;83,82,81,80;,4;87,86,85,84;,4;91,90,89,88;,4;95,94,93,92;,4;99,98,97,96;,4;103,102,101,100;,4;107,106,105,104;,4;111,110,109,108;,4;115,114,113,112;,4;119,118,117,116;,4;123,122,121,120;,4;127,126,125,124;,4;131,130,129,128;,4;135,134,133,132;,4;139,138,137,136;,4;143,142,141,140;,4;147,146,145,144;,4;151,150,149,148;,4;155,154,153,152;,4;159,158,157,156;,4;163,162,161,160;,4;167,166,165,164;,4;171,170,169,168;,4;175,174,173,172;,4;179,178,177,176;,4;183,182,181,180;,4;187,186,185,184;,4;191,190,189,188;,4;195,194,193,192;,4;199,198,197,196;,4;203,202,201,200;,4;207,206,205,204;,4;211,210,209,208;,4;215,214,213,212;,4;219,218,217,216;,4;223,222,221,220;,4;227,226,225,224;,4;231,230,229,228;,4;235,234,233,232;,4;239,238,237,236;,4;243,242,241,240;,4;247,246,245,244;,4;251,250,249,248;,4;255,254,253,252;,4;259,258,257,256;,4;263,262,261,260;,4;267,266,265,264;,4;271,270,269,268;;MeshNormals{272;0;-0.707083;0.707083;,0;0.707083;0.707083;,0.577349;0.577349;0.577349;,0.577349;-0.577349;0.577349;,0.577349;-0.577349;0.577349;,0.577349;0.577349;0.577349;,0.577349;0.577349;-0.577349;,0.577349;-0.577349;-0.577349;,0;-0.707083;0.707083;,0.577349;-0.577349;0.577349;,0.577349;-0.577349;-0.577349;,0;-0.707083;-0.707083;,-0.577349;-0.577349;-0.577349;,-0.577349;0.577349;-0.577349;,-0.577349;0.577349;0.577349;,-0.577349;-0.577349;0.577349;,-0.577349;-0.577349;0.577349;,-0.577349;0.577349;0.577349;,0.577349;0.577349;0.577349;,0.577349;-0.577349;0.577349;,0.577349;-0.577349;0.577349;,0.577349;0.577349;0.577349;,0.577349;0.577349;-0.577349;,0.577349;-0.577349;-0.577349;,0.577349;-0.577349;-0.577349;,0.577349;0.577349;-0.577349;,-0.577349;0.577349;-0.577349;,-0.577349;-0.577349;-0.577349;,-0.577349;-0.577349;0.577349;,0.577349;-0.577349;0.577349;,0.577349;-0.577349;-0.577349;,-0.577349;-0.577349;-0.577349;,0.577349;0.577349;0.577349;,-0.577349;0.577349;0.577349;,-0.577349;0.577349;-0.577349;,0.577349;0.577349;-0.577349;,0.577349;0.577349;0.577349;,0;0.707083;0.707083;,0;0.707083;-0.707083;,0.577349;0.577349;-0.577349;,0.577349;-0.577349;-0.577349;,0.577349;0.577349;-0.577349;,0;0.707083;-0.707083;,0;-0.707083;-0.707083;,-0.286019;-0.764733;-0.577349;,-0.764733;0.286019;-0.577349;,-0.764733;0.286019;0.577349;,-0.286019;-0.764733;0.577349;,-0.286019;-0.764733;0.577349;,-0.764733;0.286019;0.577349;,0.286019;0.764733;0.577349;,0.764733;-0.286019;0.577349;,0.764733;-0.286019;0.577349;,0.286019;0.764733;0.577349;,0.286019;0.764733;-0.577349;,0.764733;-0.286019;-0.577349;,0.764733;-0.286019;-0.577349;,0.286019;0.764733;-0.577349;,-0.764733;0.286019;-0.577349;,-0.286019;-0.764733;-0.577349;,-0.286019;-0.764733;0.577349;,0.764733;-0.286019;0.577349;,0.764733;-0.286019;-0.577349;,-0.286019;-0.764733;-0.577349;,0.286019;0.764733;0.577349;,-0.764733;0.286019;0.577349;,-0.764733;0.286019;-0.577349;,0.286019;0.764733;-0.577349;,-0.286019;-0.764733;-0.577349;,-0.764733;0.286019;-0.577349;,-0.764733;0.286019;0.577349;,-0.286019;-0.764733;0.577349;,-0.286019;-0.764733;0.577349;,-0.764733;0.286019;0.577349;,0.286019;0.764733;0.577349;,0.764733;-0.286019;0.577349;,0.764733;-0.286019;0.577349;,0.286019;0.764733;0.577349;,0.286019;0.764733;-0.577349;,0.764733;-0.286019;-0.577349;,0.764733;-0.286019;-0.577349;,0.286019;0.764733;-0.577349;,-0.764733;0.286019;-0.577349;,-0.286019;-0.764733;-0.577349;,-0.286019;-0.764733;0.577349;,0.764733;-0.286019;0.577349;,0.764733;-0.286019;-0.577349;,-0.286019;-0.764733;-0.577349;,0.286019;0.764733;0.577349;,-0.764733;0.286019;0.577349;,-0.764733;0.286019;-0.577349;,0.286019;0.764733;-0.577349;,-0.577349;-0.577349;-0.577349;,-0.577349;0.577349;-0.577349;,-0.577349;0.577349;0.577349;,-0.577349;-0.577349;0.577349;,-0.577349;-0.577349;0.577349;,-0.577349;0.577349;0.577349;,0.577349;0.577349;0.577349;,0.577349;-0.577349;0.577349;,0.577349;-0.577349;0.577349;,0.577349;0.577349;0.577349;,0.577349;0.577349;-0.577349;,0.577349;-0.577349;-0.577349;,0.577349;-0.577349;-0.577349;,0.577349;0.577349;-0.577349;,-0.577349;0.577349;-0.577349;,-0.577349;-0.577349;-0.577349;,-0.577349;-0.577349;0.577349;,0.577349;-0.577349;0.577349;,0.577349;-0.577349;-0.577349;,-0.577349;-0.577349;-0.577349;,0.577349;0.577349;0.577349;,-0.577349;0.577349;0.577349;,-0.577349;0.577349;-0.577349;,0.577349;0.577349;-0.577349;,0;-0.707083;0.707083;,0;0.707083;0.707083;,0.599902;0.565722;0.565722;,0.599902;-0.565722;0.565722;,0.599902;-0.565722;0.565722;,0.599902;0.565722;0.565722;,0.599902;0.565722;-0.565722;,0.599902;-0.565722;-0.565722;,0;-0.707083;0.707083;,0.599902;-0.565722;0.565722;,0.599902;-0.565722;-0.565722;,0;-0.707083;-0.707083;,0.599902;0.565722;0.565722;,0;0.707083;0.707083;,0;0.707083;-0.707083;,0.599902;0.565722;-0.565722;,0.599902;-0.565722;-0.565722;,0.599902;0.565722;-0.565722;,0;0.707083;-0.707083;,0;-0.707083;-0.707083;,0;-0.707083;0.707083;,-0.577349;-0.577349;0.577349;,-0.577349;0.577349;0.577349;,0;0.707083;0.707083;,-0.577349;-0.577349;0.577349;,-0.577349;-0.577349;-0.577349;,-0.577349;0.577349;-0.577349;,-0.577349;0.577349;0.577349;,0;-0.707083;0.707083;,0;-0.707083;-0.707083;,-0.577349;-0.577349;-0.577349;,-0.577349;-0.577349;0.577349;,0.577349;-0.577349;-0.577349;,0.577349;-0.577349;0.577349;,0.577349;0.577349;0.577349;,0.577349;0.577349;-0.577349;,0.577349;-0.577349;0.577349;,-0.577349;-0.577349;0.577349;,-0.577349;0.577349;0.577349;,0.577349;0.577349;0.577349;,-0.577349;-0.577349;0.577349;,-0.577349;-0.577349;-0.577349;,-0.577349;0.577349;-0.577349;,-0.577349;0.577349;0.577349;,-0.577349;-0.577349;-0.577349;,0.577349;-0.577349;-0.577349;,0.577349;0.577349;-0.577349;,-0.577349;0.577349;-0.577349;,0.577349;-0.577349;0.577349;,0.577349;-0.577349;-0.577349;,-0.577349;-0.577349;-0.577349;,-0.577349;-0.577349;0.577349;,-0.577349;0.577349;0.577349;,-0.577349;0.577349;-0.577349;,0.577349;0.577349;-0.577349;,0.577349;0.577349;0.577349;,-0.577349;0.577349;0.577349;,-0.577349;0.577349;-0.577349;,0;0.707083;-0.707083;,0;0.707083;0.707083;,-0.577349;-0.577349;-0.577349;,0;-0.707083;-0.707083;,0;0.707083;-0.707083;,-0.577349;0.577349;-0.577349;,0.286019;-0.764733;-0.577349;,0.286019;-0.764733;0.577349;,0.764733;0.286019;0.577349;,0.764733;0.286019;-0.577349;,0.286019;-0.764733;0.577349;,-0.764733;-0.286019;0.577349;,-0.286019;0.764733;0.577349;,0.764733;0.286019;0.577349;,-0.764733;-0.286019;0.577349;,-0.764733;-0.286019;-0.577349;,-0.286019;0.764733;-0.577349;,-0.286019;0.764733;0.577349;,-0.764733;-0.286019;-0.577349;,0.286019;-0.764733;-0.577349;,0.764733;0.286019;-0.577349;,-0.286019;0.764733;-0.577349;,0.286019;-0.764733;0.577349;,0.286019;-0.764733;-0.577349;,-0.764733;-0.286019;-0.577349;,-0.764733;-0.286019;0.577349;,-0.286019;0.764733;0.577349;,-0.286019;0.764733;-0.577349;,0.764733;0.286019;-0.577349;,0.764733;0.286019;0.577349;,0.286019;-0.764733;-0.577349;,0.286019;-0.764733;0.577349;,0.764733;0.286019;0.577349;,0.764733;0.286019;-0.577349;,0.286019;-0.764733;0.577349;,-0.764733;-0.286019;0.577349;,-0.286019;0.764733;0.577349;,0.764733;0.286019;0.577349;,-0.764733;-0.286019;0.577349;,-0.764733;-0.286019;-0.577349;,-0.286019;0.764733;-0.577349;,-0.286019;0.764733;0.577349;,-0.764733;-0.286019;-0.577349;,0.286019;-0.764733;-0.577349;,0.764733;0.286019;-0.577349;,-0.286019;0.764733;-0.577349;,0.286019;-0.764733;0.577349;,0.286019;-0.764733;-0.577349;,-0.764733;-0.286019;-0.577349;,-0.764733;-0.286019;0.577349;,-0.286019;0.764733;0.577349;,-0.286019;0.764733;-0.577349;,0.764733;0.286019;-0.577349;,0.764733;0.286019;0.577349;,0.577349;-0.577349;-0.577349;,0.577349;-0.577349;0.577349;,0.577349;0.577349;0.577349;,0.577349;0.577349;-0.577349;,0.577349;-0.577349;0.577349;,-0.577349;-0.577349;0.577349;,-0.577349;0.577349;0.577349;,0.577349;0.577349;0.577349;,-0.577349;-0.577349;0.577349;,-0.577349;-0.577349;-0.577349;,-0.577349;0.577349;-0.577349;,-0.577349;0.577349;0.577349;,-0.577349;-0.577349;-0.577349;,0.577349;-0.577349;-0.577349;,0.577349;0.577349;-0.577349;,-0.577349;0.577349;-0.577349;,0.577349;-0.577349;0.577349;,0.577349;-0.577349;-0.577349;,-0.577349;-0.577349;-0.577349;,-0.577349;-0.577349;0.577349;,-0.577349;0.577349;0.577349;,-0.577349;0.577349;-0.577349;,0.577349;0.577349;-0.577349;,0.577349;0.577349;0.577349;,0;-0.707083;0.707083;,-0.599902;-0.565722;0.565722;,-0.599872;0.565722;0.565722;,0;0.707083;0.707083;,-0.599902;-0.565722;0.565722;,-0.599902;-0.565722;-0.565722;,-0.599872;0.565722;-0.565722;,-0.599872;0.565722;0.565722;,0;-0.707083;0.707083;,0;-0.707083;-0.707083;,-0.599902;-0.565722;-0.565722;,-0.599902;-0.565722;0.565722;,-0.599872;0.565722;0.565722;,-0.599872;0.565722;-0.565722;,0;0.707083;-0.707083;,0;0.707083;0.707083;,-0.599902;-0.565722;-0.565722;,0;-0.707083;-0.707083;,0;0.707083;-0.707083;,-0.599872;0.565722;-0.565722;;68;4;3,2,1,0;,4;7,6,5,4;,4;11,10,9,8;,4;15,14,13,12;,4;19,18,17,16;,4;23,22,21,20;,4;27,26,25,24;,4;31,30,29,28;,4;35,34,33,32;,4;39,38,37,36;,4;43,42,41,40;,4;47,46,45,44;,4;51,50,49,48;,4;55,54,53,52;,4;59,58,57,56;,4;63,62,61,60;,4;67,66,65,64;,4;71,70,69,68;,4;75,74,73,72;,4;79,78,77,76;,4;83,82,81,80;,4;87,86,85,84;,4;91,90,89,88;,4;95,94,93,92;,4;99,98,97,96;,4;103,102,101,100;,4;107,106,105,104;,4;111,110,109,108;,4;115,114,113,112;,4;119,118,117,116;,4;123,122,121,120;,4;127,126,125,124;,4;131,130,129,128;,4;135,134,133,132;,4;139,138,137,136;,4;143,142,141,140;,4;147,146,145,144;,4;151,150,149,148;,4;155,154,153,152;,4;159,158,157,156;,4;163,162,161,160;,4;167,166,165,164;,4;171,170,169,168;,4;175,174,173,172;,4;179,178,177,176;,4;183,182,181,180;,4;187,186,185,184;,4;191,190,189,188;,4;195,194,193,192;,4;199,198,197,196;,4;203,202,201,200;,4;207,206,205,204;,4;211,210,209,208;,4;215,214,213,212;,4;219,218,217,216;,4;223,222,221,220;,4;227,226,225,224;,4;231,230,229,228;,4;235,234,233,232;,4;239,238,237,236;,4;243,242,241,240;,4;247,246,245,244;,4;251,250,249,248;,4;255,254,253,252;,4;259,258,257,256;,4;263,262,261,260;,4;267,266,265,264;,4;271,270,269,268;;}MeshTextureCoords{272;0.849264;0.899246;,0.849264;0.931916;,0.861547;0.931916;,0.861547;0.899246;,0.916988;0.931916;,0.916988;0.899246;,0.9054;0.899246;,0.9054;0.931916;,0.84857;0.844707;,0.84857;0.83254;,0.836981;0.83254;,0.836981;0.844707;,0.927004;0.903587;,0.927004;0.931916;,0.937019;0.931916;,0.937019;0.903587;,0.937019;0.903587;,0.937019;0.931916;,0.946887;0.931916;,0.946887;0.903587;,0.888533;0.856954;,0.888533;0.828625;,0.878517;0.828625;,0.878517;0.856954;,0.939292;0.870917;,0.939292;0.899246;,0.949159;0.899246;,0.949159;0.870917;,0.946887;0.91117;,0.956719;0.91117;,0.956719;0.901213;,0.946887;0.901213;,0.865118;0.813135;,0.855286;0.813135;,0.855286;0.823092;,0.865118;0.823092;,0.866874;0.847426;,0.866874;0.835259;,0.855286;0.835259;,0.855286;0.847426;,0.598002;0.973516;,0.598002;0.206739;,0.309722;0.206739;,0.309722;0.973516;,0.909393;0.822135;,0.909393;0.841014;,0.915938;0.841014;,0.915938;0.822135;,0.951962;0.931916;,0.951962;0.91117;,0.946887;0.91117;,0.946887;0.931916;,0.948762;0.841801;,0.948762;0.822921;,0.942217;0.822921;,0.942217;0.841801;,0.893608;0.838075;,0.893608;0.817329;,0.888533;0.817329;,0.888533;0.838075;,0.900724;0.909292;,0.90515;0.909292;,0.90515;0.902786;,0.900724;0.902786;,0.953585;0.871994;,0.949159;0.871994;,0.949159;0.8785;,0.953585;0.8785;,0.84857;0.837995;,0.84857;0.856874;,0.855114;0.856874;,0.855114;0.837995;,0.902881;0.83746;,0.902881;0.816714;,0.897805;0.816714;,0.897805;0.83746;,0.942217;0.841801;,0.942217;0.822921;,0.935673;0.822921;,0.935673;0.841801;,0.949159;0.8785;,0.949159;0.899246;,0.954235;0.899246;,0.954235;0.8785;,0.919226;0.822135;,0.923651;0.822135;,0.923651;0.815629;,0.919226;0.815629;,0.928077;0.815629;,0.923651;0.815629;,0.923651;0.822135;,0.928077;0.822135;,0.865301;0.847426;,0.865301;0.876542;,0.875317;0.876542;,0.875317;0.847426;,0.909393;0.841014;,0.909393;0.87013;,0.919261;0.87013;,0.919261;0.841014;,0.855286;0.847426;,0.855286;0.876542;,0.865301;0.876542;,0.865301;0.847426;,0.919261;0.841014;,0.919261;0.87013;,0.929128;0.87013;,0.929128;0.841014;,0.878517;0.828625;,0.888349;0.828625;,0.88835;0.818668;,0.878517;0.818668;,0.836981;0.83254;,0.846814;0.83254;,0.846814;0.822583;,0.836981;0.822583;,0.857749;0.887894;,0.836981;0.887894;,0.837473;0.899246;,0.857257;0.899246;,0.855286;0.876542;,0.855286;0.856874;,0.836981;0.856874;,0.836981;0.876542;,0.897805;0.887893;,0.897313;0.876622;,0.879009;0.876622;,0.878517;0.887893;,0.886604;0.909292;,0.886112;0.920645;,0.9054;0.920645;,0.904908;0.909292;,0.977665;0.442421;,0.977665;0.131438;,0.799225;0.123708;,0.799225;0.450151;,0.849264;0.899246;,0.836981;0.899246;,0.836981;0.931916;,0.849264;0.931916;,0.909393;0.866576;,0.897805;0.866576;,0.897805;0.899246;,0.909393;0.899246;,0.84857;0.844707;,0.836981;0.844707;,0.836981;0.856874;,0.84857;0.856874;,0.929276;0.899246;,0.939292;0.899246;,0.939292;0.870917;,0.929276;0.870917;,0.876741;0.819096;,0.866874;0.819096;,0.866874;0.847426;,0.876741;0.847426;,0.939144;0.841801;,0.929128;0.841801;,0.929128;0.87013;,0.939144;0.87013;,0.949011;0.841801;,0.939144;0.841801;,0.939144;0.87013;,0.949011;0.87013;,0.836981;0.812626;,0.836981;0.822583;,0.846814;0.822583;,0.846814;0.812626;,0.909393;0.812178;,0.909393;0.822135;,0.919226;0.822135;,0.919226;0.812178;,0.866874;0.823092;,0.855286;0.823092;,0.855286;0.835259;,0.866874;0.835259;,0.021442;0.973516;,0.309722;0.973516;,0.309722;0.206739;,0.021442;0.206739;,0.916039;0.841014;,0.922583;0.841014;,0.922583;0.822135;,0.916039;0.822135;,0.907956;0.816714;,0.902881;0.816714;,0.902881;0.83746;,0.907956;0.83746;,0.929128;0.822135;,0.922583;0.822135;,0.922583;0.841014;,0.929128;0.841014;,0.853645;0.817249;,0.84857;0.817249;,0.84857;0.837995;,0.853645;0.837995;,0.900724;0.909292;,0.900724;0.902786;,0.895944;0.902786;,0.895944;0.909292;,0.93896;0.816415;,0.93896;0.822921;,0.94374;0.822921;,0.94374;0.816415;,0.935673;0.822921;,0.929128;0.822921;,0.929128;0.841801;,0.935673;0.841801;,0.954087;0.849384;,0.949011;0.849384;,0.949011;0.87013;,0.954087;0.87013;,0.895077;0.838075;,0.888533;0.838075;,0.888533;0.856954;,0.895077;0.856954;,0.948762;0.841801;,0.953838;0.841801;,0.953838;0.821055;,0.948762;0.821055;,0.94374;0.816415;,0.94374;0.822921;,0.94852;0.822921;,0.94852;0.816415;,0.949011;0.842878;,0.949011;0.849384;,0.953791;0.849384;,0.953791;0.842878;,0.919409;0.87013;,0.909393;0.87013;,0.909393;0.899246;,0.919409;0.899246;,0.897805;0.866576;,0.907672;0.866576;,0.907672;0.83746;,0.897805;0.83746;,0.927004;0.9028;,0.916988;0.9028;,0.916988;0.931916;,0.927004;0.931916;,0.929276;0.87013;,0.919409;0.87013;,0.919409;0.899246;,0.929276;0.899246;,0.93896;0.822921;,0.93896;0.812965;,0.929128;0.812965;,0.929128;0.822921;,0.886112;0.899336;,0.886112;0.909292;,0.895944;0.909292;,0.895944;0.899336;,0.857749;0.887894;,0.857257;0.876542;,0.837473;0.876542;,0.836981;0.887894;,0.896821;0.856954;,0.878517;0.856954;,0.878517;0.876622;,0.896821;0.876622;,0.897805;0.887893;,0.878517;0.887893;,0.879009;0.899246;,0.897313;0.899246;,0.886604;0.931916;,0.904908;0.931916;,0.9054;0.920645;,0.886112;0.920645;,0.620785;0.44242;,0.799225;0.450151;,0.799225;0.123708;,0.620785;0.131438;;}XSkinMeshHeader{3;9;10;}SkinWeights{"Armature_arm_l";24;44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,67,66;1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1;-0.047733,0.901521,0.430095,0,-0.097683,0.424309,-0.900233,0,-0.994073,-0.084983,0.06781,0,0.374873,-2.006904,2.980378,1;;}SkinWeights{"Armature_elbow_r";24;216,219,218,213,212,215,214,209,224,208,227,211,226,210,206,221,207,220,204,223,205,222,225,217;1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1;0.102316,0.92166,-0.374266,0,-0.090709,-0.366028,-0.926173,0,-0.990608,0.128712,0.046152,0,0.402018,1.853661,2.350172,1;;}SkinWeights{"Armature_arm_r";24;186,187,184,185,182,183,180,194,195,203,202,192,193,201,200,199,190,198,191,197,188,196,189,181;1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1;-0.047733,0.901521,-0.430095,0,-0.05233,-0.432251,-0.900234,0,-0.997489,-0.020464,0.067809,0,0.160852,2.035269,2.980378,1;;}SkinWeights{"Armature_knee_l";24;105,99,114,106,98,115,107,101,93,108,100,92,109,103,95,110,102,94,111,97,112,104,113,96;1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1;1,0,0,0,0,0.054357,-0.998522,0,0,0.998501,0.054355,0,-0.246294,-0.008592,1.301673,1;;}SkinWeights{"Armature_Bone_007";40;132,133,134,135,124,125,126,252,253,254,255,121,122,264,265,123,267,268,269,270,116,256,258,259,260,261,262,263,271,266,120,119,117,128,129,127,130,118,131,257;1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1;1,0,0,0,0,0,1,0,0,-1,0,0,0,0,-3.793003,1;;}SkinWeights{"Armature_elbow_l";24;88,80,72,91,83,75,90,82,74,70,85,77,71,84,76,68,87,79,69,86,78,89,81,73;1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1;0.102316,0.92166,0.374266,0,-0.008222,0.377011,-0.926173,0,-0.994719,0.091686,0.046152,0,-0.014321,-1.896701,2.350171,1;;}SkinWeights{"Armature_knee_r";24;249,235,250,234,251,229,244,228,245,231,246,230,247,240,241,242,243,237,236,239,238,233,248,232;1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1;1,0,0,0,0,0.054357,-0.998522,0,0,0.998501,0.054355,0,0.246294,-0.008592,1.301673,1;;}SkinWeights{"Armature_leg_l";38;0,3,4,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,40,43,136,145,177,144;0.055873,0.852304,0.852304,0.82998,0.055873,0.852304,0.82998,0.054606,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.82998,0.054606,0.055873,0.054606,0.054606,0.055873;1,0,0,0,0,-0.056452,-0.998405,0,0,0.998385,-0.056452,0,-0.246294,0.135476,2.396023,1;;}SkinWeights{"Armature_leg_r";38;0,170,169,11,168,151,150,149,148,147,146,176,145,177,144,159,158,157,156,155,154,153,167,136,166,137,165,164,163,140,162,141,161,43,160,152,8,171;0.055873,1,1,0.054606,1,1,1,1,1,0.852304,0.82998,0.82998,0.054606,0.054606,0.055873,1,1,1,1,1,1,1,1,0.055873,1,0.852304,1,1,1,0.852304,1,0.82998,1,0.054606,1,1,0.055873,1;1,0,0,0,0,-0.056452,-0.998405,0,0,0.998385,-0.056452,0,0.246294,0.135476,2.396023,1;;}SkinWeights{"Armature_body";40;0,1,2,3,4,5,6,7,8,9,10,11,36,37,38,39,40,41,42,43,136,137,138,139,140,147,141,146,142,145,143,144,179,174,178,173,177,172,176,175;0.888255,1,1,0.147696,0.147696,1,1,0.17002,0.888255,0.147696,0.17002,0.890788,1,1,1,1,0.17002,1,1,0.890788,0.888255,0.147696,1,1,0.147696,0.147696,0.17002,0.17002,1,0.890788,1,0.888255,1,1,1,1,0.890788,1,0.17002,1;1,0,0,0,0,0,1,0,0,-1,0,0,0,0,-2.571201,1;;}}}}} \ No newline at end of file diff --git a/games/devtest/mods/testentities/visuals.lua b/games/devtest/mods/testentities/visuals.lua index dfbf655ea7..26b538e7f6 100644 --- a/games/devtest/mods/testentities/visuals.lua +++ b/games/devtest/mods/testentities/visuals.lua @@ -102,6 +102,19 @@ core.register_entity("testentities:lava_flan", { end, }) +core.register_entity("testentities:cool_guy", { + initial_properties = { + visual = "mesh", + mesh = "testentities_cool_guy.x", + textures = { + "testentities_cool_guy.png" + }, + }, + on_activate = function(self) + self.object:set_animation({x = 0, y = 29}, 30, 0, true) + end, +}) + -- Advanced visual tests -- An entity for testing animated and yaw-modulated sprites diff --git a/irr/include/IAnimatedMesh.h b/irr/include/IAnimatedMesh.h index 62568bf6b3..0ef0971bd8 100644 --- a/irr/include/IAnimatedMesh.h +++ b/irr/include/IAnimatedMesh.h @@ -13,8 +13,8 @@ namespace scene //! Interface for an animated mesh. /** There are already simple implementations of this interface available so you don't have to implement this interface on your own if you need to: -You might want to use irr::scene::SAnimatedMesh, irr::scene::SMesh, -irr::scene::SMeshBuffer etc. */ +You might want to use irr::scene::SMesh, irr::scene::SMeshBuffer etc. +*/ class IAnimatedMesh : public IMesh { public: @@ -34,22 +34,8 @@ public: scene node the mesh is instantiated in.*/ virtual void setAnimationSpeed(f32 fps) = 0; - //! Returns the IMesh interface for a frame. - /** \param frame: Frame number, >= 0, <= getMaxFrameNumber() - Linear interpolation is used if this is between two frames. - \return Returns the animated mesh for the given frame */ - virtual IMesh *getMesh(f32 frame) = 0; - - //! Returns the type of the animated mesh. - /** In most cases it is not necessary to use this method. - This is useful for making a safe downcast. For example, - if getMeshType() returns EAMT_MD2 it's safe to cast the - IAnimatedMesh to IAnimatedMeshMD2. - \returns Type of the mesh. */ - E_ANIMATED_MESH_TYPE getMeshType() const override - { - return EAMT_UNKNOWN; - } + //! Returns the type of the animated mesh. Useful for safe downcasts. + E_ANIMATED_MESH_TYPE getMeshType() const = 0; }; } // end namespace scene diff --git a/irr/include/IAnimatedMeshSceneNode.h b/irr/include/IAnimatedMeshSceneNode.h index 8f9f6d661f..2df8da9174 100644 --- a/irr/include/IAnimatedMeshSceneNode.h +++ b/irr/include/IAnimatedMeshSceneNode.h @@ -12,35 +12,8 @@ namespace irr { namespace scene { -enum E_JOINT_UPDATE_ON_RENDER -{ - //! do nothing - EJUOR_NONE = 0, - - //! get joints positions from the mesh (for attached nodes, etc) - EJUOR_READ, - - //! control joint positions in the mesh (eg. ragdolls, or set the animation from animateJoints() ) - EJUOR_CONTROL -}; - class IAnimatedMeshSceneNode; -//! Callback interface for catching events of ended animations. -/** Implement this interface and use -IAnimatedMeshSceneNode::setAnimationEndCallback to be able to -be notified if an animation playback has ended. -**/ -class IAnimationEndCallBack : public virtual IReferenceCounted -{ -public: - //! Will be called when the animation playback has ended. - /** See IAnimatedMeshSceneNode::setAnimationEndCallback for - more information. - \param node: Node of which the animation has ended. */ - virtual void OnAnimationEnd(IAnimatedMeshSceneNode *node) = 0; -}; - //! Scene node capable of displaying an animated mesh. class IAnimatedMeshSceneNode : public ISceneNode { @@ -120,11 +93,10 @@ public: /** When true the animations are played looped */ virtual bool getLoopMode() const = 0; - //! Sets a callback interface which will be called if an animation playback has ended. - /** Set this to 0 to disable the callback again. - Please note that this will only be called when in non looped - mode, see IAnimatedMeshSceneNode::setLoopMode(). */ - virtual void setAnimationEndCallback(IAnimationEndCallBack *callback = 0) = 0; + //! Will be called right after the joints have been animated, + //! but before the transforms have been propagated recursively to children. + virtual void setOnAnimateCallback( + const std::function &cb) = 0; //! Sets if the scene node should not copy the materials of the mesh but use them in a read only style. /** In this way it is possible to change the materials a mesh @@ -139,20 +111,15 @@ public: virtual void setMesh(IAnimatedMesh *mesh) = 0; //! Returns the current mesh - virtual IAnimatedMesh *getMesh(void) = 0; - - //! Set how the joints should be updated on render - virtual void setJointMode(E_JOINT_UPDATE_ON_RENDER mode) = 0; + virtual IAnimatedMesh *getMesh() = 0; //! Sets the transition time in seconds - /** Note: This needs to enable joints, and setJointmode set to - EJUOR_CONTROL. You must call animateJoints(), or the mesh will - not animate. */ + /** Note: You must call animateJoints(), or the mesh will not animate. */ virtual void setTransitionTime(f32 Time) = 0; //! animates the joints in the mesh based on the current frame. /** Also takes in to account transitions. */ - virtual void animateJoints(bool CalculateAbsolutePositions = true) = 0; + virtual void animateJoints() = 0; //! render mesh ignoring its transformation. /** Culling is unaffected. */ diff --git a/irr/include/IBoneSceneNode.h b/irr/include/IBoneSceneNode.h index eef55f6e03..d169b8d30b 100644 --- a/irr/include/IBoneSceneNode.h +++ b/irr/include/IBoneSceneNode.h @@ -11,85 +11,41 @@ namespace irr namespace scene { -//! Enumeration for different bone animation modes -enum E_BONE_ANIMATION_MODE -{ - //! The bone is usually animated, unless it's parent is not animated - EBAM_AUTOMATIC = 0, - - //! The bone is animated by the skin, if it's parent is not animated then animation will resume from this bone onward - EBAM_ANIMATED, - - //! The bone is not animated by the skin - EBAM_UNANIMATED, - - //! Not an animation mode, just here to count the available modes - EBAM_COUNT - -}; - -enum E_BONE_SKINNING_SPACE -{ - //! local skinning, standard - EBSS_LOCAL = 0, - - //! global skinning - EBSS_GLOBAL, - - EBSS_COUNT -}; - -//! Names for bone animation modes -const c8 *const BoneAnimationModeNames[] = { - "automatic", - "animated", - "unanimated", - 0, - }; - //! Interface for bones used for skeletal animation. /** Used with SkinnedMesh and IAnimatedMeshSceneNode. */ class IBoneSceneNode : public ISceneNode { public: - IBoneSceneNode(ISceneNode *parent, ISceneManager *mgr, s32 id = -1) : - ISceneNode(parent, mgr, id), positionHint(-1), scaleHint(-1), rotationHint(-1) {} + IBoneSceneNode(ISceneNode *parent, ISceneManager *mgr, + s32 id = -1, u32 boneIndex = 0, + const std::optional &boneName = std::nullopt) + : + ISceneNode(parent, mgr, id), + BoneIndex(boneIndex) + { + setName(boneName); + } - //! Get the index of the bone - virtual u32 getBoneIndex() const = 0; + //! Returns the index of the bone + u32 getBoneIndex() const + { + return BoneIndex; + } - //! Sets the animation mode of the bone. - /** \return True if successful. (Unused) */ - virtual bool setAnimationMode(E_BONE_ANIMATION_MODE mode) = 0; + //! returns the axis aligned bounding box of this node + const core::aabbox3d &getBoundingBox() const override + { + return Box; + } - //! Gets the current animation mode of the bone - virtual E_BONE_ANIMATION_MODE getAnimationMode() const = 0; + const u32 BoneIndex; - //! Get the axis aligned bounding box of this node - const core::aabbox3d &getBoundingBox() const override = 0; - - //! Returns the relative transformation of the scene node. - // virtual core::matrix4 getRelativeTransformation() const = 0; - - //! The animation method. - void OnAnimate(u32 timeMs) override = 0; + // Bogus box; bone scene nodes are not rendered anyways. + static constexpr core::aabbox3d Box = {{0, 0, 0}}; //! The render method. /** Does nothing as bones are not visible. */ void render() override {} - - //! How the relative transformation of the bone is used - virtual void setSkinningSpace(E_BONE_SKINNING_SPACE space) = 0; - - //! How the relative transformation of the bone is used - virtual E_BONE_SKINNING_SPACE getSkinningSpace() const = 0; - - //! Updates the absolute position based on the relative and the parents position - virtual void updateAbsolutePositionOfAllChildren() = 0; - - s32 positionHint; - s32 scaleHint; - s32 rotationHint; }; } // end namespace scene diff --git a/irr/include/IMesh.h b/irr/include/IMesh.h index 8ee180d5d3..e8656e8682 100644 --- a/irr/include/IMesh.h +++ b/irr/include/IMesh.h @@ -20,38 +20,6 @@ enum E_ANIMATED_MESH_TYPE //! Unknown animated mesh type. EAMT_UNKNOWN = 0, - //! Quake 2 MD2 model file - EAMT_MD2, - - //! Quake 3 MD3 model file - EAMT_MD3, - - //! Maya .obj static model - EAMT_OBJ, - - //! Quake 3 .bsp static Map - EAMT_BSP, - - //! 3D Studio .3ds file - EAMT_3DS, - - //! My3D Mesh, the file format by Zhuck Dimitry - EAMT_MY3D, - - //! Pulsar LMTools .lmts file. This Irrlicht loader was written by Jonas Petersen - EAMT_LMTS, - - //! Cartography Shop .csm file. This loader was created by Saurav Mohapatra. - EAMT_CSM, - - //! .oct file for Paul Nette's FSRad or from Murphy McCauley's Blender .oct exporter. - /** The oct file format contains 3D geometry and lightmaps and - can be loaded directly by Irrlicht */ - EAMT_OCT, - - //! Halflife MDL model file - EAMT_MDL_HALFLIFE, - //! generic skinned mesh EAMT_SKINNED, @@ -119,9 +87,7 @@ public: virtual void setDirty(E_BUFFER_TYPE buffer = EBT_VERTEX_AND_INDEX) = 0; //! Returns the type of the meshes. - /** This is useful for making a safe downcast. For example, - if getMeshType() returns EAMT_MD2 it's safe to cast the - IMesh to IAnimatedMeshMD2. + /** This is useful for making a safe downcast. Note: It's no longer just about animated meshes, that name has just historical reasons. \returns Type of the mesh */ virtual E_ANIMATED_MESH_TYPE getMeshType() const diff --git a/irr/include/IMeshManipulator.h b/irr/include/IMeshManipulator.h index c9d989cae8..0312a38c86 100644 --- a/irr/include/IMeshManipulator.h +++ b/irr/include/IMeshManipulator.h @@ -66,26 +66,6 @@ public: IReferenceCounted::drop() for more information. */ virtual SMesh *createMeshCopy(IMesh *mesh) const = 0; - //! Get amount of polygons in mesh. - /** \param mesh Input mesh - \return Number of polygons in mesh. */ - virtual s32 getPolyCount(IMesh *mesh) const = 0; - - //! Get amount of polygons in mesh. - /** \param mesh Input mesh - \return Number of polygons in mesh. */ - virtual s32 getPolyCount(IAnimatedMesh *mesh) const = 0; - - //! Create a new AnimatedMesh and adds the mesh to it - /** \param mesh Input mesh - \param type The type of the animated mesh to create. - \return Newly created animated mesh with mesh as its only - content. When you don't need the animated mesh anymore, you - should call IAnimatedMesh::drop(). See - IReferenceCounted::drop() for more information. */ - virtual IAnimatedMesh *createAnimatedMesh(IMesh *mesh, - scene::E_ANIMATED_MESH_TYPE type = scene::EAMT_UNKNOWN) const = 0; - //! Apply a manipulator on the Meshbuffer /** \param func A functor defining the mesh manipulation. \param buffer The Meshbuffer to apply the manipulator to. diff --git a/irr/include/IMeshSceneNode.h b/irr/include/IMeshSceneNode.h index 1fb0054054..3065f4c07d 100644 --- a/irr/include/IMeshSceneNode.h +++ b/irr/include/IMeshSceneNode.h @@ -32,7 +32,7 @@ public: //! Get the currently defined mesh for display. /** \return Pointer to mesh which is displayed by this node. */ - virtual IMesh *getMesh(void) = 0; + virtual IMesh *getMesh() = 0; //! Sets if the scene node should not copy the materials of the mesh but use them directly. /** In this way it is possible to change the materials of a mesh diff --git a/irr/include/ISceneNode.h b/irr/include/ISceneNode.h index f91fd64997..5737158617 100644 --- a/irr/include/ISceneNode.h +++ b/irr/include/ISceneNode.h @@ -94,16 +94,12 @@ public: \param timeMs Current time in milliseconds. */ virtual void OnAnimate(u32 timeMs) { - if (IsVisible) { - // update absolute position - updateAbsolutePosition(); + if (!IsVisible && Children.empty()) + return; - // perform the post render process on all children - - ISceneNodeList::iterator it = Children.begin(); - for (; it != Children.end(); ++it) - (*it)->OnAnimate(timeMs); - } + updateAbsolutePosition(); + for (auto *child : Children) + child->OnAnimate(timeMs); } //! Renders the node. diff --git a/irr/include/SAnimatedMesh.h b/irr/include/SAnimatedMesh.h deleted file mode 100644 index dcc65410f7..0000000000 --- a/irr/include/SAnimatedMesh.h +++ /dev/null @@ -1,167 +0,0 @@ -// 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 "IAnimatedMesh.h" -#include "IMesh.h" -#include "aabbox3d.h" - -namespace irr -{ -namespace scene -{ - -//! Simple implementation of the IAnimatedMesh interface. -struct SAnimatedMesh final : public IAnimatedMesh -{ - //! constructor - SAnimatedMesh(scene::IMesh *mesh = 0, scene::E_ANIMATED_MESH_TYPE type = scene::EAMT_UNKNOWN) : - IAnimatedMesh(), FramesPerSecond(25.f), Type(type) - { - addMesh(mesh); - recalculateBoundingBox(); - } - - //! destructor - virtual ~SAnimatedMesh() - { - // drop meshes - for (auto *mesh : Meshes) - mesh->drop(); - } - - f32 getMaxFrameNumber() const override - { - return static_cast(Meshes.size() - 1); - } - - //! Gets the default animation speed of the animated mesh. - /** \return Amount of frames per second. If the amount is 0, it is a static, non animated mesh. */ - f32 getAnimationSpeed() const override - { - return FramesPerSecond; - } - - //! Gets the frame count of the animated mesh. - /** \param fps Frames per second to play the animation with. If the amount is 0, it is not animated. - The actual speed is set in the scene node the mesh is instantiated in.*/ - void setAnimationSpeed(f32 fps) override - { - FramesPerSecond = fps; - } - - //! Returns the IMesh interface for a frame. - /** \param frame: Frame number as zero based index. - \return The animated mesh based for the given frame */ - IMesh *getMesh(f32 frame) override - { - if (Meshes.empty()) - return nullptr; - - return Meshes[static_cast(frame)]; - } - - //! adds a Mesh - void addMesh(IMesh *mesh) - { - if (mesh) { - mesh->grab(); - Meshes.push_back(mesh); - } - } - - //! Returns an axis aligned bounding box of the mesh. - /** \return A bounding box of this mesh is returned. */ - const core::aabbox3d &getBoundingBox() const override - { - return Box; - } - - //! set user axis aligned bounding box - void setBoundingBox(const core::aabbox3df &box) override - { - Box = box; - } - - //! Recalculates the bounding box. - void recalculateBoundingBox() - { - Box.reset(0, 0, 0); - - if (Meshes.empty()) - return; - - Box = Meshes[0]->getBoundingBox(); - - for (u32 i = 1; i < Meshes.size(); ++i) - Box.addInternalBox(Meshes[i]->getBoundingBox()); - } - - //! Returns the type of the animated mesh. - E_ANIMATED_MESH_TYPE getMeshType() const override - { - return Type; - } - - //! returns amount of mesh buffers. - u32 getMeshBufferCount() const override - { - if (Meshes.empty()) - return 0; - - return Meshes[0]->getMeshBufferCount(); - } - - //! returns pointer to a mesh buffer - IMeshBuffer *getMeshBuffer(u32 nr) const override - { - if (Meshes.empty()) - return 0; - - return Meshes[0]->getMeshBuffer(nr); - } - - //! Returns pointer to a mesh buffer which fits a material - /** \param material: material to search for - \return Returns the pointer to the mesh buffer or - NULL if there is no such mesh buffer. */ - IMeshBuffer *getMeshBuffer(const video::SMaterial &material) const override - { - if (Meshes.empty()) - return 0; - - return Meshes[0]->getMeshBuffer(material); - } - - //! set the hardware mapping hint, for driver - void setHardwareMappingHint(E_HARDWARE_MAPPING newMappingHint, E_BUFFER_TYPE buffer = EBT_VERTEX_AND_INDEX) override - { - for (u32 i = 0; i < Meshes.size(); ++i) - Meshes[i]->setHardwareMappingHint(newMappingHint, buffer); - } - - //! flags the meshbuffer as changed, reloads hardware buffers - void setDirty(E_BUFFER_TYPE buffer = EBT_VERTEX_AND_INDEX) override - { - for (u32 i = 0; i < Meshes.size(); ++i) - Meshes[i]->setDirty(buffer); - } - - //! All meshes defining the animated mesh - std::vector Meshes; - - //! The bounding box of this mesh - core::aabbox3d Box{{0.0f, 0.0f, 0.0f}}; - - //! Default animation speed of this mesh. - f32 FramesPerSecond; - - //! The type of the mesh. - E_ANIMATED_MESH_TYPE Type; -}; - -} // end namespace scene -} // end namespace irr diff --git a/irr/include/SMesh.h b/irr/include/SMesh.h index 5e76fafc51..b22bb77494 100644 --- a/irr/include/SMesh.h +++ b/irr/include/SMesh.h @@ -5,7 +5,7 @@ #pragma once #include -#include "IMesh.h" +#include "IAnimatedMesh.h" #include "IMeshBuffer.h" #include "aabbox3d.h" @@ -14,7 +14,7 @@ namespace irr namespace scene { //! Simple implementation of the IMesh interface. -struct SMesh final : public IMesh +struct SMesh final : public IAnimatedMesh { //! constructor SMesh() {} @@ -134,6 +134,15 @@ struct SMesh final : public IMesh //! The bounding box of this mesh core::aabbox3d BoundingBox{{0, 0, 0}}; + + // Implement animated mesh interface as a static mesh. + // Slightly hacky: Eventually should be consolidated with SSkinnedMesh, + // with all the animation-related parts behind an optional. + + virtual f32 getMaxFrameNumber() const override { return 0.0f; } + virtual f32 getAnimationSpeed() const override { return 0.0f; } + virtual void setAnimationSpeed(f32 fps) override {} + E_ANIMATED_MESH_TYPE getMeshType() const override { return EAMT_STATIC; } }; } // end namespace scene diff --git a/irr/include/SkinnedMesh.h b/irr/include/SkinnedMesh.h index a527db76e5..aa718c882b 100644 --- a/irr/include/SkinnedMesh.h +++ b/irr/include/SkinnedMesh.h @@ -8,11 +8,18 @@ #include "ISceneManager.h" #include "SMeshBuffer.h" #include "SSkinMeshBuffer.h" +#include "aabbox3d.h" +#include "irrMath.h" +#include "irrTypes.h" +#include "matrix4.h" #include "quaternion.h" #include "vector3d.h" +#include "Transform.h" #include #include +#include +#include namespace irr { @@ -37,9 +44,8 @@ public: //! constructor SkinnedMesh(SourceFormat src_format) : EndFrame(0.f), FramesPerSecond(25.f), - LastAnimatedFrame(-1), SkinnedLastFrame(false), HasAnimation(false), PreparedForSkinning(false), - AnimateNormals(true), HardwareSkinning(false), + AnimateNormals(true), SrcFormat(src_format) { SkinningBuffers = &LocalBuffers; @@ -64,14 +70,12 @@ public: The actual speed is set in the scene node the mesh is instantiated in.*/ void setAnimationSpeed(f32 fps) override; - //! returns the animated mesh for the given frame - IMesh *getMesh(f32) override; + //! Turns the given array of local matrices into an array of global matrices + //! by multiplying with respective parent matrices. + void calculateGlobalMatrices(std::vector &matrices) const; - //! Animates joints based on frame input - void animateMesh(f32 frame); - - //! Performs a software skin on this mesh based of joint positions - void skinMesh(); + //! Performs a software skin on this mesh based on the given joint matrices + void skinMesh(const std::vector &animated_transforms); //! returns amount of mesh buffers. u32 getMeshBufferCount() const override; @@ -89,14 +93,15 @@ public: void setTextureSlot(u32 meshbufNr, u32 textureSlot); - //! returns an axis aligned bounding box + //! Returns bounding box of the mesh *in static pose*. const core::aabbox3d &getBoundingBox() const override { - return BoundingBox; + // TODO ideally we shouldn't be forced to implement this + return StaticPoseBox; } - //! set user axis aligned bounding box + //! Set bounding box of the mesh *in static pose*. void setBoundingBox(const core::aabbox3df &box) override { - BoundingBox = box; + StaticPoseBox = box; } //! set the hardware mapping hint, for driver @@ -140,28 +145,15 @@ public: return !HasAnimation; } - //! Allows to enable hardware skinning. - /* This feature is not implemented in Irrlicht yet */ - bool setHardwareSkinning(bool on); - //! Refreshes vertex data cached in joints such as positions and normals void refreshJointCache(); //! Moves the mesh into static position. void resetAnimation(); - void updateBoundingBox(); - - //! Recovers the joints from the mesh - void recoverJointsFromMesh(std::vector &jointChildSceneNodes); - - //! Transfers the joint data to the mesh - void transferJointsToMesh(const std::vector &jointChildSceneNodes); - //! Creates an array of joints from this mesh as children of node - void addJoints(std::vector &jointChildSceneNodes, - IAnimatedMeshSceneNode *node, - ISceneManager *smgr); + std::vector addJoints( + IAnimatedMeshSceneNode *node, ISceneManager *smgr); //! A vertex weight struct SWeight @@ -236,7 +228,7 @@ public: static core::quaternion interpolateValue(core::quaternion from, core::quaternion to, f32 time) { core::quaternion result; - result.slerp(from, to, time, 0.001f); + result.slerp(from, to, time); return result; } @@ -288,15 +280,14 @@ public: }); } - void updateTransform(f32 frame, - core::vector3df &t, core::quaternion &r, core::vector3df &s) const + void updateTransform(f32 frame, core::Transform &transform) const { if (auto pos = position.get(frame)) - t = *pos; + transform.translation = *pos; if (auto rot = rotation.get(frame)) - r = *rot; + transform.rotation = *rot; if (auto scl = scale.get(frame)) - s = *scl; + transform.scale = *scl; } void cleanup() { @@ -309,16 +300,34 @@ public: //! Joints struct SJoint { - SJoint() : GlobalSkinningSpace(false) {} + SJoint() {} //! The name of this joint std::optional Name; - //! Local matrix of this joint - core::matrix4 LocalMatrix; + //! Local transformation to be set by loaders. Mutated by animation. + using VariantTransform = std::variant; + VariantTransform transform{core::Transform{}}; + + VariantTransform animate(f32 frame) const { + if (keys.empty()) + return transform; + + if (std::holds_alternative(transform)) { + // .x lets animations override matrix transforms entirely, + // which is what we implement here. + // .gltf does not allow animation of nodes using matrix transforms. + // Note that a decomposition into a TRS transform need not exist! + core::Transform trs; + keys.updateTransform(frame, trs); + return {trs}; + } + + auto trs = std::get(transform); + keys.updateTransform(frame, trs); + return {trs}; + } - //! List of child joints - std::vector Children; //! List of attached meshes std::vector AttachedMeshes; @@ -329,42 +338,49 @@ public: //! Skin weights std::vector Weights; + //! Bounding box of all affected vertices, in local space + core::aabbox3df LocalBoundingBox{{0, 0, 0}}; + //! Unnecessary for loaders, will be overwritten on finalize core::matrix4 GlobalMatrix; // loaders may still choose to set this (temporarily) to calculate absolute vertex data. - core::matrix4 GlobalAnimatedMatrix; - core::matrix4 LocalAnimatedMatrix; - - //! These should be set by loaders. - core::vector3df Animatedposition; - core::vector3df Animatedscale; - core::quaternion Animatedrotation; // The .x and .gltf formats pre-calculate this std::optional GlobalInversedMatrix; - private: - //! Internal members used by SkinnedMesh - friend class SkinnedMesh; - bool GlobalSkinningSpace; + void setParent(SJoint *parent) { + ParentJointID = parent ? parent->JointID : std::optional{}; + } + + u16 JointID; // TODO refactor away: pointers -> IDs (problem: .x loader abuses SJoint) + std::optional ParentJointID; }; + //! Animates joints based on frame input + std::vector animateMesh(f32 frame); + + //! Calculates a bounding box given an animation in the form of global joint transforms. + core::aabbox3df calculateBoundingBox( + const std::vector &global_transforms); + + void recalculateBaseBoundingBoxes(); + const std::vector &getAllJoints() const { return AllJoints; } protected: - void checkForAnimation(); + bool checkForAnimation() const; + + void topoSortJoints(); + + void prepareForSkinning(); + + void calculateStaticBoundingBox(); + void calculateJointBoundingBoxes(); + void calculateBufferBoundingBoxes(); void normalizeWeights(); - void buildAllLocalAnimatedMatrices(); - - void buildAllGlobalAnimatedMatrices(SJoint *Joint = 0, SJoint *ParentJoint = 0); - - void calculateGlobalMatrices(SJoint *Joint, SJoint *ParentJoint); - - void skinJoint(SJoint *Joint, SJoint *ParentJoint); - void calculateTangents(core::vector3df &normal, core::vector3df &tangent, core::vector3df &binormal, const core::vector3df &vt1, const core::vector3df &vt2, const core::vector3df &vt3, @@ -376,25 +392,25 @@ protected: //! Mapping from meshbuffer number to bindable texture slot std::vector TextureSlots; + //! Joints, topologically sorted (parents come before their children). std::vector AllJoints; - std::vector RootJoints; // bool can't be used here because std::vector // doesn't allow taking a reference to individual elements. std::vector> Vertices_Moved; - core::aabbox3d BoundingBox{{0, 0, 0}}; + //! Bounding box of just the static parts of the mesh + core::aabbox3df StaticPartsBox{{0, 0, 0}}; + + //! Bounding box of the mesh in static pose + core::aabbox3df StaticPoseBox{{0, 0, 0}}; f32 EndFrame; f32 FramesPerSecond; - f32 LastAnimatedFrame; - bool SkinnedLastFrame; - bool HasAnimation; bool PreparedForSkinning; bool AnimateNormals; - bool HardwareSkinning; SourceFormat SrcFormat; }; diff --git a/irr/include/Transform.h b/irr/include/Transform.h new file mode 100644 index 0000000000..1e96e183d2 --- /dev/null +++ b/irr/include/Transform.h @@ -0,0 +1,42 @@ +#pragma once + +#include "irrMath.h" +#include +#include +#include + +namespace irr +{ +namespace core +{ + +struct Transform { + vector3df translation; + quaternion rotation; + vector3df scale{1}; + + Transform interpolate(Transform to, f32 time) const + { + core::quaternion interpolated_rotation; + interpolated_rotation.slerp(rotation, to.rotation, time); + return { + to.translation.getInterpolated(translation, time), + interpolated_rotation, + to.scale.getInterpolated(scale, time), + }; + } + + matrix4 buildMatrix() const + { + matrix4 T; + T.setTranslation(translation); + matrix4 R; + rotation.getMatrix_transposed(R); + matrix4 S; + S.setScale(scale); + return T * R * S; + } +}; + +} // end namespace core +} // end namespace irr diff --git a/irr/include/quaternion.h b/irr/include/quaternion.h index e23b1317d0..42e0428a90 100644 --- a/irr/include/quaternion.h +++ b/irr/include/quaternion.h @@ -180,7 +180,7 @@ public: linear interpolation. */ quaternion &slerp(quaternion q1, quaternion q2, - f32 time, f32 threshold = .05f); + f32 time, f32 threshold = .001f); //! Set this quaternion to represent a rotation from angle and axis. /** Axis must be unit length. diff --git a/irr/src/CAnimatedMeshSceneNode.cpp b/irr/src/CAnimatedMeshSceneNode.cpp index 09d83038b7..7b1d9053a9 100644 --- a/irr/src/CAnimatedMeshSceneNode.cpp +++ b/irr/src/CAnimatedMeshSceneNode.cpp @@ -3,9 +3,13 @@ // For conditions of distribution and use, see copyright notice in irrlicht.h #include "CAnimatedMeshSceneNode.h" +#include "CBoneSceneNode.h" #include "IVideoDriver.h" #include "ISceneManager.h" #include "S3DVertex.h" +#include "Transform.h" +#include "irrTypes.h" +#include "matrix4.h" #include "os.h" #include "SkinnedMesh.h" #include "IDummyTransformationSceneNode.h" @@ -17,6 +21,9 @@ #include "IFileSystem.h" #include "quaternion.h" #include +#include +#include +#include namespace irr { @@ -30,13 +37,13 @@ CAnimatedMeshSceneNode::CAnimatedMeshSceneNode(IAnimatedMesh *mesh, const core::vector3df &rotation, const core::vector3df &scale) : IAnimatedMeshSceneNode(parent, mgr, id, position, rotation, scale), - Mesh(0), + Mesh(nullptr), StartFrame(0), EndFrame(0), FramesPerSecond(0.025f), CurrentFrameNr(0.f), LastTimeMs(0), TransitionTime(0), Transiting(0.f), TransitingBlend(0.f), - JointMode(EJUOR_NONE), JointsUsed(false), + JointsUsed(false), Looping(true), ReadOnlyMaterials(false), RenderFromIdentity(false), - LoopCallBack(0), PassCount(0) + PassCount(0) { setMesh(mesh); } @@ -44,8 +51,6 @@ CAnimatedMeshSceneNode::CAnimatedMeshSceneNode(IAnimatedMesh *mesh, //! destructor CAnimatedMeshSceneNode::~CAnimatedMeshSceneNode() { - if (LoopCallBack) - LoopCallBack->drop(); if (Mesh) Mesh->drop(); } @@ -87,8 +92,7 @@ void CAnimatedMeshSceneNode::buildFrameNr(u32 timeMs) if (FramesPerSecond > 0.f) { // forwards... if (CurrentFrameNr > EndFrame) CurrentFrameNr = StartFrame + fmodf(CurrentFrameNr - StartFrame, EndFrame - StartFrame); - } else // backwards... - { + } else { // backwards... if (CurrentFrameNr < StartFrame) CurrentFrameNr = EndFrame - fmodf(EndFrame - CurrentFrameNr, EndFrame - StartFrame); } @@ -97,18 +101,9 @@ void CAnimatedMeshSceneNode::buildFrameNr(u32 timeMs) CurrentFrameNr += timeMs * FramesPerSecond; if (FramesPerSecond > 0.f) { // forwards... - if (CurrentFrameNr > EndFrame) { - CurrentFrameNr = EndFrame; - if (LoopCallBack) - LoopCallBack->OnAnimationEnd(this); - } - } else // backwards... - { - if (CurrentFrameNr < StartFrame) { - CurrentFrameNr = StartFrame; - if (LoopCallBack) - LoopCallBack->OnAnimationEnd(this); - } + CurrentFrameNr = std::min(CurrentFrameNr, EndFrame); + } else { // backwards... + CurrentFrameNr = std::max(CurrentFrameNr, StartFrame); } } } @@ -156,38 +151,18 @@ void CAnimatedMeshSceneNode::OnRegisterSceneNode() IMesh *CAnimatedMeshSceneNode::getMeshForCurrentFrame() { if (Mesh->getMeshType() != EAMT_SKINNED) { - return Mesh->getMesh(getFrameNr()); - } else { - // As multiple scene nodes may be sharing the same skinned mesh, we have to - // re-animate it every frame to ensure that this node gets the mesh that it needs. - - SkinnedMesh *skinnedMesh = static_cast(Mesh); - - if (JointMode == EJUOR_CONTROL) // write to mesh - skinnedMesh->transferJointsToMesh(JointChildSceneNodes); - else - skinnedMesh->animateMesh(getFrameNr()); - - // Update the skinned mesh for the current joint transforms. - skinnedMesh->skinMesh(); - - if (JointMode == EJUOR_READ) { // read from mesh - skinnedMesh->recoverJointsFromMesh(JointChildSceneNodes); - - //---slow--- - for (u32 n = 0; n < JointChildSceneNodes.size(); ++n) - if (JointChildSceneNodes[n]->getParent() == this) { - JointChildSceneNodes[n]->updateAbsolutePositionOfAllChildren(); // temp, should be an option - } - } - - if (JointMode == EJUOR_CONTROL) { - // For meshes other than EJUOR_CONTROL, this is done by calling animateMesh() - skinnedMesh->updateBoundingBox(); - } - - return skinnedMesh; + return Mesh; } + + // As multiple scene nodes may be sharing the same skinned mesh, we have to + // re-animate it every frame to ensure that this node gets the mesh that it needs. + + auto *skinnedMesh = static_cast(Mesh); + + // Matrices have already been calculated in OnAnimate + skinnedMesh->skinMesh(PerJoint.GlobalMatrices); + + return skinnedMesh; } //! OnAnimate() is called just before rendering the whole scene. @@ -201,7 +176,28 @@ void CAnimatedMeshSceneNode::OnAnimate(u32 timeMs) buildFrameNr(timeMs - LastTimeMs); LastTimeMs = timeMs; + // This needs to be done on animate, which is called recursively *before* + // anything is rendered so that the transformations of children are up to date + animateJoints(); + + // Copy old transforms *before* bone overrides have been applied. + // TODO if there are no bone overrides or no animation blending, this is unnecessary. + copyOldTransforms(); + + if (OnAnimateCallback) + OnAnimateCallback(timeMs / 1000.0f); + IAnimatedMeshSceneNode::OnAnimate(timeMs); + + if (auto *skinnedMesh = dynamic_cast(Mesh)) { + for (u16 i = 0; i < PerJoint.SceneNodes.size(); ++i) + PerJoint.GlobalMatrices[i] = PerJoint.SceneNodes[i]->getRelativeTransformation(); + assert(PerJoint.GlobalMatrices.size() == skinnedMesh->getJointCount()); + skinnedMesh->calculateGlobalMatrices(PerJoint.GlobalMatrices); + Box = skinnedMesh->calculateBoundingBox(PerJoint.GlobalMatrices); + } else { + Box = Mesh->getBoundingBox(); + } } //! renders the node. @@ -218,15 +214,7 @@ void CAnimatedMeshSceneNode::render() ++PassCount; scene::IMesh *m = getMeshForCurrentFrame(); - - if (m) { - Box = m->getBoundingBox(); - } else { -#ifdef _DEBUG - os::Printer::log("Animated Mesh returned no mesh to render.", ELL_WARNING); -#endif - return; - } + assert(m); driver->setTransform(video::ETS_WORLD, AbsoluteTransformation); @@ -294,11 +282,12 @@ void CAnimatedMeshSceneNode::render() if (DebugDataVisible & scene::EDS_SKELETON) { if (Mesh->getMeshType() == EAMT_SKINNED) { // draw skeleton - - for (auto *joint : ((SkinnedMesh *)Mesh)->getAllJoints()) { - for (const auto *childJoint : joint->Children) { - driver->draw3DLine(joint->GlobalAnimatedMatrix.getTranslation(), - childJoint->GlobalAnimatedMatrix.getTranslation(), + const auto &joints = (static_cast(Mesh))->getAllJoints(); + for (u16 i = 0; i < PerJoint.GlobalMatrices.size(); ++i) { + const auto translation = PerJoint.GlobalMatrices[i].getTranslation(); + if (auto pjid = joints[i]->ParentJointID) { + const auto parent_translation = PerJoint.GlobalMatrices[*pjid].getTranslation(); + driver->draw3DLine(parent_translation, translation, video::SColor(255, 51, 66, 255)); } } @@ -407,12 +396,12 @@ IBoneSceneNode *CAnimatedMeshSceneNode::getJointNode(const c8 *jointName) return 0; } - if (JointChildSceneNodes.size() <= *number) { + if (PerJoint.SceneNodes.size() <= *number) { os::Printer::log("Joint was found in mesh, but is not loaded into node", jointName, ELL_WARNING); return 0; } - return JointChildSceneNodes[*number]; + return PerJoint.SceneNodes[*number]; } //! Returns a pointer to a child node, which has the same transformation as @@ -426,12 +415,12 @@ IBoneSceneNode *CAnimatedMeshSceneNode::getJointNode(u32 jointID) checkJoints(); - if (JointChildSceneNodes.size() <= jointID) { + if (PerJoint.SceneNodes.size() <= jointID) { os::Printer::log("Joint not loaded into node", ELL_WARNING); return 0; } - return JointChildSceneNodes[jointID]; + return PerJoint.SceneNodes[jointID]; } //! Gets joint count. @@ -452,9 +441,9 @@ bool CAnimatedMeshSceneNode::removeChild(ISceneNode *child) { if (ISceneNode::removeChild(child)) { if (JointsUsed) { // stop weird bugs caused while changing parents as the joints are being created - for (u32 i = 0; i < JointChildSceneNodes.size(); ++i) { - if (JointChildSceneNodes[i] == child) { - JointChildSceneNodes[i] = 0; // remove link to child + for (u32 i = 0; i < PerJoint.SceneNodes.size(); ++i) { + if (PerJoint.SceneNodes[i] == child) { + PerJoint.SceneNodes[i] = 0; // remove link to child break; } } @@ -478,22 +467,6 @@ bool CAnimatedMeshSceneNode::getLoopMode() const return Looping; } -//! Sets a callback interface which will be called if an animation -//! playback has ended. Set this to 0 to disable the callback again. -void CAnimatedMeshSceneNode::setAnimationEndCallback(IAnimationEndCallBack *callback) -{ - if (callback == LoopCallBack) - return; - - if (LoopCallBack) - LoopCallBack->drop(); - - LoopCallBack = callback; - - if (LoopCallBack) - LoopCallBack->grab(); -} - //! Sets if the scene node should not copy the materials of the mesh but use them in a read only style. void CAnimatedMeshSceneNode::setReadOnlyMaterials(bool readonly) { @@ -525,18 +498,15 @@ void CAnimatedMeshSceneNode::setMesh(IAnimatedMesh *mesh) // get materials and bounding box Box = Mesh->getBoundingBox(); - IMesh *m = Mesh->getMesh(0); - if (m) { - Materials.clear(); - Materials.reallocate(m->getMeshBufferCount()); + Materials.clear(); + Materials.reallocate(Mesh->getMeshBufferCount()); - for (u32 i = 0; i < m->getMeshBufferCount(); ++i) { - IMeshBuffer *mb = m->getMeshBuffer(i); - if (mb) - Materials.push_back(mb->getMaterial()); - else - Materials.push_back(video::SMaterial()); - } + for (u32 i = 0; i < Mesh->getMeshBufferCount(); ++i) { + IMeshBuffer *mb = Mesh->getMeshBuffer(i); + if (mb) + Materials.push_back(mb->getMaterial()); + else + Materials.push_back(video::SMaterial()); } // clean up joint nodes @@ -556,14 +526,7 @@ void CAnimatedMeshSceneNode::updateAbsolutePosition() IAnimatedMeshSceneNode::updateAbsolutePosition(); } -//! Set the joint update mode (0-unused, 1-get joints only, 2-set joints only, 3-move and set) -void CAnimatedMeshSceneNode::setJointMode(E_JOINT_UPDATE_ON_RENDER mode) -{ - checkJoints(); - JointMode = mode; -} - -//! Sets the transition time in seconds (note: This needs to enable joints, and setJointmode maybe set to 2) +//! Sets the transition time in seconds (note: This needs to enable joints) //! you must call animateJoints(), or the mesh will not animate void CAnimatedMeshSceneNode::setTransitionTime(f32 time) { @@ -571,10 +534,6 @@ void CAnimatedMeshSceneNode::setTransitionTime(f32 time) if (TransitionTime == ttime) return; TransitionTime = ttime; - if (ttime != 0) - setJointMode(EJUOR_CONTROL); - else - setJointMode(EJUOR_NONE); } //! render mesh ignoring its transformation. Used with ragdolls. (culling is unaffected) @@ -583,120 +542,104 @@ void CAnimatedMeshSceneNode::setRenderFromIdentity(bool enable) RenderFromIdentity = enable; } -//! updates the joint positions of this mesh -void CAnimatedMeshSceneNode::animateJoints(bool CalculateAbsolutePositions) +void CAnimatedMeshSceneNode::addJoints() { - if (Mesh && Mesh->getMeshType() == EAMT_SKINNED) { - checkJoints(); - const f32 frame = getFrameNr(); // old? + const auto &joints = static_cast(Mesh)->getAllJoints(); + PerJoint.setN(joints.size()); + PerJoint.SceneNodes.clear(); + PerJoint.SceneNodes.reserve(joints.size()); + for (size_t i = 0; i < joints.size(); ++i) { + const auto *joint = joints[i]; + ISceneNode *parent = this; + if (joint->ParentJointID) + parent = PerJoint.SceneNodes.at(*joint->ParentJointID); // exists because of topo. order + assert(parent); + const auto *matrix = std::get_if(&joint->transform); + PerJoint.SceneNodes.push_back(new CBoneSceneNode( + parent, SceneManager, 0, i, joint->Name, + matrix ? core::Transform{} : std::get(joint->transform), + matrix ? *matrix : std::optional{})); + } +} - SkinnedMesh *skinnedMesh = static_cast(Mesh); - - skinnedMesh->animateMesh(frame); - skinnedMesh->recoverJointsFromMesh(JointChildSceneNodes); - - //----------------------------------------- - // Transition - //----------------------------------------- - - if (Transiting != 0.f) { - // Init additional matrices - if (PretransitingSave.size() < JointChildSceneNodes.size()) { - for (u32 n = PretransitingSave.size(); n < JointChildSceneNodes.size(); ++n) - PretransitingSave.push_back(core::matrix4()); - } - - for (u32 n = 0; n < JointChildSceneNodes.size(); ++n) { - //------Position------ - - JointChildSceneNodes[n]->setPosition( - core::lerp( - PretransitingSave[n].getTranslation(), - JointChildSceneNodes[n]->getPosition(), - TransitingBlend)); - - //------Rotation------ - - // Code is slow, needs to be fixed up - - const core::quaternion RotationStart(PretransitingSave[n].getRotationRadians()); - const core::quaternion RotationEnd(JointChildSceneNodes[n]->getRotation() * core::DEGTORAD); - - core::quaternion QRotation; - QRotation.slerp(RotationStart, RotationEnd, TransitingBlend); - - core::vector3df tmpVector; - QRotation.toEuler(tmpVector); - tmpVector *= core::RADTODEG; // convert from radians back to degrees - JointChildSceneNodes[n]->setRotation(tmpVector); - - //------Scale------ - - // JointChildSceneNodes[n]->setScale( - // core::lerp( - // PretransitingSave[n].getScale(), - // JointChildSceneNodes[n]->getScale(), - // TransitingBlend)); - } +void CAnimatedMeshSceneNode::updateJointSceneNodes( + const std::vector &transforms) +{ + for (size_t i = 0; i < transforms.size(); ++i) { + const auto &transform = transforms[i]; + auto *node = static_cast(PerJoint.SceneNodes[i]); + if (const auto *trs = std::get_if(&transform)) { + node->setTransform(*trs); + // .x lets animations override matrix transforms entirely. + node->Matrix = std::nullopt; + } else { + node->Matrix = std::get(transform); } + } +} - if (CalculateAbsolutePositions) { - //---slow--- - for (u32 n = 0; n < JointChildSceneNodes.size(); ++n) { - if (JointChildSceneNodes[n]->getParent() == this) { - JointChildSceneNodes[n]->updateAbsolutePositionOfAllChildren(); // temp, should be an option - } +//! updates the joint positions of this mesh +void CAnimatedMeshSceneNode::animateJoints() +{ + if (!Mesh || Mesh->getMeshType() != EAMT_SKINNED) + return; + + checkJoints(); + + SkinnedMesh *skinnedMesh = static_cast(Mesh); + if (!skinnedMesh->isStatic()) + updateJointSceneNodes(skinnedMesh->animateMesh(getFrameNr())); + + //----------------------------------------- + // Transition + //----------------------------------------- + + if (Transiting != 0.f) { + for (u32 i = 0; i < PerJoint.SceneNodes.size(); ++i) { + if (PerJoint.PreTransSaves[i]) { + PerJoint.SceneNodes[i]->setTransform(PerJoint.PreTransSaves[i]->interpolate( + PerJoint.SceneNodes[i]->getTransform(), TransitingBlend)); } } } } -/*! - */ void CAnimatedMeshSceneNode::checkJoints() { if (!Mesh || Mesh->getMeshType() != EAMT_SKINNED) return; if (!JointsUsed) { - for (u32 i = 0; i < JointChildSceneNodes.size(); ++i) - removeChild(JointChildSceneNodes[i]); - JointChildSceneNodes.clear(); - - // Create joints for SkinnedMesh - ((SkinnedMesh *)Mesh)->addJoints(JointChildSceneNodes, this, SceneManager); - ((SkinnedMesh *)Mesh)->recoverJointsFromMesh(JointChildSceneNodes); + for (u32 i = 0; i < PerJoint.SceneNodes.size(); ++i) + removeChild(PerJoint.SceneNodes[i]); + addJoints(); JointsUsed = true; - JointMode = EJUOR_READ; } } -/*! - */ +void CAnimatedMeshSceneNode::copyOldTransforms() +{ + for (u32 i = 0; i < PerJoint.SceneNodes.size(); ++i) { + if (!PerJoint.SceneNodes[i]->Matrix) { + PerJoint.PreTransSaves[i] = PerJoint.SceneNodes[i]->getTransform(); + } else { + PerJoint.PreTransSaves[i] = std::nullopt; + } + } +} + void CAnimatedMeshSceneNode::beginTransition() { if (!JointsUsed) return; if (TransitionTime != 0) { - // Check the array is big enough - if (PretransitingSave.size() < JointChildSceneNodes.size()) { - for (u32 n = PretransitingSave.size(); n < JointChildSceneNodes.size(); ++n) - PretransitingSave.push_back(core::matrix4()); - } - - // Copy the position of joints - for (u32 n = 0; n < JointChildSceneNodes.size(); ++n) - PretransitingSave[n] = JointChildSceneNodes[n]->getRelativeTransformation(); - Transiting = core::reciprocal((f32)TransitionTime); } TransitingBlend = 0.f; } -/*! - */ ISceneNode *CAnimatedMeshSceneNode::clone(ISceneNode *newParent, ISceneManager *newManager) { if (!newParent) @@ -722,19 +665,15 @@ ISceneNode *CAnimatedMeshSceneNode::clone(ISceneNode *newParent, ISceneManager * newNode->EndFrame = EndFrame; newNode->FramesPerSecond = FramesPerSecond; newNode->CurrentFrameNr = CurrentFrameNr; - newNode->JointMode = JointMode; newNode->JointsUsed = JointsUsed; newNode->TransitionTime = TransitionTime; newNode->Transiting = Transiting; newNode->TransitingBlend = TransitingBlend; newNode->Looping = Looping; newNode->ReadOnlyMaterials = ReadOnlyMaterials; - newNode->LoopCallBack = LoopCallBack; - if (newNode->LoopCallBack) - newNode->LoopCallBack->grab(); newNode->PassCount = PassCount; - newNode->JointChildSceneNodes = JointChildSceneNodes; - newNode->PretransitingSave = PretransitingSave; + newNode->PerJoint.SceneNodes = PerJoint.SceneNodes; + newNode->PerJoint.PreTransSaves = PerJoint.PreTransSaves; newNode->RenderFromIdentity = RenderFromIdentity; return newNode; diff --git a/irr/src/CAnimatedMeshSceneNode.h b/irr/src/CAnimatedMeshSceneNode.h index 5149f7618f..c67479481e 100644 --- a/irr/src/CAnimatedMeshSceneNode.h +++ b/irr/src/CAnimatedMeshSceneNode.h @@ -4,9 +4,12 @@ #pragma once +#include "CBoneSceneNode.h" #include "IAnimatedMeshSceneNode.h" #include "IAnimatedMesh.h" +#include "SkinnedMesh.h" +#include "Transform.h" #include "matrix4.h" namespace irr @@ -54,9 +57,11 @@ public: //! returns the current loop mode bool getLoopMode() const override; - //! Sets a callback interface which will be called if an animation - //! playback has ended. Set this to 0 to disable the callback again. - void setAnimationEndCallback(IAnimationEndCallBack *callback = 0) override; + void setOnAnimateCallback( + const std::function &cb) override + { + OnAnimateCallback = cb; + } //! sets the speed with which the animation is played //! NOTE: setMesh will also change this value and set it to the default speed of the mesh @@ -117,15 +122,16 @@ public: //! updates the absolute position based on the relative and the parents position void updateAbsolutePosition() override; - //! Set the joint update mode (0-unused, 1-get joints only, 2-set joints only, 3-move and set) - void setJointMode(E_JOINT_UPDATE_ON_RENDER mode) override; - - //! Sets the transition time in seconds (note: This needs to enable joints, and setJointmode maybe set to 2) + //! Sets the transition time in seconds (note: This needs to enable joints) //! you must call animateJoints(), or the mesh will not animate void setTransitionTime(f32 Time) override; + void updateJointSceneNodes(const std::vector &transforms); + //! updates the joint positions of this mesh - void animateJoints(bool CalculateAbsolutePositions = true) override; + void animateJoints() override; + + void addJoints(); //! render mesh ignoring its transformation. Used with ragdolls. (culling is unaffected) void setRenderFromIdentity(bool On) override; @@ -142,6 +148,7 @@ private: void buildFrameNr(u32 timeMs); void checkJoints(); + void copyOldTransforms(); void beginTransition(); core::array Materials; @@ -158,19 +165,30 @@ private: f32 Transiting; // is mesh transiting (plus cache of TransitionTime) f32 TransitingBlend; // 0-1, calculated on buildFrameNr - // 0-unused, 1-get joints only, 2-set joints only, 3-move and set - E_JOINT_UPDATE_ON_RENDER JointMode; bool JointsUsed; bool Looping; bool ReadOnlyMaterials; bool RenderFromIdentity; - IAnimationEndCallBack *LoopCallBack; s32 PassCount; + std::function OnAnimateCallback; - std::vector JointChildSceneNodes; - core::array PretransitingSave; + struct PerJointData { + std::vector SceneNodes; + std::vector GlobalMatrices; + std::vector> PreTransSaves; + void setN(u16 n) { + SceneNodes.clear(); + SceneNodes.resize(n); + GlobalMatrices.clear(); + GlobalMatrices.resize(n); + PreTransSaves.clear(); + PreTransSaves.resize(n); + } + }; + + PerJointData PerJoint; }; } // end namespace scene diff --git a/irr/src/CB3DMeshFileLoader.cpp b/irr/src/CB3DMeshFileLoader.cpp index 51342f4517..6e3e147916 100644 --- a/irr/src/CB3DMeshFileLoader.cpp +++ b/irr/src/CB3DMeshFileLoader.cpp @@ -143,31 +143,25 @@ bool CB3DMeshFileLoader::readChunkNODE(SkinnedMesh::SJoint *inJoint) os::Printer::log(logStr.c_str(), joint->Name.value_or("").c_str(), ELL_DEBUG); #endif - f32 position[3], scale[3], rotation[4]; + core::Transform transform; + { + f32 t[3], s[3], r[4]; - readFloats(position, 3); - readFloats(scale, 3); - readFloats(rotation, 4); + readFloats(t, 3); + readFloats(s, 3); + readFloats(r, 4); - joint->Animatedposition = core::vector3df(position[0], position[1], position[2]); - joint->Animatedscale = core::vector3df(scale[0], scale[1], scale[2]); - joint->Animatedrotation = core::quaternion(rotation[1], rotation[2], rotation[3], rotation[0]); - - // Build LocalMatrix: - - core::matrix4 positionMatrix; - positionMatrix.setTranslation(joint->Animatedposition); - core::matrix4 scaleMatrix; - scaleMatrix.setScale(joint->Animatedscale); - core::matrix4 rotationMatrix; - joint->Animatedrotation.getMatrix_transposed(rotationMatrix); - - joint->LocalMatrix = positionMatrix * rotationMatrix * scaleMatrix; + joint->transform = transform = { + {t[0], t[1], t[2]}, + {r[1], r[2], r[3], r[0]}, + {s[0], s[1], s[2]}, + }; + } if (inJoint) - joint->GlobalMatrix = inJoint->GlobalMatrix * joint->LocalMatrix; + joint->GlobalMatrix = inJoint->GlobalMatrix * transform.buildMatrix(); else - joint->GlobalMatrix = joint->LocalMatrix; + joint->GlobalMatrix = transform.buildMatrix(); while (B3dStack.getLast().startposition + B3dStack.getLast().length > B3DFile->getPos()) // this chunk repeats { diff --git a/irr/src/CBoneSceneNode.cpp b/irr/src/CBoneSceneNode.cpp deleted file mode 100644 index 7aa637094b..0000000000 --- a/irr/src/CBoneSceneNode.cpp +++ /dev/null @@ -1,86 +0,0 @@ -// 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 "CBoneSceneNode.h" - -#include - -namespace irr -{ -namespace scene -{ - -//! constructor -CBoneSceneNode::CBoneSceneNode(ISceneNode *parent, ISceneManager *mgr, s32 id, - u32 boneIndex, const std::optional &boneName) : - IBoneSceneNode(parent, mgr, id), - BoneIndex(boneIndex), - AnimationMode(EBAM_AUTOMATIC), SkinningSpace(EBSS_LOCAL) -{ - setName(boneName); -} - -//! Returns the index of the bone -u32 CBoneSceneNode::getBoneIndex() const -{ - return BoneIndex; -} - -//! Sets the animation mode of the bone. Returns true if successful. -bool CBoneSceneNode::setAnimationMode(E_BONE_ANIMATION_MODE mode) -{ - AnimationMode = mode; - return true; -} - -//! Gets the current animation mode of the bone -E_BONE_ANIMATION_MODE CBoneSceneNode::getAnimationMode() const -{ - return AnimationMode; -} - -//! returns the axis aligned bounding box of this node -const core::aabbox3d &CBoneSceneNode::getBoundingBox() const -{ - return Box; -} - -/* -//! Returns the relative transformation of the scene node. -core::matrix4 CBoneSceneNode::getRelativeTransformation() const -{ - return core::matrix4(); // RelativeTransformation; -} -*/ - -void CBoneSceneNode::OnAnimate(u32 timeMs) -{ - if (IsVisible) { - // update absolute position - // updateAbsolutePosition(); - - // perform the post render process on all children - ISceneNodeList::iterator it = Children.begin(); - for (; it != Children.end(); ++it) - (*it)->OnAnimate(timeMs); - } -} - -void CBoneSceneNode::helper_updateAbsolutePositionOfAllChildren(ISceneNode *Node) -{ - Node->updateAbsolutePosition(); - - ISceneNodeList::const_iterator it = Node->getChildren().begin(); - for (; it != Node->getChildren().end(); ++it) { - helper_updateAbsolutePositionOfAllChildren((*it)); - } -} - -void CBoneSceneNode::updateAbsolutePositionOfAllChildren() -{ - helper_updateAbsolutePositionOfAllChildren(this); -} - -} // namespace scene -} // namespace irr diff --git a/irr/src/CBoneSceneNode.h b/irr/src/CBoneSceneNode.h index d900570db0..4151f73721 100644 --- a/irr/src/CBoneSceneNode.h +++ b/irr/src/CBoneSceneNode.h @@ -7,6 +7,8 @@ // Used with SkinnedMesh and IAnimatedMeshSceneNode, for boned meshes #include "IBoneSceneNode.h" +#include "Transform.h" +#include "matrix4.h" #include @@ -21,49 +23,48 @@ public: //! constructor CBoneSceneNode(ISceneNode *parent, ISceneManager *mgr, s32 id = -1, u32 boneIndex = 0, - const std::optional &boneName = std::nullopt); - - //! Returns the index of the bone - u32 getBoneIndex() const override; - - //! Sets the animation mode of the bone. Returns true if successful. - bool setAnimationMode(E_BONE_ANIMATION_MODE mode) override; - - //! Gets the current animation mode of the bone - E_BONE_ANIMATION_MODE getAnimationMode() const override; - - //! returns the axis aligned bounding box of this node - const core::aabbox3d &getBoundingBox() const override; - - /* - //! Returns the relative transformation of the scene node. - //core::matrix4 getRelativeTransformation() const override; - */ - - void OnAnimate(u32 timeMs) override; - - void updateAbsolutePositionOfAllChildren() override; - - //! How the relative transformation of the bone is used - void setSkinningSpace(E_BONE_SKINNING_SPACE space) override + const std::optional &boneName = std::nullopt, + const core::Transform &transform = {}, + const std::optional &matrix = std::nullopt) : + IBoneSceneNode(parent, mgr, id, boneIndex, boneName), + Matrix(matrix) { - SkinningSpace = space; + setTransform(transform); } - E_BONE_SKINNING_SPACE getSkinningSpace() const override + void setTransform(const core::Transform &transform) { - return SkinningSpace; + setPosition(transform.translation); + { + core::vector3df euler; + auto rot = transform.rotation; + // Invert to be consistent with setRotationDegrees + rot.makeInverse(); + rot.toEuler(euler); + setRotation(euler * core::RADTODEG); + } + setScale(transform.scale); } -private: - void helper_updateAbsolutePositionOfAllChildren(ISceneNode *Node); + core::Transform getTransform() const + { + return { + getPosition(), + core::quaternion(getRotation() * core::DEGTORAD).makeInverse(), + getScale() + }; + } - u32 BoneIndex; + core::matrix4 getRelativeTransformation() const override + { + if (Matrix) + return *Matrix; + return IBoneSceneNode::getRelativeTransformation(); + } - core::aabbox3d Box{-1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f}; - - E_BONE_ANIMATION_MODE AnimationMode; - E_BONE_SKINNING_SPACE SkinningSpace; + //! Some file formats alternatively let bones specify a transformation matrix. + //! If this is set, it overrides the TRS properties. + std::optional Matrix; }; } // end namespace scene diff --git a/irr/src/CGLTFMeshFileLoader.cpp b/irr/src/CGLTFMeshFileLoader.cpp index 3f2096f40e..53ae1b5808 100644 --- a/irr/src/CGLTFMeshFileLoader.cpp +++ b/irr/src/CGLTFMeshFileLoader.cpp @@ -539,34 +539,25 @@ static core::matrix4 loadTransform(const tiniergltf::Node::Matrix &m, SkinnedMes mat[i] = static_cast(m[i]); mat = convertHandedness(mat); - // Decompose the matrix into translation, scale, and rotation. - joint->Animatedposition = mat.getTranslation(); - - auto scale = mat.getScale(); - joint->Animatedscale = scale; - joint->Animatedrotation = mat.getRotationRadians(scale); - // Invert the rotation because it is applied using `getMatrix_transposed`, - // which again inverts. - joint->Animatedrotation.makeInverse(); - + // Note: "When a node is targeted for animation [...], + // only TRS properties MAY be present; matrix MUST NOT be present." + // Thus we MUST NOT do any decomposition, which in general need not exist. + joint->transform = mat; return mat; } static core::matrix4 loadTransform(const tiniergltf::Node::TRS &trs, SkinnedMesh::SJoint *joint) { - const auto &trans = trs.translation; - const auto &rot = trs.rotation; - const auto &scale = trs.scale; - core::matrix4 transMat; - joint->Animatedposition = convertHandedness(core::vector3df(trans[0], trans[1], trans[2])); - transMat.setTranslation(joint->Animatedposition); - core::matrix4 rotMat; - joint->Animatedrotation = convertHandedness(core::quaternion(rot[0], rot[1], rot[2], rot[3])); - core::quaternion(joint->Animatedrotation).getMatrix_transposed(rotMat); - joint->Animatedscale = core::vector3df(scale[0], scale[1], scale[2]); - core::matrix4 scaleMat; - scaleMat.setScale(joint->Animatedscale); - return transMat * rotMat * scaleMat; + const auto &t = trs.translation; + const auto &r = trs.rotation; + const auto &s = trs.scale; + core::Transform transform{ + convertHandedness(core::vector3df(t[0], t[1], t[2])), + convertHandedness(core::quaternion(r[0], r[1], r[2], r[3])), + core::vector3df(s[0], s[1], s[2]), + }; + joint->transform = transform; + return transform.buildMatrix(); } static core::matrix4 loadTransform(std::optional> transform, @@ -584,8 +575,7 @@ void SelfType::MeshExtractor::loadNode( const auto &node = m_gltf_model.nodes->at(nodeIdx); auto *joint = m_irr_model->addJoint(parent); const core::matrix4 transform = loadTransform(node.transform, joint); - joint->LocalMatrix = transform; - joint->GlobalMatrix = parent ? parent->GlobalMatrix * joint->LocalMatrix : joint->LocalMatrix; + joint->GlobalMatrix = parent ? parent->GlobalMatrix * transform : transform; if (node.name.has_value()) { joint->Name = node.name->c_str(); } @@ -642,7 +632,6 @@ void SelfType::MeshExtractor::loadAnimation(const std::size_t animIdx) { const auto &anim = m_gltf_model.animations->at(animIdx); for (const auto &channel : anim.channels) { - const auto &sampler = anim.samplers.at(channel.sampler); bool interpolate = ([&]() { @@ -663,6 +652,11 @@ void SelfType::MeshExtractor::loadAnimation(const std::size_t animIdx) throw std::runtime_error("no animated node"); auto *joint = m_loaded_nodes.at(*channel.target.node); + if (std::holds_alternative(joint->transform)) { + warn("nodes using matrix transforms must not be animated"); + continue; + } + switch (channel.target.path) { case tiniergltf::AnimationChannelTarget::Path::TRANSLATION: { const auto outputAccessor = Accessor::make(m_gltf_model, sampler.output); diff --git a/irr/src/CMakeLists.txt b/irr/src/CMakeLists.txt index 83da89c116..c752d7d31c 100644 --- a/irr/src/CMakeLists.txt +++ b/irr/src/CMakeLists.txt @@ -320,7 +320,6 @@ set(IRRMESHLOADER add_library(IRRMESHOBJ OBJECT SkinnedMesh.cpp - CBoneSceneNode.cpp CMeshSceneNode.cpp CAnimatedMeshSceneNode.cpp ${IRRMESHLOADER} diff --git a/irr/src/CMeshCache.cpp b/irr/src/CMeshCache.cpp index 4f7e1203c2..9a41aa5826 100644 --- a/irr/src/CMeshCache.cpp +++ b/irr/src/CMeshCache.cpp @@ -35,7 +35,7 @@ void CMeshCache::removeMesh(const IMesh *const mesh) if (!mesh) return; for (u32 i = 0; i < Meshes.size(); ++i) { - if (Meshes[i].Mesh == mesh || (Meshes[i].Mesh && Meshes[i].Mesh->getMesh(0) == mesh)) { + if (Meshes[i].Mesh == mesh) { Meshes[i].Mesh->drop(); Meshes.erase(i); return; @@ -53,7 +53,7 @@ u32 CMeshCache::getMeshCount() const s32 CMeshCache::getMeshIndex(const IMesh *const mesh) const { for (u32 i = 0; i < Meshes.size(); ++i) { - if (Meshes[i].Mesh == mesh || (Meshes[i].Mesh && Meshes[i].Mesh->getMesh(0) == mesh)) + if (Meshes[i].Mesh == mesh) return (s32)i; } @@ -93,7 +93,7 @@ const io::SNamedPath &CMeshCache::getMeshName(const IMesh *const mesh) const return emptyNamedPath; for (u32 i = 0; i < Meshes.size(); ++i) { - if (Meshes[i].Mesh == mesh || (Meshes[i].Mesh && Meshes[i].Mesh->getMesh(0) == mesh)) + if (Meshes[i].Mesh == mesh) return Meshes[i].NamedPath; } @@ -115,7 +115,7 @@ bool CMeshCache::renameMesh(u32 index, const io::path &name) bool CMeshCache::renameMesh(const IMesh *const mesh, const io::path &name) { for (u32 i = 0; i < Meshes.size(); ++i) { - if (Meshes[i].Mesh == mesh || (Meshes[i].Mesh && Meshes[i].Mesh->getMesh(0) == mesh)) { + if (Meshes[i].Mesh == mesh) { Meshes[i].NamedPath.setPath(name); Meshes.sort(); return true; diff --git a/irr/src/CMeshManipulator.cpp b/irr/src/CMeshManipulator.cpp index 659e8dff8a..535b74686b 100644 --- a/irr/src/CMeshManipulator.cpp +++ b/irr/src/CMeshManipulator.cpp @@ -6,7 +6,6 @@ #include "SkinnedMesh.h" #include "SMesh.h" #include "CMeshBuffer.h" -#include "SAnimatedMesh.h" #include "os.h" #include @@ -178,34 +177,5 @@ SMesh *CMeshManipulator::createMeshCopy(scene::IMesh *mesh) const return clone; } -//! Returns amount of polygons in mesh. -s32 CMeshManipulator::getPolyCount(scene::IMesh *mesh) const -{ - if (!mesh) - return 0; - - s32 trianglecount = 0; - - for (u32 g = 0; g < mesh->getMeshBufferCount(); ++g) - trianglecount += mesh->getMeshBuffer(g)->getIndexCount() / 3; - - return trianglecount; -} - -//! Returns amount of polygons in mesh. -s32 CMeshManipulator::getPolyCount(scene::IAnimatedMesh *mesh) const -{ - if (mesh && mesh->getMaxFrameNumber() != 0) - return getPolyCount(mesh->getMesh(0)); - - return 0; -} - -//! create a new AnimatedMesh and adds the mesh to it -IAnimatedMesh *CMeshManipulator::createAnimatedMesh(scene::IMesh *mesh, scene::E_ANIMATED_MESH_TYPE type) const -{ - return new SAnimatedMesh(mesh, type); -} - } // end namespace scene } // end namespace irr diff --git a/irr/src/CMeshManipulator.h b/irr/src/CMeshManipulator.h index 0377d6a3a0..c5632512d2 100644 --- a/irr/src/CMeshManipulator.h +++ b/irr/src/CMeshManipulator.h @@ -31,15 +31,6 @@ public: //! Clones a static IMesh into a modifiable SMesh. SMesh *createMeshCopy(scene::IMesh *mesh) const override; - - //! Returns amount of polygons in mesh. - s32 getPolyCount(scene::IMesh *mesh) const override; - - //! Returns amount of polygons in mesh. - s32 getPolyCount(scene::IAnimatedMesh *mesh) const override; - - //! create a new AnimatedMesh and adds the mesh to it - IAnimatedMesh *createAnimatedMesh(scene::IMesh *mesh, scene::E_ANIMATED_MESH_TYPE type) const override; }; } // end namespace scene diff --git a/irr/src/CNullDriver.cpp b/irr/src/CNullDriver.cpp index 246f414de6..f7d0129131 100644 --- a/irr/src/CNullDriver.cpp +++ b/irr/src/CNullDriver.cpp @@ -1238,7 +1238,7 @@ void CNullDriver::addOcclusionQuery(scene::ISceneNode *node, const scene::IMesh else if (node->getType() == scene::ESNT_MESH) mesh = static_cast(node)->getMesh(); else - mesh = static_cast(node)->getMesh()->getMesh(0); + mesh = static_cast(node)->getMesh(); if (!mesh) return; } diff --git a/irr/src/COBJMeshFileLoader.cpp b/irr/src/COBJMeshFileLoader.cpp index 5c7f389504..bc51c10f21 100644 --- a/irr/src/COBJMeshFileLoader.cpp +++ b/irr/src/COBJMeshFileLoader.cpp @@ -7,7 +7,6 @@ #include "IVideoDriver.h" #include "SMesh.h" #include "SMeshBuffer.h" -#include "SAnimatedMesh.h" #include "IReadFile.h" #include "fast_atof.h" #include "coreutil.h" @@ -272,23 +271,19 @@ IAnimatedMesh *COBJMeshFileLoader::createMesh(io::IReadFile *file) } } - // Create the Animated mesh if there's anything in the mesh - SAnimatedMesh *animMesh = 0; - if (0 != mesh->getMeshBufferCount()) { - mesh->recalculateBoundingBox(); - animMesh = new SAnimatedMesh(); - animMesh->Type = EAMT_OBJ; - animMesh->addMesh(mesh); - animMesh->recalculateBoundingBox(); - } - // Clean up the allocate obj file contents delete[] buf; // more cleaning up cleanUp(); - mesh->drop(); - return animMesh; + // Nothing in the mesh + if (mesh->getMeshBufferCount() == 0) { + mesh->drop(); + return nullptr; + } + + mesh->recalculateBoundingBox(); + return mesh; } //! Read RGB color diff --git a/irr/src/CSceneManager.cpp b/irr/src/CSceneManager.cpp index 05f8de71a5..b0b82b7726 100644 --- a/irr/src/CSceneManager.cpp +++ b/irr/src/CSceneManager.cpp @@ -8,7 +8,6 @@ #include "CSceneManager.h" #include "IVideoDriver.h" #include "IFileSystem.h" -#include "SAnimatedMesh.h" #include "CMeshCache.h" #include "IGUIEnvironment.h" #include "IMaterialRenderer.h" diff --git a/irr/src/CXMeshFileLoader.cpp b/irr/src/CXMeshFileLoader.cpp index d93502d6b5..f2c4e94e6f 100644 --- a/irr/src/CXMeshFileLoader.cpp +++ b/irr/src/CXMeshFileLoader.cpp @@ -4,6 +4,7 @@ #include "CXMeshFileLoader.h" #include "SkinnedMesh.h" +#include "Transform.h" #include "os.h" #include "fast_atof.h" @@ -513,6 +514,7 @@ bool CXMeshFileLoader::parseDataObjectFrame(SkinnedMesh::SJoint *Parent) if (n.has_value()) { JointID = *n; joint = AnimatedMesh->getAllJoints()[JointID]; + joint->setParent(Parent); } } @@ -527,8 +529,6 @@ bool CXMeshFileLoader::parseDataObjectFrame(SkinnedMesh::SJoint *Parent) #ifdef _XREADER_DEBUG os::Printer::log("using joint ", name.c_str(), ELL_DEBUG); #endif - if (Parent) - Parent->Children.push_back(joint); } // Now inside a frame. @@ -552,12 +552,10 @@ bool CXMeshFileLoader::parseDataObjectFrame(SkinnedMesh::SJoint *Parent) if (!parseDataObjectFrame(joint)) return false; } else if (objectName == "FrameTransformMatrix") { - if (!parseDataObjectTransformationMatrix(joint->LocalMatrix)) + core::matrix4 matrix; + if (!parseDataObjectTransformationMatrix(matrix)) return false; - - // joint->LocalAnimatedMatrix - // joint->LocalAnimatedMatrix.makeInverse(); - // joint->LocalMatrix=tmp*joint->LocalAnimatedMatrix; + joint->transform = matrix; } else if (objectName == "Mesh") { /* frame.Meshes.push_back(SXMesh()); diff --git a/irr/src/SkinnedMesh.cpp b/irr/src/SkinnedMesh.cpp index 938a50e178..cf58121ba6 100644 --- a/irr/src/SkinnedMesh.cpp +++ b/irr/src/SkinnedMesh.cpp @@ -7,7 +7,15 @@ #include "CBoneSceneNode.h" #include "IAnimatedMeshSceneNode.h" #include "SSkinMeshBuffer.h" +#include "Transform.h" +#include "aabbox3d.h" +#include "irrMath.h" +#include "matrix4.h" #include "os.h" +#include "vector3d.h" +#include +#include +#include #include #include @@ -48,183 +56,77 @@ void SkinnedMesh::setAnimationSpeed(f32 fps) FramesPerSecond = fps; } -//! returns the animated mesh based -IMesh *SkinnedMesh::getMesh(f32 frame) -{ - // animate(frame,startFrameLoop, endFrameLoop); - if (frame == -1) - return this; +// Keyframe Animation - animateMesh(frame); - skinMesh(); - return this; + +using VariantTransform = SkinnedMesh::SJoint::VariantTransform; +std::vector SkinnedMesh::animateMesh(f32 frame) +{ + assert(HasAnimation); + std::vector result; + result.reserve(AllJoints.size()); + for (auto *joint : AllJoints) + result.push_back(joint->animate(frame)); + return result; } -//-------------------------------------------------------------------------- -// Keyframe Animation -//-------------------------------------------------------------------------- - -//! Animates joints based on frame input -void SkinnedMesh::animateMesh(f32 frame) +core::aabbox3df SkinnedMesh::calculateBoundingBox( + const std::vector &global_transforms) { - if (!HasAnimation || LastAnimatedFrame == frame) - return; - - LastAnimatedFrame = frame; - SkinnedLastFrame = false; - - for (auto *joint : AllJoints) { - // The joints can be animated here with no input from their - // parents, but for setAnimationMode extra checks are needed - // to their parents - joint->keys.updateTransform(frame, - joint->Animatedposition, - joint->Animatedrotation, - joint->Animatedscale); + assert(global_transforms.size() == AllJoints.size()); + core::aabbox3df result = StaticPartsBox; + // skeletal animation + for (u16 i = 0; i < AllJoints.size(); ++i) { + auto box = AllJoints[i]->LocalBoundingBox; + global_transforms[i].transformBoxEx(box); + result.addInternalBox(box); } - - // Note: - // LocalAnimatedMatrix needs to be built at some point, but this function may be called lots of times for - // one render (to play two animations at the same time) LocalAnimatedMatrix only needs to be built once. - // a call to buildAllLocalAnimatedMatrices is needed before skinning the mesh, and before the user gets the joints to move - - //---------------- - // Temp! - buildAllLocalAnimatedMatrices(); - //----------------- - - updateBoundingBox(); -} - -void SkinnedMesh::buildAllLocalAnimatedMatrices() -{ - for (auto *joint : AllJoints) { - // Could be faster: - - if (!joint->keys.empty()) { - joint->GlobalSkinningSpace = false; - - // IRR_TEST_BROKEN_QUATERNION_USE: TODO - switched to getMatrix_transposed instead of getMatrix for downward compatibility. - // Not tested so far if this was correct or wrong before quaternion fix! - // Note that using getMatrix_transposed inverts the rotation. - joint->Animatedrotation.getMatrix_transposed(joint->LocalAnimatedMatrix); - - // --- joint->LocalAnimatedMatrix *= joint->Animatedrotation.getMatrix() --- - f32 *m1 = joint->LocalAnimatedMatrix.pointer(); - core::vector3df &Pos = joint->Animatedposition; - m1[0] += Pos.X * m1[3]; - m1[1] += Pos.Y * m1[3]; - m1[2] += Pos.Z * m1[3]; - m1[4] += Pos.X * m1[7]; - m1[5] += Pos.Y * m1[7]; - m1[6] += Pos.Z * m1[7]; - m1[8] += Pos.X * m1[11]; - m1[9] += Pos.Y * m1[11]; - m1[10] += Pos.Z * m1[11]; - m1[12] += Pos.X * m1[15]; - m1[13] += Pos.Y * m1[15]; - m1[14] += Pos.Z * m1[15]; - // ----------------------------------- - - if (!joint->keys.scale.empty()) { - /* - core::matrix4 scaleMatrix; - scaleMatrix.setScale(joint->Animatedscale); - joint->LocalAnimatedMatrix *= scaleMatrix; - */ - - // -------- joint->LocalAnimatedMatrix *= scaleMatrix ----------------- - core::matrix4 &mat = joint->LocalAnimatedMatrix; - mat[0] *= joint->Animatedscale.X; - mat[1] *= joint->Animatedscale.X; - mat[2] *= joint->Animatedscale.X; - mat[3] *= joint->Animatedscale.X; - mat[4] *= joint->Animatedscale.Y; - mat[5] *= joint->Animatedscale.Y; - mat[6] *= joint->Animatedscale.Y; - mat[7] *= joint->Animatedscale.Y; - mat[8] *= joint->Animatedscale.Z; - mat[9] *= joint->Animatedscale.Z; - mat[10] *= joint->Animatedscale.Z; - mat[11] *= joint->Animatedscale.Z; - // ----------------------------------- - } - } else { - joint->LocalAnimatedMatrix = joint->LocalMatrix; + // rigid animation + for (u16 i = 0; i < AllJoints.size(); ++i) { + for (u32 j : AllJoints[i]->AttachedMeshes) { + auto box = (*SkinningBuffers)[j]->BoundingBox; + global_transforms[i].transformBoxEx(box); + result.addInternalBox(box); } } - SkinnedLastFrame = false; + return result; } -void SkinnedMesh::buildAllGlobalAnimatedMatrices(SJoint *joint, SJoint *parentJoint) +// Software Skinning + +void SkinnedMesh::skinMesh(const std::vector &global_matrices) { - if (!joint) { - for (auto *rootJoint : RootJoints) - buildAllGlobalAnimatedMatrices(rootJoint, 0); - return; - } else { - // Find global matrix... - if (!parentJoint || joint->GlobalSkinningSpace) - joint->GlobalAnimatedMatrix = joint->LocalAnimatedMatrix; - else - joint->GlobalAnimatedMatrix = parentJoint->GlobalAnimatedMatrix * joint->LocalAnimatedMatrix; - } - - for (auto *childJoint : joint->Children) - buildAllGlobalAnimatedMatrices(childJoint, joint); -} - -//-------------------------------------------------------------------------- -// Software Skinning -//-------------------------------------------------------------------------- - -//! Preforms a software skin on this mesh based of joint positions -void SkinnedMesh::skinMesh() -{ - if (!HasAnimation || SkinnedLastFrame) + if (!HasAnimation) return; - //---------------- - // This is marked as "Temp!". A shiny dubloon to whomever can tell me why. - buildAllGlobalAnimatedMatrices(); - //----------------- - - SkinnedLastFrame = true; - if (!HardwareSkinning) { - // rigid animation - for (auto *joint : AllJoints) { - for (u32 attachedMeshIdx : joint->AttachedMeshes) { - SSkinMeshBuffer *Buffer = (*SkinningBuffers)[attachedMeshIdx]; - Buffer->Transformation = joint->GlobalAnimatedMatrix; - } + // rigid animation + for (size_t i = 0; i < AllJoints.size(); ++i) { + auto *joint = AllJoints[i]; + for (u32 attachedMeshIdx : joint->AttachedMeshes) { + SSkinMeshBuffer *Buffer = (*SkinningBuffers)[attachedMeshIdx]; + Buffer->Transformation = global_matrices[i]; } - - // clear skinning helper array - for (std::vector &buf : Vertices_Moved) - std::fill(buf.begin(), buf.end(), false); - - // skin starting with the root joints - for (auto *rootJoint : RootJoints) - skinJoint(rootJoint, 0); - - for (auto *buffer : *SkinningBuffers) - buffer->setDirty(EBT_VERTEX); } - updateBoundingBox(); -} -void SkinnedMesh::skinJoint(SJoint *joint, SJoint *parentJoint) -{ - if (joint->Weights.size()) { - // Find this joints pull on vertices... + // clear skinning helper array + for (std::vector &buf : Vertices_Moved) + std::fill(buf.begin(), buf.end(), false); + + // skin starting with the root joints + for (size_t i = 0; i < AllJoints.size(); ++i) { + auto *joint = AllJoints[i]; + if (joint->Weights.empty()) + continue; + + // Find this joints pull on vertices // Note: It is assumed that the global inversed matrix has been calculated at this point. - core::matrix4 jointVertexPull = joint->GlobalAnimatedMatrix * joint->GlobalInversedMatrix.value(); + core::matrix4 jointVertexPull = global_matrices[i] * joint->GlobalInversedMatrix.value(); core::vector3df thisVertexMove, thisNormalMove; auto &buffersUsed = *SkinningBuffers; - // Skin Vertices Positions and Normals... + // Skin Vertices, Positions and Normals for (const auto &weight : joint->Weights) { // Pull this vertex... jointVertexPull.transformVect(thisVertexMove, weight.StaticPos); @@ -251,14 +153,11 @@ void SkinnedMesh::skinJoint(SJoint *joint, SJoint *parentJoint) //*(weight._Pos) += thisVertexMove * weight.strength; } - - buffersUsed[weight.buffer_id]->boundingBoxNeedsRecalculated(); } } - // Skin all children - for (auto *childJoint : joint->Children) - skinJoint(childJoint, joint); + for (auto *buffer : *SkinningBuffers) + buffer->setDirty(EBT_VERTEX); } //! Gets joint count. @@ -310,7 +209,7 @@ IMeshBuffer *SkinnedMesh::getMeshBuffer(const video::SMaterial &material) const if (LocalBuffers[i]->getMaterial() == material) return LocalBuffers[i]; } - return 0; + return nullptr; } u32 SkinnedMesh::getTextureSlot(u32 meshbufNr) const @@ -337,29 +236,6 @@ void SkinnedMesh::setDirty(E_BUFFER_TYPE buffer) LocalBuffers[i]->setDirty(buffer); } -//! (This feature is not implemented in irrlicht yet) -bool SkinnedMesh::setHardwareSkinning(bool on) -{ - if (HardwareSkinning != on) { - if (on) { - - // set mesh to static pose... - for (auto *joint : AllJoints) { - for (const auto &weight : joint->Weights) { - const u16 buffer_id = weight.buffer_id; - const u32 vertex_id = weight.vertex_id; - LocalBuffers[buffer_id]->getVertex(vertex_id)->Pos = weight.StaticPos; - LocalBuffers[buffer_id]->getVertex(vertex_id)->Normal = weight.StaticNormal; - LocalBuffers[buffer_id]->boundingBoxNeedsRecalculated(); - } - } - } - - HardwareSkinning = on; - } - return HardwareSkinning; -} - void SkinnedMesh::refreshJointCache() { // copy cache from the mesh... @@ -384,113 +260,192 @@ void SkinnedMesh::resetAnimation() LocalBuffers[buffer_id]->getVertex(vertex_id)->Normal = weight.StaticNormal; } } - SkinnedLastFrame = false; - LastAnimatedFrame = -1; } -void SkinnedMesh::calculateGlobalMatrices(SJoint *joint, SJoint *parentJoint) +//! Turns the given array of local matrices into an array of global matrices +//! by multiplying with respective parent matrices. +void SkinnedMesh::calculateGlobalMatrices(std::vector &matrices) const { - if (!joint && parentJoint) // bit of protection from endless loops - return; - - // Go through the root bones - if (!joint) { - for (auto *rootJoint : RootJoints) - calculateGlobalMatrices(rootJoint, nullptr); - return; + // Note that the joints are topologically sorted. + for (u16 i = 0; i < AllJoints.size(); ++i) { + if (auto parent_id = AllJoints[i]->ParentJointID) { + matrices[i] = matrices[*parent_id] * matrices[i]; + } } - - if (!parentJoint) - joint->GlobalMatrix = joint->LocalMatrix; - else - joint->GlobalMatrix = parentJoint->GlobalMatrix * joint->LocalMatrix; - - joint->LocalAnimatedMatrix = joint->LocalMatrix; - joint->GlobalAnimatedMatrix = joint->GlobalMatrix; - - if (!joint->GlobalInversedMatrix.has_value()) { // might be pre calculated - joint->GlobalInversedMatrix = joint->GlobalMatrix; - joint->GlobalInversedMatrix->makeInverse(); // slow - } - - for (auto *childJoint : joint->Children) - calculateGlobalMatrices(childJoint, joint); - SkinnedLastFrame = false; } -void SkinnedMesh::checkForAnimation() +bool SkinnedMesh::checkForAnimation() const { - // Check for animation... - HasAnimation = false; for (auto *joint : AllJoints) { if (!joint->keys.empty()) { - HasAnimation = true; - break; + return true; } } - // meshes with weights, are still counted as animated for ragdolls, etc - if (!HasAnimation) { - for (auto *joint : AllJoints) { - if (joint->Weights.size()) { - HasAnimation = true; - break; + // meshes with weights are animatable + for (auto *joint : AllJoints) { + if (!joint->Weights.empty()) { + return true; + } + } + + return false; +} + +void SkinnedMesh::prepareForSkinning() +{ + HasAnimation = checkForAnimation(); + if (!HasAnimation || PreparedForSkinning) + return; + + PreparedForSkinning = true; + + EndFrame = 0.0f; + for (const auto *joint : AllJoints) { + EndFrame = std::max(EndFrame, joint->keys.getEndFrame()); + } + + for (auto *joint : AllJoints) { + for (auto &weight : joint->Weights) { + const u16 buffer_id = weight.buffer_id; + const u32 vertex_id = weight.vertex_id; + + // check for invalid ids + if (buffer_id >= LocalBuffers.size()) { + os::Printer::log("Skinned Mesh: Weight buffer id too large", ELL_WARNING); + weight.buffer_id = weight.vertex_id = 0; + } else if (vertex_id >= LocalBuffers[buffer_id]->getVertexCount()) { + os::Printer::log("Skinned Mesh: Weight vertex id too large", ELL_WARNING); + weight.buffer_id = weight.vertex_id = 0; } } } - if (HasAnimation) { - EndFrame = 0.0f; - for (const auto *joint : AllJoints) { - EndFrame = std::max(EndFrame, joint->keys.getEndFrame()); + for (u32 i = 0; i < Vertices_Moved.size(); ++i) + for (u32 j = 0; j < Vertices_Moved[i].size(); ++j) + Vertices_Moved[i][j] = false; + + // For skinning: cache weight values for speed + for (auto *joint : AllJoints) { + for (auto &weight : joint->Weights) { + const u16 buffer_id = weight.buffer_id; + const u32 vertex_id = weight.vertex_id; + + weight.Moved = &Vertices_Moved[buffer_id][vertex_id]; + weight.StaticPos = LocalBuffers[buffer_id]->getVertex(vertex_id)->Pos; + weight.StaticNormal = LocalBuffers[buffer_id]->getVertex(vertex_id)->Normal; } } - if (HasAnimation && !PreparedForSkinning) { - PreparedForSkinning = true; + normalizeWeights(); - // check for bugs: - for (auto *joint : AllJoints) { - for (auto &weight : joint->Weights) { - const u16 buffer_id = weight.buffer_id; - const u32 vertex_id = weight.vertex_id; + for (auto *joint : AllJoints) { + joint->keys.cleanup(); + } +} - // check for invalid ids - if (buffer_id >= LocalBuffers.size()) { - os::Printer::log("Skinned Mesh: Weight buffer id too large", ELL_WARNING); - weight.buffer_id = weight.vertex_id = 0; - } else if (vertex_id >= LocalBuffers[buffer_id]->getVertexCount()) { - os::Printer::log("Skinned Mesh: Weight vertex id too large", ELL_WARNING); - weight.buffer_id = weight.vertex_id = 0; +void SkinnedMesh::calculateStaticBoundingBox() +{ + std::vector> animated(getMeshBufferCount()); + for (u32 mb = 0; mb < getMeshBufferCount(); mb++) + animated[mb] = std::vector(getMeshBuffer(mb)->getVertexCount()); + + for (auto *joint : AllJoints) { + for (auto &weight : joint->Weights) { + const u16 buffer_id = weight.buffer_id; + const u32 vertex_id = weight.vertex_id; + animated[buffer_id][vertex_id] = true; + } + } + + bool first = true; + for (u16 mb = 0; mb < getMeshBufferCount(); mb++) { + for (u32 v = 0; v < getMeshBuffer(mb)->getVertexCount(); v++) { + if (!animated[mb][v]) { + auto pos = getMeshBuffer(mb)->getVertexBuffer()->getPosition(v); + if (!first) { + StaticPartsBox.addInternalPoint(pos); + } else { + StaticPartsBox.reset(pos); + first = false; } } } + } +} - // An array used in skinning - - for (u32 i = 0; i < Vertices_Moved.size(); ++i) - for (u32 j = 0; j < Vertices_Moved[i].size(); ++j) - Vertices_Moved[i][j] = false; - - // For skinning: cache weight values for speed - - for (auto *joint : AllJoints) { - for (auto &weight : joint->Weights) { - const u16 buffer_id = weight.buffer_id; - const u32 vertex_id = weight.vertex_id; - - weight.Moved = &Vertices_Moved[buffer_id][vertex_id]; - weight.StaticPos = LocalBuffers[buffer_id]->getVertex(vertex_id)->Pos; - weight.StaticNormal = LocalBuffers[buffer_id]->getVertex(vertex_id)->Normal; - - // weight._Pos=&Buffers[buffer_id]->getVertex(vertex_id)->Pos; +void SkinnedMesh::calculateJointBoundingBoxes() +{ + for (auto *joint : AllJoints) { + bool first = true; + for (auto &weight : joint->Weights) { + if (weight.strength < 1e-6) + continue; + auto pos = weight.StaticPos; + joint->GlobalInversedMatrix.value().transformVect(pos); + if (!first) { + joint->LocalBoundingBox.addInternalPoint(pos); + } else { + joint->LocalBoundingBox.reset(pos); + first = false; } } - - // normalize weights - normalizeWeights(); } - SkinnedLastFrame = false; +} + +void SkinnedMesh::calculateBufferBoundingBoxes() +{ + for (u32 j = 0; j < LocalBuffers.size(); ++j) { + // If we use skeletal animation, this will just be a bounding box of the static pose; + // if we use rigid animation, this will correctly transform the points first. + LocalBuffers[j]->recalculateBoundingBox(); + } +} + +void SkinnedMesh::recalculateBaseBoundingBoxes() { + calculateStaticBoundingBox(); + calculateJointBoundingBoxes(); + calculateBufferBoundingBoxes(); +} + +void SkinnedMesh::topoSortJoints() +{ + size_t n = AllJoints.size(); + + std::vector new_to_old_id; + + std::vector> children(n); + for (u16 i = 0; i < n; ++i) { + if (auto parentId = AllJoints[i]->ParentJointID) + children[*parentId].push_back(i); + else + new_to_old_id.push_back(i); + } + + // Levelorder + for (u16 i = 0; i < n; ++i) { + new_to_old_id.insert(new_to_old_id.end(), + children[new_to_old_id[i]].begin(), + children[new_to_old_id[i]].end()); + } + + std::vector old_to_new_id(n); + for (u16 i = 0; i < n; ++i) + old_to_new_id[new_to_old_id[i]] = i; + + std::vector joints(n); + for (u16 i = 0; i < n; ++i) { + joints[i] = AllJoints[new_to_old_id[i]]; + joints[i]->JointID = i; + if (auto parentId = joints[i]->ParentJointID) + joints[i]->ParentJointID = old_to_new_id[*parentId]; + } + AllJoints = std::move(joints); + + for (u16 i = 0; i < n; ++i) { + if (auto pjid = AllJoints[i]->ParentJointID) + assert(*pjid < i); + } } //! called by loader after populating with mesh and bone data @@ -498,98 +453,44 @@ SkinnedMesh *SkinnedMeshBuilder::finalize() { os::Printer::log("Skinned Mesh - finalize", ELL_DEBUG); - // Make sure we recalc the next frame - LastAnimatedFrame = -1; - SkinnedLastFrame = false; - - // calculate bounding box - for (auto *buffer : LocalBuffers) { - buffer->recalculateBoundingBox(); - } - - if (AllJoints.size() || RootJoints.size()) { - // populate AllJoints or RootJoints, depending on which is empty - if (RootJoints.empty()) { - - for (auto *joint : AllJoints) { - - bool foundParent = false; - for (const auto *parentJoint : AllJoints) { - for (const auto *childJoint : parentJoint->Children) { - if (childJoint == joint) - foundParent = true; - } - } - - if (!foundParent) - RootJoints.push_back(joint); - } - } else { - AllJoints = RootJoints; - } - } - - // Set array sizes... + topoSortJoints(); + // Set array sizes for (u32 i = 0; i < LocalBuffers.size(); ++i) { Vertices_Moved.emplace_back(LocalBuffers[i]->getVertexCount()); } - checkForAnimation(); + prepareForSkinning(); - if (HasAnimation) { - for (auto *joint : AllJoints) { - joint->keys.cleanup(); - } - } - - // Needed for animation and skinning... - - calculateGlobalMatrices(0, 0); - - // rigid animation for non animated meshes + std::vector matrices; + matrices.reserve(AllJoints.size()); for (auto *joint : AllJoints) { + if (const auto *matrix = std::get_if(&joint->transform)) + matrices.push_back(*matrix); + else + matrices.push_back(std::get(joint->transform).buildMatrix()); + } + calculateGlobalMatrices(matrices); + + for (size_t i = 0; i < AllJoints.size(); ++i) { + auto *joint = AllJoints[i]; + if (!joint->GlobalInversedMatrix) { + joint->GlobalInversedMatrix = matrices[i]; + joint->GlobalInversedMatrix->makeInverse(); + } + // rigid animation for non animated meshes for (u32 attachedMeshIdx : joint->AttachedMeshes) { SSkinMeshBuffer *Buffer = (*SkinningBuffers)[attachedMeshIdx]; - Buffer->Transformation = joint->GlobalAnimatedMatrix; + Buffer->Transformation = matrices[i]; } } - // calculate bounding box - if (LocalBuffers.empty()) - BoundingBox.reset(0, 0, 0); - else { - irr::core::aabbox3df bb(LocalBuffers[0]->BoundingBox); - LocalBuffers[0]->Transformation.transformBoxEx(bb); - BoundingBox.reset(bb); - - for (u32 j = 1; j < LocalBuffers.size(); ++j) { - bb = LocalBuffers[j]->BoundingBox; - LocalBuffers[j]->Transformation.transformBoxEx(bb); - - BoundingBox.addInternalBox(bb); - } - } + recalculateBaseBoundingBoxes(); + StaticPoseBox = calculateBoundingBox(matrices); return this; } -void SkinnedMesh::updateBoundingBox() -{ - if (!SkinningBuffers) - return; - - BoundingBox.reset(0, 0, 0); - - for (auto *buffer : *SkinningBuffers) { - buffer->recalculateBoundingBox(); - core::aabbox3df bb = buffer->BoundingBox; - buffer->Transformation.transformBoxEx(bb); - - BoundingBox.addInternalBox(bb); - } -} - scene::SSkinMeshBuffer *SkinnedMeshBuilder::addMeshBuffer() { scene::SSkinMeshBuffer *buffer = new scene::SSkinMeshBuffer(); @@ -607,14 +508,10 @@ void SkinnedMeshBuilder::addMeshBuffer(SSkinMeshBuffer *meshbuf) SkinnedMesh::SJoint *SkinnedMeshBuilder::addJoint(SJoint *parent) { SJoint *joint = new SJoint; + joint->setParent(parent); + joint->JointID = AllJoints.size(); AllJoints.push_back(joint); - if (!parent) { - // Add root joints to array in finalize() - } else { - // Set parent (Be careful of the mesh loader also setting the parent) - parent->Children.push_back(joint); - } return joint; } @@ -684,73 +581,6 @@ void SkinnedMesh::normalizeWeights() } } -void SkinnedMesh::recoverJointsFromMesh(std::vector &jointChildSceneNodes) -{ - for (u32 i = 0; i < AllJoints.size(); ++i) { - IBoneSceneNode *node = jointChildSceneNodes[i]; - SJoint *joint = AllJoints[i]; - node->setPosition(joint->LocalAnimatedMatrix.getTranslation()); - node->setRotation(joint->LocalAnimatedMatrix.getRotationDegrees()); - node->setScale(joint->LocalAnimatedMatrix.getScale()); - - node->updateAbsolutePosition(); - } -} - -void SkinnedMesh::transferJointsToMesh(const std::vector &jointChildSceneNodes) -{ - for (u32 i = 0; i < AllJoints.size(); ++i) { - const IBoneSceneNode *const node = jointChildSceneNodes[i]; - SJoint *joint = AllJoints[i]; - - joint->LocalAnimatedMatrix.setRotationDegrees(node->getRotation()); - joint->LocalAnimatedMatrix.setTranslation(node->getPosition()); - joint->LocalAnimatedMatrix *= core::matrix4().setScale(node->getScale()); - - joint->GlobalSkinningSpace = (node->getSkinningSpace() == EBSS_GLOBAL); - } - // Make sure we recalc the next frame - LastAnimatedFrame = -1; - SkinnedLastFrame = false; -} - -void SkinnedMesh::addJoints(std::vector &jointChildSceneNodes, - IAnimatedMeshSceneNode *node, ISceneManager *smgr) -{ - // Create new joints - for (u32 i = 0; i < AllJoints.size(); ++i) { - jointChildSceneNodes.push_back(new CBoneSceneNode(0, smgr, 0, i, AllJoints[i]->Name)); - } - - // Match up parents - for (u32 i = 0; i < jointChildSceneNodes.size(); ++i) { - const SJoint *const joint = AllJoints[i]; // should be fine - - s32 parentID = -1; - - for (u32 j = 0; (parentID == -1) && (j < AllJoints.size()); ++j) { - if (i != j) { - const SJoint *const parentTest = AllJoints[j]; - for (u32 n = 0; n < parentTest->Children.size(); ++n) { - if (parentTest->Children[n] == joint) { - parentID = j; - break; - } - } - } - } - - IBoneSceneNode *bone = jointChildSceneNodes[i]; - if (parentID != -1) - bone->setParent(jointChildSceneNodes[parentID]); - else - bone->setParent(node); - - bone->drop(); - } - SkinnedLastFrame = false; -} - void SkinnedMesh::convertMeshToTangents() { // now calculate tangents diff --git a/lib/tiniergltf/tiniergltf.hpp b/lib/tiniergltf/tiniergltf.hpp index 06e2f53566..81ac10ec8f 100644 --- a/lib/tiniergltf/tiniergltf.hpp +++ b/lib/tiniergltf/tiniergltf.hpp @@ -916,12 +916,7 @@ struct Node { std::optional skin; std::optional> weights; Node(const Json::Value &o) - : transform(Matrix { - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1 - }) + : transform(TRS{}) { check(o.isObject()); if (o.isMember("camera")) { diff --git a/src/client/content_cao.cpp b/src/client/content_cao.cpp index e0ac5fff0e..ecfe0f2de8 100644 --- a/src/client/content_cao.cpp +++ b/src/client/content_cao.cpp @@ -178,15 +178,6 @@ static void setBillboardTextureMatrix(scene::IBillboardSceneNode *bill, matrix.setTextureScale(txs, tys); } -// Evaluate transform chain recursively; irrlicht does not do this for us -static void updatePositionRecursive(scene::ISceneNode *node) -{ - scene::ISceneNode *parent = node->getParent(); - if (parent) - updatePositionRecursive(parent); - node->updateAbsolutePosition(); -} - static bool logOnce(const std::ostringstream &from, std::ostream &log_to) { thread_local std::vector logged; @@ -682,7 +673,6 @@ void GenericCAO::addToScene(ITextureSource *tsrc, scene::ISceneManager *smgr) m_animated_meshnode = m_smgr->addAnimatedMeshSceneNode(mesh, m_matrixnode); m_animated_meshnode->grab(); mesh->drop(); // The scene node took hold of it - m_animated_meshnode->animateJoints(); // Needed for some animations m_animated_meshnode->setScale(m_prop.visual_size); // set vertex colors to ensure alpha is set @@ -693,6 +683,21 @@ void GenericCAO::addToScene(ITextureSource *tsrc, scene::ISceneManager *smgr) m_animated_meshnode->forEachMaterial([this] (auto &mat) { mat.BackfaceCulling = m_prop.backface_culling; }); + + m_animated_meshnode->setOnAnimateCallback([&](f32 dtime) { + for (auto &it : m_bone_override) { + auto* bone = m_animated_meshnode->getJointNode(it.first.c_str()); + if (!bone) + continue; + + BoneOverride &props = it.second; + props.dtime_passed += dtime; + + bone->setPosition(props.getPosition(bone->getPosition())); + bone->setRotation(props.getRotationEulerDeg(bone->getRotation())); + bone->setScale(props.getScale(bone->getScale())); + } + }); } else errorstream<<"GenericCAO::addToScene(): Could not load mesh "<updateAbsolutePosition(); - m_animated_meshnode->animateJoints(); - updateBones(dtime); - } } static void setMeshBufferTextureCoords(scene::IMeshBuffer *buf, const v2f *uv, u32 count) @@ -1394,44 +1386,6 @@ void GenericCAO::updateAnimationSpeed() m_animated_meshnode->setAnimationSpeed(m_animation_speed); } -void GenericCAO::updateBones(f32 dtime) -{ - if (!m_animated_meshnode) - return; - if (m_bone_override.empty()) { - m_animated_meshnode->setJointMode(scene::EJUOR_NONE); - return; - } - - m_animated_meshnode->setJointMode(scene::EJUOR_CONTROL); // To write positions to the mesh on render - for (auto &it : m_bone_override) { - std::string bone_name = it.first; - scene::IBoneSceneNode* bone = m_animated_meshnode->getJointNode(bone_name.c_str()); - if (!bone) - continue; - - BoneOverride &props = it.second; - props.dtime_passed += dtime; - - bone->setPosition(props.getPosition(bone->getPosition())); - bone->setRotation(props.getRotationEulerDeg(bone->getRotation())); - bone->setScale(props.getScale(bone->getScale())); - } - - // The following is needed for set_bone_pos to propagate to - // attached objects correctly. - // Irrlicht ought to do this, but doesn't when using EJUOR_CONTROL. - for (u32 i = 0; i < m_animated_meshnode->getJointCount(); ++i) { - auto bone = m_animated_meshnode->getJointNode(i); - // Look for the root bone. - if (bone && bone->getParent() == m_animated_meshnode) { - // Update entire skeleton. - bone->updateAbsolutePositionOfAllChildren(); - break; - } - } -} - void GenericCAO::updateAttachments() { ClientActiveObject *parent = getParent(); @@ -1747,7 +1701,6 @@ void GenericCAO::processMessage(const std::string &data) } else { m_bone_override[bone] = props; } - // updateBones(); now called every step } else if (cmd == AO_CMD_ATTACH_TO) { u16 parent_id = readS16(is); std::string bone = deSerializeString16(is); diff --git a/src/client/content_cao.h b/src/client/content_cao.h index 9762684f61..2e765ab6e7 100644 --- a/src/client/content_cao.h +++ b/src/client/content_cao.h @@ -286,8 +286,6 @@ public: void updateAnimationSpeed(); - void updateBones(f32 dtime); - void processMessage(const std::string &data) override; bool directReportPunch(v3f dir, const ItemStack *punchitem, diff --git a/src/client/mesh.cpp b/src/client/mesh.cpp index 808dcdd181..45b665bba5 100644 --- a/src/client/mesh.cpp +++ b/src/client/mesh.cpp @@ -10,10 +10,9 @@ #include #include #include -#include #include #include "S3DVertex.h" -#include "SMesh.h" +#include #include "SMeshBuffer.h" inline static void applyShadeFactor(video::SColor& color, float factor) @@ -97,11 +96,8 @@ scene::IAnimatedMesh* createCubeMesh(v3f scale) mesh->addMeshBuffer(buf); buf->drop(); } - - scene::SAnimatedMesh *anim_mesh = new scene::SAnimatedMesh(mesh); - mesh->drop(); - scaleMesh(anim_mesh, scale); // also recalculates bounding box - return anim_mesh; + scaleMesh(mesh, scale); // also recalculates bounding box + return mesh; } template diff --git a/src/nodedef.cpp b/src/nodedef.cpp index 04f1959c14..9d54c39579 100644 --- a/src/nodedef.cpp +++ b/src/nodedef.cpp @@ -4,7 +4,6 @@ #include "nodedef.h" -#include "SAnimatedMesh.h" #include "itemdef.h" #if CHECK_CLIENT_BUILD() #include "client/mesh.h" @@ -964,13 +963,6 @@ void ContentFeatures::updateTextures(ITextureSource *tsrc, IShaderSource *shdsrc // Note: By freshly reading, we get an unencumbered mesh. if (scene::IMesh *src_mesh = client->getMesh(mesh)) { bool apply_bs = false; - // For frame-animated meshes, always get the first frame, - // which holds a model for which we can eventually get the static pose. - while (auto *src_meshes = dynamic_cast(src_mesh)) { - src_mesh = src_meshes->getMesh(0.0f); - src_mesh->grab(); - src_meshes->drop(); - } if (auto *skinned_mesh = dynamic_cast(src_mesh)) { // Compatibility: Animated meshes, as well as static gltf meshes, are not scaled by BS. // See https://github.com/luanti-org/luanti/pull/16112#issuecomment-2881860329 diff --git a/src/unittest/CMakeLists.txt b/src/unittest/CMakeLists.txt index 2a38af68d0..159e816c74 100644 --- a/src/unittest/CMakeLists.txt +++ b/src/unittest/CMakeLists.txt @@ -59,6 +59,8 @@ set (UNITTEST_CLIENT_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/test_eventmanager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_gameui.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_irr_gltf_mesh_loader.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test_irr_x_mesh_loader.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test_irr_matrix4.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_mesh_compare.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_keycode.cpp PARENT_SCOPE) diff --git a/src/unittest/test_irr_gltf_mesh_loader.cpp b/src/unittest/test_irr_gltf_mesh_loader.cpp index f689838db1..6cd6f0f1de 100644 --- a/src/unittest/test_irr_gltf_mesh_loader.cpp +++ b/src/unittest/test_irr_gltf_mesh_loader.cpp @@ -394,36 +394,40 @@ SECTION("simple skin") const auto joints = csm->getAllJoints(); REQUIRE(joints.size() == 3); - const auto findJoint = [&](const std::function &predicate) { - for (std::size_t i = 0; i < joints.size(); ++i) { - if (predicate(joints[i])) { - return joints[i]; + const auto findJoint = [&](const std::function &predicate) { + for (const auto *joint : joints) { + if (predicate(joint)) { + return joint; } } throw std::runtime_error("joint not found"); }; // Check the node hierarchy - const auto parent = findJoint([](auto joint) { - return !joint->Children.empty(); + const auto child = findJoint([&](auto *joint) { + return !!joint->ParentJointID; }); - REQUIRE(parent->Children.size() == 1); - const auto child = parent->Children[0]; - REQUIRE(child != parent); + const auto *parent = joints.at(*child->ParentJointID); SECTION("transformations are correct") { - CHECK(parent->Animatedposition == v3f(0, 0, 0)); - CHECK(parent->Animatedrotation == irr::core::quaternion()); - CHECK(parent->Animatedscale == v3f(1, 1, 1)); - CHECK(parent->GlobalInversedMatrix == irr::core::matrix4()); - const v3f childTranslation(0, 1, 0); - CHECK(child->Animatedposition == childTranslation); - CHECK(child->Animatedrotation == irr::core::quaternion()); - CHECK(child->Animatedscale == v3f(1, 1, 1)); - irr::core::matrix4 inverseBindMatrix; - inverseBindMatrix.setTranslation(-childTranslation); - CHECK(child->GlobalInversedMatrix == inverseBindMatrix); + { + const auto &transform = std::get(parent->transform); + CHECK(transform.translation == v3f(0, 0, 0)); + CHECK(transform.rotation == irr::core::quaternion()); + CHECK(transform.scale == v3f(1, 1, 1)); + CHECK(parent->GlobalInversedMatrix == irr::core::matrix4()); + } + { + const auto &transform = std::get(child->transform); + const v3f translation(0, 1, 0); + CHECK(transform.translation == translation); + CHECK(transform.rotation == irr::core::quaternion()); + CHECK(transform.scale == v3f(1, 1, 1)); + irr::core::matrix4 inverseBindMatrix; + inverseBindMatrix.setTranslation(-translation); + CHECK(child->GlobalInversedMatrix == inverseBindMatrix); + } } SECTION("weights are correct") diff --git a/src/unittest/test_irr_x_mesh_loader.cpp b/src/unittest/test_irr_x_mesh_loader.cpp new file mode 100644 index 0000000000..cb61ce7da8 --- /dev/null +++ b/src/unittest/test_irr_x_mesh_loader.cpp @@ -0,0 +1,111 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "catch_amalgamated.hpp" +#include "content/subgames.h" +#include "filesys.h" + +#include "irrlichttypes.h" +#include "irr_ptr.h" + +#include "EDriverTypes.h" +#include "IFileSystem.h" +#include "IReadFile.h" +#include "ISceneManager.h" +#include "SkinnedMesh.h" +#include "irrlicht.h" + +#include "catch.h" +#include "matrix4.h" + +TEST_CASE("x") { + +const auto gamespec = findSubgame("devtest"); + +if (!gamespec.isValid()) + SKIP(); + +irr::SIrrlichtCreationParameters p; +p.DriverType = video::EDT_NULL; +auto *driver = irr::createDeviceEx(p); + +REQUIRE(driver); + +auto *smgr = driver->getSceneManager(); +const auto loadMesh = [&] (const io::path& filepath) { + irr_ptr file(driver->getFileSystem()->createAndOpenFile(filepath)); + REQUIRE(file); + return smgr->getMesh(file.get()); +}; + +const static auto model_stem = gamespec.gamemods_path + + DIR_DELIM + "testentities" + DIR_DELIM + "models" + DIR_DELIM + "testentities_"; + +SECTION("cool guy") { + const auto *mesh = dynamic_cast(loadMesh(model_stem + "cool_guy.x")); + REQUIRE(mesh); + REQUIRE(mesh->getMeshBufferCount() == 1); + + auto getJointId = [&](auto name) { + return mesh->getJointNumber(name).value(); + }; + + const auto root = getJointId("Root"); + const auto armature = getJointId("Armature"); + const auto armature_body = getJointId("Armature_body"); + const auto armature_arm_r = getJointId("Armature_arm_r"); + + std::vector matrices; + matrices.reserve(mesh->getJointCount()); + for (auto *joint : mesh->getAllJoints()) { + if (const auto *matrix = std::get_if(&joint->transform)) + matrices.push_back(*matrix); + else + matrices.push_back(std::get(joint->transform).buildMatrix()); + } + auto local_matrices = matrices; + mesh->calculateGlobalMatrices(matrices); + + SECTION("joints are topologically sorted") { + REQUIRE(root < armature); + REQUIRE(armature < armature_body); + REQUIRE(armature_body < armature_arm_r); + } + + SECTION("parents are correct") { + const auto get_parent = [&](auto id) { + return mesh->getAllJoints()[id]->ParentJointID; + }; + REQUIRE(!get_parent(root)); + REQUIRE(get_parent(armature).value() == root); + REQUIRE(get_parent(armature_body).value() == armature); + REQUIRE(get_parent(armature_arm_r).value() == armature_body); + } + + SECTION("local matrices are correct") { + REQUIRE(local_matrices[root].equals(core::IdentityMatrix)); + REQUIRE(local_matrices[armature].equals(core::IdentityMatrix)); + REQUIRE(local_matrices[armature_body] == core::matrix4( + -1,0,0,0, + 0,0,1,0, + 0,1,0,0, + 0,2.571201,0,1 + )); + REQUIRE(local_matrices[armature_arm_r] == core::matrix4( + -0.047733,0.997488,-0.05233,0, + 0.901521,0.020464,-0.432251,0, + -0.430095,-0.067809,-0.900233, + 0,-0.545315,0,1,1 + )); + } + + SECTION("global matrices are correct") { + REQUIRE(matrices[armature_body] == local_matrices[armature_body]); + REQUIRE(matrices[armature_arm_r] == + matrices[armature_body] * local_matrices[armature_arm_r]); + } +} + +driver->closeDevice(); +driver->drop(); +} From 4454d71d7dca852dd16db01bc1bea8336c6d16ce Mon Sep 17 00:00:00 2001 From: siliconsniffer <97843108+siliconsniffer@users.noreply.github.com> Date: Sat, 7 Jun 2025 14:41:29 +0200 Subject: [PATCH 05/31] Formspec: change tabs with ctrl(+shift)+tab (#16167) This change makes it easier to go to the next/previous tab using keyboard controls. --- src/gui/guiFormSpecMenu.cpp | 25 +++++++++++++++++++++++++ src/gui/guiTable.cpp | 4 +++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/gui/guiFormSpecMenu.cpp b/src/gui/guiFormSpecMenu.cpp index 97d86b05f8..38193684f0 100644 --- a/src/gui/guiFormSpecMenu.cpp +++ b/src/gui/guiFormSpecMenu.cpp @@ -4068,6 +4068,31 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event) { if (event.EventType==EET_KEY_INPUT_EVENT) { KeyPress kp(event.KeyInput); + // Ctrl (+ Shift) + Tab: Select the (previous or) next tab of a TabControl instance. + bool shift = event.KeyInput.Shift; + bool ctrl = event.KeyInput.Control; + if (event.KeyInput.PressedDown && (event.KeyInput.Key == KEY_TAB && ctrl)) { + // Try to find a tab control among our elements + for (const FieldSpec &s : m_fields) { + if (s.ftype != f_TabHeader) + continue; + + IGUIElement *element = getElementFromId(s.fid, true); + if (!element || element->getType() != gui::EGUIET_TAB_CONTROL) + continue; + + gui::IGUITabControl *tabs = static_cast(element); + s32 num_tabs = tabs->getTabCount(); + if (num_tabs <= 1) + continue; + + s32 active = tabs->getActiveTab(); + // Shift: Previous tab, No shift: Next tab + active = (active + (shift ? -1 : 1) + num_tabs) % num_tabs; + tabs->setActiveTab(active); + return true; // handled + } + } if (event.KeyInput.PressedDown && ( (kp == EscapeKey) || ((m_client != NULL) && (kp == getKeySetting("keymap_inventory"))))) { diff --git a/src/gui/guiTable.cpp b/src/gui/guiTable.cpp index c129c3c2bb..3f2613510a 100644 --- a/src/gui/guiTable.cpp +++ b/src/gui/guiTable.cpp @@ -831,8 +831,10 @@ bool GUITable::OnEvent(const SEvent &event) return true; } else if (event.KeyInput.Key == KEY_ESCAPE || - event.KeyInput.Key == KEY_SPACE) { + event.KeyInput.Key == KEY_SPACE || + (event.KeyInput.Key == KEY_TAB && event.KeyInput.Control)) { // pass to parent + return IGUIElement::OnEvent(event); } else if (event.KeyInput.PressedDown && event.KeyInput.Char) { // change selection based on text as it is typed From 29a9056731a3be850ab36207e28e765062b7c2b8 Mon Sep 17 00:00:00 2001 From: jordan4ibanez Date: Sat, 7 Jun 2025 08:42:14 -0400 Subject: [PATCH 06/31] Fix 'core.mod_channel_join' return value documentation (#16218) --- doc/lua_api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/lua_api.md b/doc/lua_api.md index 07f70c36fe..5a2e167073 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -6839,6 +6839,7 @@ You can find mod channels communication scheme in `doc/mod_channels.png`. * Server joins channel `channel_name`, and creates it if necessary. You should listen for incoming messages with `core.register_on_modchannel_message` + * This returns a [ModChannel] object. Inventory --------- From b9af44b1949a37cfbbba4890f988c8a71be1f1e8 Mon Sep 17 00:00:00 2001 From: Gwyndolyn Shafer <71569880+scoutgwyndolyn@users.noreply.github.com> Date: Sat, 7 Jun 2025 05:43:03 -0700 Subject: [PATCH 07/31] Remove redundant descriptions from key bindings (#16220) --- builtin/settingtypes.txt | 69 ++-------------------------------------- 1 file changed, 2 insertions(+), 67 deletions(-) diff --git a/builtin/settingtypes.txt b/builtin/settingtypes.txt index 17ecf5b34f..37cd7d5e2c 100644 --- a/builtin/settingtypes.txt +++ b/builtin/settingtypes.txt @@ -182,23 +182,17 @@ invert_hotbar_mouse_wheel (Hotbar: Invert mouse wheel direction) bool false [**Keybindings] -# Key for moving the player forward. keymap_forward (Move forward) key SYSTEM_SCANCODE_26 -# Key for moving the player backward. # Will also disable autoforward, when active. keymap_backward (Move backward) key SYSTEM_SCANCODE_22 -# Key for moving the player left. keymap_left (Move left) key SYSTEM_SCANCODE_4 -# Key for moving the player right. keymap_right (Move right) key SYSTEM_SCANCODE_7 -# Key for jumping. keymap_jump (Jump) key SYSTEM_SCANCODE_44 -# Key for sneaking. # Also used for climbing down and descending in water if aux1_descends is disabled. keymap_sneak (Sneak) key SYSTEM_SCANCODE_225 @@ -210,13 +204,11 @@ keymap_dig (Dig/punch/use) key KEY_LBUTTON # (Note: The actual meaning might vary on a per-game basis.) keymap_place (Place/use) key KEY_RBUTTON -# Key for opening the inventory. keymap_inventory (Open inventory) key SYSTEM_SCANCODE_12 # Key for moving fast in fast mode. keymap_aux1 (Aux1) key SYSTEM_SCANCODE_8 -# Key for opening the chat window. keymap_chat (Open chat) key SYSTEM_SCANCODE_23 # Key for opening the chat window to type commands. @@ -225,70 +217,48 @@ keymap_cmd (Command) key SYSTEM_SCANCODE_56 # Key for opening the chat window to type local commands. keymap_cmd_local (Local command) key SYSTEM_SCANCODE_55 -# Key for toggling unlimited view range. -keymap_rangeselect (Range select) key +keymap_rangeselect (Toggle unlimited view range) key -# Key for toggling flying. keymap_freemove (Toggle fly) key SYSTEM_SCANCODE_14 -# Key for toggling pitch move mode. keymap_pitchmove (Toggle pitchmove) key -# Key for toggling fast mode. keymap_fastmove (Toggle fast) key SYSTEM_SCANCODE_13 -# Key for toggling noclip mode. keymap_noclip (Toggle noclip) key SYSTEM_SCANCODE_11 -# Key for selecting the next item in the hotbar. keymap_hotbar_next (Hotbar: select next item) key SYSTEM_SCANCODE_17 -# Key for selecting the previous item in the hotbar. keymap_hotbar_previous (Hotbar: select previous item) key SYSTEM_SCANCODE_5 -# Key for muting the game. keymap_mute (Mute) key SYSTEM_SCANCODE_16 -# Key for increasing the volume. keymap_increase_volume (Increase volume) key -# Key for decreasing the volume. keymap_decrease_volume (Decrease volume) key -# Key for toggling autoforward. keymap_autoforward (Toggle automatic forward) key -# Key for toggling cinematic mode. keymap_cinematic (Toggle cinematic mode) key -# Key for toggling display of minimap. keymap_minimap (Toggle minimap) key SYSTEM_SCANCODE_25 -# Key for taking screenshots. keymap_screenshot (Screenshot) key SYSTEM_SCANCODE_69 -# Key for toggling fullscreen mode. keymap_fullscreen (Toggle fullscreen) key SYSTEM_SCANCODE_68 -# Key for dropping the currently selected item. keymap_drop (Drop item) key SYSTEM_SCANCODE_20 -# Key to use view zoom when possible. keymap_zoom (Zoom) key SYSTEM_SCANCODE_29 -# Key for toggling the display of the HUD. keymap_toggle_hud (Toggle HUD) key SYSTEM_SCANCODE_58 -# Key for toggling the display of chat. keymap_toggle_chat (Toggle chat log) key SYSTEM_SCANCODE_59 -# Key for toggling the display of the large chat console. -keymap_console (Large chat console) key SYSTEM_SCANCODE_67 +keymap_console (Toggle large chat console) key SYSTEM_SCANCODE_67 -# Key for toggling the display of fog. keymap_toggle_fog (Toggle fog) key SYSTEM_SCANCODE_60 -# Key for toggling the display of debug info. keymap_toggle_debug (Toggle debug info) key SYSTEM_SCANCODE_62 # Key for toggling the display of the profiler. Used for development. @@ -297,109 +267,74 @@ keymap_toggle_profiler (Toggle profiler) key SYSTEM_SCANCODE_63 # Key for toggling the display of mapblock boundaries. keymap_toggle_block_bounds (Toggle block bounds) key -# Key for switching between first- and third-person camera. keymap_camera_mode (Toggle camera mode) key SYSTEM_SCANCODE_6 -# Key for increasing the viewing range. keymap_increase_viewing_range_min (Increase view range) key SYSTEM_SCANCODE_46 -# Key for decreasing the viewing range. keymap_decrease_viewing_range_min (Decrease view range) key SYSTEM_SCANCODE_45 -# Key for selecting the first hotbar slot. keymap_slot1 (Hotbar slot 1) key SYSTEM_SCANCODE_30 -# Key for selecting the second hotbar slot. keymap_slot2 (Hotbar slot 2) key SYSTEM_SCANCODE_31 -# Key for selecting the third hotbar slot. keymap_slot3 (Hotbar slot 3) key SYSTEM_SCANCODE_32 -# Key for selecting the fourth hotbar slot. keymap_slot4 (Hotbar slot 4) key SYSTEM_SCANCODE_33 -# Key for selecting the fifth hotbar slot. keymap_slot5 (Hotbar slot 5) key SYSTEM_SCANCODE_34 -# Key for selecting the sixth hotbar slot. keymap_slot6 (Hotbar slot 6) key SYSTEM_SCANCODE_35 -# Key for selecting the seventh hotbar slot. keymap_slot7 (Hotbar slot 7) key SYSTEM_SCANCODE_36 -# Key for selecting the eighth hotbar slot. keymap_slot8 (Hotbar slot 8) key SYSTEM_SCANCODE_37 -# Key for selecting the ninth hotbar slot. keymap_slot9 (Hotbar slot 9) key SYSTEM_SCANCODE_38 -# Key for selecting the tenth hotbar slot. keymap_slot10 (Hotbar slot 10) key SYSTEM_SCANCODE_39 -# Key for selecting the 11th hotbar slot. keymap_slot11 (Hotbar slot 11) key -# Key for selecting the 12th hotbar slot. keymap_slot12 (Hotbar slot 12) key -# Key for selecting the 13th hotbar slot. keymap_slot13 (Hotbar slot 13) key -# Key for selecting the 14th hotbar slot. keymap_slot14 (Hotbar slot 14) key -# Key for selecting the 15th hotbar slot. keymap_slot15 (Hotbar slot 15) key -# Key for selecting the 16th hotbar slot. keymap_slot16 (Hotbar slot 16) key -# Key for selecting the 17th hotbar slot. keymap_slot17 (Hotbar slot 17) key -# Key for selecting the 18th hotbar slot. keymap_slot18 (Hotbar slot 18) key -# Key for selecting the 19th hotbar slot. keymap_slot19 (Hotbar slot 19) key -# Key for selecting the 20th hotbar slot. keymap_slot20 (Hotbar slot 20) key -# Key for selecting the 21st hotbar slot. keymap_slot21 (Hotbar slot 21) key -# Key for selecting the 22nd hotbar slot. keymap_slot22 (Hotbar slot 22) key -# Key for selecting the 23rd hotbar slot. keymap_slot23 (Hotbar slot 23) key -# Key for selecting the 24th hotbar slot. keymap_slot24 (Hotbar slot 24) key -# Key for selecting the 25th hotbar slot. keymap_slot25 (Hotbar slot 25) key -# Key for selecting the 26th hotbar slot. keymap_slot26 (Hotbar slot 26) key -# Key for selecting the 27th hotbar slot. keymap_slot27 (Hotbar slot 27) key -# Key for selecting the 28th hotbar slot. keymap_slot28 (Hotbar slot 28) key -# Key for selecting the 29th hotbar slot. keymap_slot29 (Hotbar slot 29) key -# Key for selecting the 30th hotbar slot. keymap_slot30 (Hotbar slot 30) key -# Key for selecting the 31st hotbar slot. keymap_slot31 (Hotbar slot 31) key -# Key for selecting the 32nd hotbar slot. keymap_slot32 (Hotbar slot 32) key [*Touchscreen] From 38255cb6bb1e8d0d441b6a6f36a899bbb8001bae Mon Sep 17 00:00:00 2001 From: sfan5 Date: Wed, 4 Jun 2025 12:15:04 +0200 Subject: [PATCH 08/31] Clean up makeScreenshot() and make message translateable --- src/client/client.cpp | 43 +++++++++++++++++++++---------------------- src/gettime.h | 2 +- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/client/client.cpp b/src/client/client.cpp index 5147dd2c98..02e1805af2 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -1897,39 +1897,38 @@ float Client::getCurRate() void Client::makeScreenshot() { - irr::video::IVideoDriver *driver = m_rendering_engine->get_video_driver(); - irr::video::IImage* const raw_image = driver->createScreenShot(); + video::IVideoDriver *driver = m_rendering_engine->get_video_driver(); + video::IImage* const raw_image = driver->createScreenShot(); - if (!raw_image) + if (!raw_image) { + errorstream << "Could not take screenshot" << std::endl; return; + } const struct tm tm = mt_localtime(); - char timetstamp_c[64]; - strftime(timetstamp_c, sizeof(timetstamp_c), "%Y%m%d_%H%M%S", &tm); + char timestamp_c[64]; + strftime(timestamp_c, sizeof(timestamp_c), "%Y%m%d_%H%M%S", &tm); - std::string screenshot_dir; - - if (fs::IsPathAbsolute(g_settings->get("screenshot_path"))) - screenshot_dir = g_settings->get("screenshot_path"); - else - screenshot_dir = porting::path_user + DIR_DELIM + g_settings->get("screenshot_path"); + std::string screenshot_dir = g_settings->get("screenshot_path"); + if (!fs::IsPathAbsolute(screenshot_dir)) + screenshot_dir = porting::path_user + DIR_DELIM + screenshot_dir; std::string filename_base = screenshot_dir + DIR_DELIM + std::string("screenshot_") - + std::string(timetstamp_c); + + timestamp_c; std::string filename_ext = "." + g_settings->get("screenshot_format"); - std::string filename; // Create the directory if it doesn't already exist. // Otherwise, saving the screenshot would fail. - fs::CreateDir(screenshot_dir); + fs::CreateAllDirs(screenshot_dir); u32 quality = (u32)g_settings->getS32("screenshot_quality"); - quality = MYMIN(MYMAX(quality, 0), 100) / 100.0 * 255; + quality = rangelim(quality, 0, 100) / 100.0f * 255; // Try to find a unique filename + std::string filename; unsigned serial = 0; while (serial < SCREENSHOT_MAX_SERIAL_TRIES) { @@ -1940,23 +1939,23 @@ void Client::makeScreenshot() } if (serial == SCREENSHOT_MAX_SERIAL_TRIES) { - infostream << "Could not find suitable filename for screenshot" << std::endl; + errorstream << "Could not find suitable filename for screenshot" << std::endl; } else { - irr::video::IImage* const image = + video::IImage* const image = driver->createImage(video::ECF_R8G8B8, raw_image->getDimension()); if (image) { raw_image->copyTo(image); - std::ostringstream sstr; + std::string msg; if (driver->writeImageToFile(image, filename.c_str(), quality)) { - sstr << "Saved screenshot to '" << filename << "'"; + msg = fmtgettext("Saved screenshot to \"%s\"", filename.c_str()); } else { - sstr << "Failed to save screenshot '" << filename << "'"; + msg = fmtgettext("Failed to save screenshot to \"%s\"", filename.c_str()); } pushToChatQueue(new ChatMessage(CHATMESSAGE_TYPE_SYSTEM, - utf8_to_wide(sstr.str()))); - infostream << sstr.str() << std::endl; + utf8_to_wide(msg))); + infostream << msg << std::endl; image->drop(); } } diff --git a/src/gettime.h b/src/gettime.h index 3a77c45ed1..2986ea6871 100644 --- a/src/gettime.h +++ b/src/gettime.h @@ -20,7 +20,7 @@ inline struct tm mt_localtime() #endif }); - struct tm ret; + struct tm ret{}; time_t t = time(NULL); // TODO we should check if the function returns NULL, which would mean error #ifdef _WIN32 From f431c12b85e44039a3641ea3b1a9ec3847dc89e6 Mon Sep 17 00:00:00 2001 From: Lars Mueller Date: Wed, 4 Jun 2025 13:20:02 +0200 Subject: [PATCH 09/31] Fix model[] not supporting float frames --- src/gui/guiFormSpecMenu.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/gui/guiFormSpecMenu.cpp b/src/gui/guiFormSpecMenu.cpp index 38193684f0..29ff83af44 100644 --- a/src/gui/guiFormSpecMenu.cpp +++ b/src/gui/guiFormSpecMenu.cpp @@ -2852,12 +2852,13 @@ void GUIFormSpecMenu::parseModel(parserData *data, const std::string &element) e->enableContinuousRotation(inf_rotation); e->enableMouseControl(mousectrl); - s32 frame_loop_begin = 0; - s32 frame_loop_end = 0x7FFFFFFF; + f32 frame_loop_begin = 0; + // This will be clamped to the animation duration. + f32 frame_loop_end = std::numeric_limits::infinity(); if (frame_loop.size() == 2) { - frame_loop_begin = stoi(frame_loop[0]); - frame_loop_end = stoi(frame_loop[1]); + frame_loop_begin = stof(frame_loop[0]); + frame_loop_end = stof(frame_loop[1]); } e->setFrameLoop(frame_loop_begin, frame_loop_end); From e452c2900f60635c00a12f120aeef9629cc141bd Mon Sep 17 00:00:00 2001 From: grorp Date: Thu, 5 Jun 2025 23:55:58 +0200 Subject: [PATCH 10/31] Don't force-enable tileable_horizontal/vertical for solid nodes --- src/client/content_mapblock.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/client/content_mapblock.cpp b/src/client/content_mapblock.cpp index c07a0a3b13..6c1cfea33c 100644 --- a/src/client/content_mapblock.cpp +++ b/src/client/content_mapblock.cpp @@ -464,8 +464,6 @@ void MapblockMeshGenerator::drawSolidNode() for (auto &layer : tiles[face].layers) { if (backface_culling) layer.material_flags |= MATERIAL_FLAG_BACKFACE_CULLING; - layer.material_flags |= MATERIAL_FLAG_TILEABLE_HORIZONTAL; - layer.material_flags |= MATERIAL_FLAG_TILEABLE_VERTICAL; } if (!data->m_smooth_lighting) { lights[face] = getFaceLight(cur_node.n, neighbor, nodedef); From aba2b6638ec76488a5e12a940c35eff0d80fda96 Mon Sep 17 00:00:00 2001 From: Xeno333 <149852758+Xeno333@users.noreply.github.com> Date: Sat, 7 Jun 2025 07:44:35 -0500 Subject: [PATCH 11/31] Add documentation for ValueNoise during load time (#16235) --- doc/lua_api.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/lua_api.md b/doc/lua_api.md index 5a2e167073..ca3a5fee79 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -6586,6 +6586,7 @@ Environment access * `core.get_value_noise(noiseparams)` * Return world-specific value noise. * The actual seed used is the noiseparams seed plus the world seed. + * **Important**: Requires the mapgen environment to be initalized, do not use at load time. * `core.get_value_noise(seeddiff, octaves, persistence, spread)` * Deprecated: use `core.get_value_noise(noiseparams)` instead. * `core.get_perlin(noiseparams)` @@ -9270,6 +9271,8 @@ It can be created via `ValueNoise()` or `core.get_value_noise()`. For `core.get_value_noise()`, the actual seed used is the noiseparams seed plus the world seed, to create world-specific noise. +**Important**: These require the mapgen environment to be initalized, do not use at load time. + * `ValueNoise(noiseparams)` * `ValueNoise(seed, octaves, persistence, spread)` (deprecated) * `core.get_value_noise(noiseparams)` @@ -9306,6 +9309,8 @@ For each of the functions with an optional `buffer` parameter: If `buffer` is not nil, this table will be used to store the result instead of creating a new table. +**Important**: These require the mapgen environment to be initalized, do not use at load time. + ### Methods * `get_2d_map(pos)`: returns a `` times `` 2D array of 2D noise From f75d16c1e671222cae1bf9c0f1ad90a28363c790 Mon Sep 17 00:00:00 2001 From: DS Date: Fri, 13 Jun 2025 23:32:55 +0200 Subject: [PATCH 12/31] Fix some misinformation in `world_format.md` (#16256) --- doc/world_format.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/doc/world_format.md b/doc/world_format.md index c519361d99..119114c7c8 100644 --- a/doc/world_format.md +++ b/doc/world_format.md @@ -401,7 +401,7 @@ See below for description. Luanti will correct lighting in the day light bank when the block at `(1, 0, 0)` is also loaded. -Timestamp and node ID mappings were introduced in map format version 29. +Timestamp and node ID mappings come here if map format version >= 29. * `u32` timestamp * Timestamp when last saved, as seconds from starting the game. * `0xffffffff` = invalid/unknown timestamp, nothing should be done with the time @@ -482,13 +482,7 @@ Timestamp and node ID mappings were introduced in map format version 29. * `s32` timeout * 1000 * `s32` elapsed * 1000 -* Since map format version 25: - * `u8` length of the data of a single timer (always 2+4+4=10) - * `u16` `num_of_timers` - * foreach `num_of_timers`: - * `u16` timer position (`(z*16*16 + y*16 + x)`) - * `s32` timeout * 1000 - * `s32` elapsed * 1000 +* Map format version >= 25: see below `u8` static object version: * Always 0 @@ -516,6 +510,14 @@ Before map format version 29: * `u16` `name_len` * `u8[name_len]` `name` +Since map format version 25, node timers come here: + * `u8` length of the data of a single timer (always 2+4+4=10) + * `u16` `num_of_timers` + * foreach `num_of_timers`: + * `u16` timer position (`(z*16*16 + y*16 + x)`) + * `s32` timeout * 1000 + * `s32` elapsed * 1000 + End of File (EOF). # Format of Nodes From 225d2cf9166588477e3f8a4fcfe1ff4337558178 Mon Sep 17 00:00:00 2001 From: Xeno333 <149852758+Xeno333@users.noreply.github.com> Date: Fri, 13 Jun 2025 16:33:10 -0500 Subject: [PATCH 13/31] Make minimap respect `drawtype = "airlike"` (#16251) --- src/client/mapblock_mesh.cpp | 2 +- src/client/minimap.cpp | 8 ++++---- src/client/minimap.h | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/client/mapblock_mesh.cpp b/src/client/mapblock_mesh.cpp index a847471881..07381b4739 100644 --- a/src/client/mapblock_mesh.cpp +++ b/src/client/mapblock_mesh.cpp @@ -626,7 +626,7 @@ MapBlockMesh::MapBlockMesh(Client *client, MeshMakeData *data): if (data->m_vmanip.getNodeNoEx(p).getContent() != CONTENT_IGNORE) { MinimapMapblock *block = new MinimapMapblock; m_minimap_mapblocks[mesh_grid.getOffsetIndex(ofs)] = block; - block->getMinimapNodes(&data->m_vmanip, p); + block->getMinimapNodes(&data->m_vmanip, data->m_nodedef, p); } } } diff --git a/src/client/minimap.cpp b/src/client/minimap.cpp index 0d44f9d000..ae6244b93b 100644 --- a/src/client/minimap.cpp +++ b/src/client/minimap.cpp @@ -713,9 +713,8 @@ void Minimap::updateActiveMarkers() //// MinimapMapblock //// -void MinimapMapblock::getMinimapNodes(VoxelManipulator *vmanip, const v3s16 &pos) +void MinimapMapblock::getMinimapNodes(VoxelManipulator *vmanip, const NodeDefManager *nodedef, const v3s16 &pos) { - for (s16 x = 0; x < MAP_BLOCKSIZE; x++) for (s16 z = 0; z < MAP_BLOCKSIZE; z++) { s16 air_count = 0; @@ -725,11 +724,12 @@ void MinimapMapblock::getMinimapNodes(VoxelManipulator *vmanip, const v3s16 &pos for (s16 y = MAP_BLOCKSIZE -1; y >= 0; y--) { v3s16 p(x, y, z); MapNode n = vmanip->getNodeNoEx(pos + p); - if (!surface_found && n.getContent() != CONTENT_AIR) { + const ContentFeatures &f = nodedef->get(n); + if (!surface_found && f.drawtype != NDT_AIRLIKE) { mmpixel->height = y; mmpixel->n = n; surface_found = true; - } else if (n.getContent() == CONTENT_AIR) { + } else if (f.drawtype == NDT_AIRLIKE) { air_count++; } } diff --git a/src/client/minimap.h b/src/client/minimap.h index 36c900134c..8897f449f7 100644 --- a/src/client/minimap.h +++ b/src/client/minimap.h @@ -66,7 +66,7 @@ struct MinimapPixel { }; struct MinimapMapblock { - void getMinimapNodes(VoxelManipulator *vmanip, const v3s16 &pos); + void getMinimapNodes(VoxelManipulator *vmanip, const NodeDefManager *nodedef, const v3s16 &pos); MinimapPixel data[MAP_BLOCKSIZE * MAP_BLOCKSIZE]; }; From 81d62d01d119a5d519b923b839ea4c08900e1d1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20R=C3=BChle?= Date: Fri, 13 Jun 2025 23:36:44 +0200 Subject: [PATCH 14/31] Detect mouse moving out of inventory slot (#16101) --- src/gui/guiInventoryList.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui/guiInventoryList.cpp b/src/gui/guiInventoryList.cpp index b8ed324c37..5a96a48187 100644 --- a/src/gui/guiInventoryList.cpp +++ b/src/gui/guiInventoryList.cpp @@ -203,10 +203,10 @@ bool GUIInventoryList::OnEvent(const SEvent &event) if (!hovered || hovered->getID() == -1) hovered = m_fs_menu; - bool ret = hovered->OnEvent(event); - IsVisible = was_visible; + bool ret = hovered->OnEvent(event); + return ret; } From 49f48e0a7c91964333ad8e9369dd5d1cc735836f Mon Sep 17 00:00:00 2001 From: "Miguel P.L" <99091580+MiguelPL4@users.noreply.github.com> Date: Sun, 22 Jun 2025 20:04:42 +0000 Subject: [PATCH 15/31] Update links and names in the documentation (#16153) --- .github/CONTRIBUTING.md | 20 +++++++++---------- README.md | 2 +- .../settings/generate_from_settingtypes.lua | 2 +- doc/android.md | 2 +- doc/developing/README.md | 16 +++++++-------- doc/direction.md | 2 +- doc/lua_api.md | 2 +- irr/README.md | 4 ++-- misc/org.luanti.luanti.metainfo.xml | 6 +++--- misc/redirect.html | 6 +++--- src/CMakeLists.txt | 2 +- src/server.cpp | 2 +- src/serverenvironment.cpp | 4 ++-- 13 files changed, 35 insertions(+), 35 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 5c27183668..7d33ddade0 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -25,16 +25,16 @@ Contributions are welcome! Here's how you can help: the work, to avoid disappointment. You may also benefit from discussing on our IRC development channel - [#luanti-dev](http://www.luanti.org/irc/). Note that a proper IRC client + [#luanti-dev](https://docs.luanti.org/about/irc/). Note that a proper IRC client is required to speak on this channel. 3. Start coding! - Refer to the [Lua API](https://github.com/luanti-org/luanti/blob/master/doc/lua_api.md), - [Developer Wiki](https://dev.luanti.org/) and other + [Luanti Documentation](https://docs.luanti.org/) and other [documentation](https://github.com/luanti-org/luanti/tree/master/doc). - - Follow the [C/C++](https://dev.luanti.org/Code_style_guidelines) and - [Lua](https://dev.luanti.org/Lua_code_style_guidelines) code style guidelines. + - Follow the [C/C++](https://docs.luanti.org/for-engine-devs/code-style-guidelines/) and + [Lua](https://docs.luanti.org/for-engine-devs/lua-code-style-guidelines/) code style guidelines. - Check your code works as expected and document any changes to the Lua API. - To avoid conflicting changes between contributions, do not do the following manually. They will be done before each release. - Run `updatepo.sh` or update `luanti.po{,t}` even if your code adds new translatable strings. @@ -64,8 +64,8 @@ Contributions are welcome! Here's how you can help: picture of the project. 2. It works. 3. It follows the code style for - [C/C++](https://dev.luanti.org/Code_style_guidelines) or - [Lua](https://dev.luanti.org/Lua_code_style_guidelines). + [C/C++](https://docs.luanti.org/for-engine-devs/code-style-guidelines/) or + [Lua](https://docs.luanti.org/for-engine-devs/lua-code-style-guidelines/). 4. The code's interfaces are well designed, regardless of other aspects that might need more work in the future. 5. It uses protocols and formats which include the required compatibility. @@ -106,7 +106,7 @@ the project page with a list of current languages Builtin (the component which contains things like server messages, chat command descriptions, privilege descriptions) is translated separately; it needs to be translated by editing a `.tr` text file. See -[Translation](https://dev.luanti.org/Translation) for more information. +[Translation](https://docs.luanti.org/for-creators/translation/) for more information. ## Donations @@ -116,11 +116,11 @@ methods on [our website](http://www.luanti.org/development/#donate). # Maintaining * This is a concise version of the - [Rules & Guidelines](https://dev.luanti.org/engine-dev-process/) on the developer wiki.* + [Rules & Guidelines](https://docs.luanti.org/for-engine-devs/) on the Luanti Documentation.* These notes are for those who have push access Luanti (core developers / maintainers). -- See the [project organisation](https://dev.luanti.org/Organisation) for the people involved. +- See the [project organisation](https://docs.luanti.org/for-engine-devs/organization/) for the people involved. ## Concept approvals and roadmaps @@ -169,4 +169,4 @@ Submit a :+1: (+1) or "Looks good" comment to show you believe the pull-request ## Releasing a new version -*Refer to [dev.luanti.org/Releasing_Luanti](https://dev.luanti.org/Releasing_Luanti)* +*Refer to [docs.luanti.org/for-engine-devs/releasing-luanti](https://docs.luanti.org/for-engine-devs/releasing-luanti/)* diff --git a/README.md b/README.md index 6abd6d22c9..9e5e7e7c40 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Table of Contents Further documentation ---------------------- - Website: https://www.luanti.org/ -- Wiki: https://wiki.luanti.org/ +- Luanti Documentation: https://docs.luanti.org/ - Forum: https://forum.luanti.org/ - GitHub: https://github.com/luanti-org/luanti/ - [Developer documentation](doc/developing/) diff --git a/builtin/common/settings/generate_from_settingtypes.lua b/builtin/common/settings/generate_from_settingtypes.lua index 13b3035cef..1c1535a171 100644 --- a/builtin/common/settings/generate_from_settingtypes.lua +++ b/builtin/common/settings/generate_from_settingtypes.lua @@ -16,7 +16,7 @@ local minetest_example_header = [[ # to the program, eg. "luanti.exe --config ../minetest.conf.example". # Further documentation: -# https://wiki.luanti.org/ +# https://docs.luanti.org/ ]] diff --git a/doc/android.md b/doc/android.md index 353a7d1c81..f51bce3ee9 100644 --- a/doc/android.md +++ b/doc/android.md @@ -42,7 +42,7 @@ configuration file can usually be found at: * After 5.4.2: * `/sdcard/Android/data/net.minetest.minetest/` or `/storage/emulated/0/Android/data/net.minetest.minetest/` if stored on the device * `/storage/emulated/(varying folder name)/Android/data/net.minetest.minetest/` if stored on the SD card -* [Learn more about Android directory](https://wiki.luanti.org/Accessing_Android_Data_Directory) +* [Learn more about Android directory](https://docs.luanti.org/for-players/mobile/) ## Useful settings diff --git a/doc/developing/README.md b/doc/developing/README.md index 419e41edd8..80e531bea3 100644 --- a/doc/developing/README.md +++ b/doc/developing/README.md @@ -1,16 +1,16 @@ # Developer documentation -## Wiki +## Luanti Documentation -Some important development docs are found in the wiki: https://dev.luanti.org/ +Some important development docs are found in the Luanti Documentation: https://docs.luanti.org/ Notable pages: -- [Releasing Luanti](https://dev.luanti.org/Releasing_Luanti) -- [Engine translations](https://dev.luanti.org/Translation#Maintaining_engine_translations) -- [Changelog](https://dev.luanti.org/Changelog) -- [Organisation](https://dev.luanti.org/Organisation) -- [Code style guidelines](https://dev.luanti.org/Code_style_guidelines) +- [Releasing Luanti](https://docs.luanti.org/for-engine-devs/releasing-luanti/) +- [Engine translations](https://docs.luanti.org/for-creators/translation/) +- [Changelog](https://docs.luanti.org/about/changelog/) +- [Organisation](https://docs.luanti.org/for-engine-devs/organization/) +- [Code style guidelines](https://docs.luanti.org/for-engine-devs/code-style-guidelines/) ## In this folder @@ -23,4 +23,4 @@ Notable pages: Oftentimes knowledge hasn't been written down (yet) and your best bet is to ask someone experienced and/or the core developers. -Feel free to join the [#minetest-dev IRC](https://wiki.luanti.org/IRC) and ask questions related to **engine development**. +Feel free to join the [#luanti-dev IRC](https://docs.luanti.org/about/irc/) and ask questions related to **engine development**. diff --git a/doc/direction.md b/doc/direction.md index bd70003d1a..4119f7d8fc 100644 --- a/doc/direction.md +++ b/doc/direction.md @@ -37,7 +37,7 @@ Examples include [general view distance](https://github.com/luanti-org/luanti/issues/7222). This includes work on maintaining -[our Irrlicht fork](https://github.com/minetest/irrlicht), and switching to +[our Irrlicht fork](https://github.com/luanti-org/luanti/tree/master/irr), and switching to alternative libraries to replace Irrlicht functionality as needed ### 2.2 Internal code refactoring diff --git a/doc/lua_api.md b/doc/lua_api.md index ca3a5fee79..ae0533d7ec 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -10,7 +10,7 @@ safely without breaking backwards compatibility. * More information at * Additional documentation: -* (Unofficial) Minetest Modding Book by rubenwardy: +* (Unofficial) Luanti Modding Book by rubenwardy: * Modding tools: Introduction diff --git a/irr/README.md b/irr/README.md index 449903cfe0..eb7f14809f 100644 --- a/irr/README.md +++ b/irr/README.md @@ -1,9 +1,9 @@ IrrlichtMt version 1.9 ====================== -IrrlichtMt is the 3D engine of [Minetest](https://github.com/minetest). +IrrlichtMt is the 3D engine of [Luanti](https://github.com/luanti-org). It is based on the [Irrlicht Engine](https://irrlicht.sourceforge.io/) but is now developed independently. -It is intentionally not compatible to upstream and is planned to be eventually absorbed into Minetest. +It is intentionally not compatible to upstream and is planned to be eventually absorbed into Luanti. Build ----- diff --git a/misc/org.luanti.luanti.metainfo.xml b/misc/org.luanti.luanti.metainfo.xml index 3e99aa36b9..6759cdc1f0 100644 --- a/misc/org.luanti.luanti.metainfo.xml +++ b/misc/org.luanti.luanti.metainfo.xml @@ -158,10 +158,10 @@ https://www.luanti.org https://www.luanti.org/get-involved/#reporting-issues - https://dev.luanti.org/Translation/ + https://docs.luanti.org/for-creators/translation/ https://www.luanti.org/get-involved/#donate - https://wiki.luanti.org/FAQ - https://wiki.luanti.org + https://docs.luanti.org/about/faq + https://docs.luanti.org https://github.com/luanti-org/luanti https://www.luanti.org/get-involved diff --git a/misc/redirect.html b/misc/redirect.html index f08a49ae14..4a219f7f00 100644 --- a/misc/redirect.html +++ b/misc/redirect.html @@ -4,11 +4,11 @@ Luanti API documentation - - + +

Redirecting…

- Click here if you are not redirected. + Click here if you are not redirected. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1231f49baf..bc87ca0704 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -953,7 +953,7 @@ else() set(MATH_FLAGS "-fno-math-errno -fno-trapping-math -fno-signed-zeros") # Enable SSE for floating point math on 32-bit x86 by default - # reasoning see minetest issue #11810 and https://gcc.gnu.org/wiki/FloatingPointMath + # reasoning see luanti issue #11810 and https://gcc.gnu.org/wiki/FloatingPointMath if(CMAKE_SIZEOF_VOID_P EQUAL 4) check_c_source_compiles("#ifndef __i686__\n#error\n#endif\nint main(){}" IS_I686) if(IS_I686) diff --git a/src/server.cpp b/src/server.cpp index 89bba75fbd..e3854802dc 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -4285,7 +4285,7 @@ ModStorageDatabase *Server::openModStorageDatabase(const std::string &world_path warningstream << "/!\\ You are using the old mod storage files backend. " << "This backend is deprecated and may be removed in a future release /!\\" << std::endl << "Switching to SQLite3 is advised, " - << "please read https://wiki.luanti.org/Database_backends." << std::endl; + << "please read https://docs.luanti.org/for-server-hosts/database-backends." << std::endl; return openModStorageDatabase(backend, world_path, world_mt); } diff --git a/src/serverenvironment.cpp b/src/serverenvironment.cpp index 6368f4f8f6..3914ef3643 100644 --- a/src/serverenvironment.cpp +++ b/src/serverenvironment.cpp @@ -257,14 +257,14 @@ void ServerEnvironment::init() warningstream << "/!\\ You are using old player file backend. " << "This backend is deprecated and will be removed in a future release /!\\" << std::endl << "Switching to SQLite3 or PostgreSQL is advised, " - << "please read https://wiki.luanti.org/Database_backends." << std::endl; + << "please read https://docs.luanti.org/for-server-hosts/database-backends." << std::endl; } if (auth_backend_name == "files") { warningstream << "/!\\ You are using old auth file backend. " << "This backend is deprecated and will be removed in a future release /!\\" << std::endl << "Switching to SQLite3 is advised, " - << "please read https://wiki.luanti.org/Database_backends." << std::endl; + << "please read https://docs.luanti.org/for-server-hosts/database-backends." << std::endl; } m_player_database = openPlayerDatabase(player_backend_name, world_path, conf); From d41de3da79ea282ad9e3f4b6b2eacaedafeabe55 Mon Sep 17 00:00:00 2001 From: Lars Date: Fri, 20 Jun 2025 12:15:04 -0700 Subject: [PATCH 16/31] Add test benchmark for Map --- src/benchmark/CMakeLists.txt | 1 + src/benchmark/benchmark_map.cpp | 177 ++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 src/benchmark/benchmark_map.cpp diff --git a/src/benchmark/CMakeLists.txt b/src/benchmark/CMakeLists.txt index e8150848a0..bf2bf15db6 100644 --- a/src/benchmark/CMakeLists.txt +++ b/src/benchmark/CMakeLists.txt @@ -4,6 +4,7 @@ set (BENCHMARK_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/benchmark_lighting.cpp ${CMAKE_CURRENT_SOURCE_DIR}/benchmark_serialize.cpp ${CMAKE_CURRENT_SOURCE_DIR}/benchmark_mapblock.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/benchmark_map.cpp ${CMAKE_CURRENT_SOURCE_DIR}/benchmark_mapmodify.cpp ${CMAKE_CURRENT_SOURCE_DIR}/benchmark_sha.cpp PARENT_SCOPE) diff --git a/src/benchmark/benchmark_map.cpp b/src/benchmark/benchmark_map.cpp new file mode 100644 index 0000000000..0658b6ccb9 --- /dev/null +++ b/src/benchmark/benchmark_map.cpp @@ -0,0 +1,177 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2023 Minetest Authors + +#include "catch.h" +#include "dummygamedef.h" +#include "map.h" +#include "mapsector.h" + +class TestMap : public Map { +public: + TestMap(IGameDef *gamedef) : Map(gamedef) {} + + MapBlock * createBlockTest(v3s16 p) + { + v2s16 p2d(p.X, p.Z); + s16 block_y = p.Y; + + MapSector *sector = getSectorNoGenerate(p2d); + if (!sector) { + sector = new MapSector(this, p2d, m_gamedef); + m_sectors[p2d] = sector; + } + + MapBlock *block = sector->getBlockNoCreateNoEx(block_y); + if (block) + return block; + + return sector->createBlankBlock(block_y); + } + +}; + +static void fillMap(TestMap &map, s16 n) +{ + for(s16 z=0; z0; y--) { + v3s16 p(x,y,z); + MapBlock *block = map.getBlockNoCreateNoEx(p); + if (block) { + result++; + } + } + return result; +} + +static int readNodes(Map &map, s16 n) +{ + int result = 0; + for(s16 z=0; z Date: Sun, 22 Jun 2025 22:06:47 +0200 Subject: [PATCH 17/31] Formspec: Show a player inventory using core.show_formspec (#15963) 'core.show_formspec' now shows and updates the inventory formspec as if it was opened using the hotkey on client-side. --- doc/lua_api.md | 18 +++++++++++++----- src/client/game.cpp | 15 ++++++++++----- src/client/game_formspec.cpp | 29 ++++++++++++++++++++--------- src/client/game_formspec.h | 4 +++- src/client/localplayer.h | 2 ++ src/network/clientpackethandler.cpp | 1 + src/network/serveropcodes.cpp | 1 + src/remoteplayer.h | 2 ++ src/script/lua_api/l_object.cpp | 4 +++- src/server.cpp | 6 ++++-- 10 files changed, 59 insertions(+), 23 deletions(-) diff --git a/doc/lua_api.md b/doc/lua_api.md index ae0533d7ec..b34e571c22 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -6872,10 +6872,15 @@ Formspec * `core.show_formspec(playername, formname, formspec)` * `playername`: name of player to show formspec * `formname`: name passed to `on_player_receive_fields` callbacks. - It should follow the `"modname:"` naming convention. - * `formname` must not be empty, unless you want to reshow - the inventory formspec without updating it for future opens. + * It should follow the `"modname:"` naming convention. + * If empty: Shows a custom, temporary inventory formspec. + * An inventory formspec shown this way will also be updated if + `ObjectRef:set_inventory_formspec` is called. + * Use `ObjectRef:set_inventory_formspec` to change the player's + inventory formspec for future opens. + * Supported if server AND client are both of version >= 5.13.0. * `formspec`: formspec to display + * See also: `core.register_on_player_receive_fields` * `core.close_formspec(playername, formname)` * `playername`: name of player to close formspec * `formname`: has to exactly match the one given in `show_formspec`, or the @@ -8653,9 +8658,12 @@ child will follow movement and rotation of that bone. * Returns `nil` if no attribute found. * `get_meta()`: Returns metadata associated with the player (a PlayerMetaRef). * `set_inventory_formspec(formspec)` - * Redefine player's inventory form - * Should usually be called in `on_joinplayer` + * Redefines the player's inventory formspec. + * Should usually be called at least once in the `on_joinplayer` callback. * If `formspec` is `""`, the player's inventory is disabled. + * If the inventory formspec is currently open on the client, it is + updated immediately. + * See also: `core.register_on_player_receive_fields` * `get_inventory_formspec()`: returns a formspec string * `set_formspec_prepend(formspec)`: * the formspec string will be added to every formspec shown to the user, diff --git a/src/client/game.cpp b/src/client/game.cpp index f9a52c43f5..ebfc3f1c87 100644 --- a/src/client/game.cpp +++ b/src/client/game.cpp @@ -1925,7 +1925,7 @@ void Game::processKeyInput() if (g_settings->getBool("continuous_forward")) toggleAutoforward(); } else if (wasKeyDown(KeyType::INVENTORY)) { - m_game_formspec.showPlayerInventory(); + m_game_formspec.showPlayerInventory(nullptr); } else if (input->cancelPressed()) { #ifdef __ANDROID__ m_android_chat_open = false; @@ -2714,11 +2714,16 @@ void Game::handleClientEvent_DeathscreenLegacy(ClientEvent *event, CameraOrienta void Game::handleClientEvent_ShowFormSpec(ClientEvent *event, CameraOrientation *cam) { - m_game_formspec.showFormSpec(*event->show_formspec.formspec, - *event->show_formspec.formname); + auto &fs = event->show_formspec; - delete event->show_formspec.formspec; - delete event->show_formspec.formname; + if (fs.formname->empty() && !fs.formspec->empty()) { + m_game_formspec.showPlayerInventory(fs.formspec); + } else { + m_game_formspec.showFormSpec(*fs.formspec, *fs.formname); + } + + delete fs.formspec; + delete fs.formname; } void Game::handleClientEvent_ShowCSMFormSpec(ClientEvent *event, CameraOrientation *cam) diff --git a/src/client/game_formspec.cpp b/src/client/game_formspec.cpp index 3d8dc6fc8d..407b1d9d90 100644 --- a/src/client/game_formspec.cpp +++ b/src/client/game_formspec.cpp @@ -178,6 +178,10 @@ public: const std::string &getForm() const { LocalPlayer *player = m_client->getEnv().getLocalPlayer(); + + if (!player->inventory_formspec_override.empty()) + return player->inventory_formspec_override; + return player->inventory_formspec; } @@ -304,7 +308,7 @@ void GameFormSpec::showNodeFormspec(const std::string &formspec, const v3s16 &no m_formspec->setFormSpec(formspec, inventoryloc); } -void GameFormSpec::showPlayerInventory() +void GameFormSpec::showPlayerInventory(const std::string *fs_override) { /* * Don't permit to open inventory is CAO or player doesn't exists. @@ -317,28 +321,35 @@ void GameFormSpec::showPlayerInventory() infostream << "Game: Launching inventory" << std::endl; - PlayerInventoryFormSource *fs_src = new PlayerInventoryFormSource(m_client); + auto fs_src = std::make_unique(m_client); InventoryLocation inventoryloc; inventoryloc.setCurrentPlayer(); - if (m_client->modsLoaded() && m_client->getScript()->on_inventory_open(m_client->getInventory(inventoryloc))) { - delete fs_src; - return; + if (fs_override) { + // Temporary overwrite for this specific formspec. + player->inventory_formspec_override = *fs_override; + } else { + // Show the regular inventory formspec + player->inventory_formspec_override.clear(); } - if (fs_src->getForm().empty()) { - delete fs_src; + // If prevented by Client-Side Mods + if (m_client->modsLoaded() && m_client->getScript()->on_inventory_open(m_client->getInventory(inventoryloc))) + return; + + // Empty formspec -> do not show. + if (fs_src->getForm().empty()) return; - } TextDest *txt_dst = new TextDestPlayerInventory(m_client); GUIFormSpecMenu::create(m_formspec, m_client, m_rendering_engine->get_gui_env(), - &m_input->joystick, fs_src, txt_dst, m_client->getFormspecPrepend(), + &m_input->joystick, fs_src.get(), txt_dst, m_client->getFormspecPrepend(), m_client->getSoundManager()); m_formspec->setFormSpec(fs_src->getForm(), inventoryloc); + fs_src.release(); // owned by GUIFormSpecMenu } #define SIZE_TAG "size[11,5.5,true]" // Fixed size (ignored in touchscreen mode) diff --git a/src/client/game_formspec.h b/src/client/game_formspec.h index 980dac47f9..8ad3059af2 100644 --- a/src/client/game_formspec.h +++ b/src/client/game_formspec.h @@ -34,7 +34,9 @@ struct GameFormSpec // Currently only used for the in-game settings menu. void showPauseMenuFormSpec(const std::string &formspec, const std::string &formname); void showNodeFormspec(const std::string &formspec, const v3s16 &nodepos); - void showPlayerInventory(); + /// If `!fs_override`: Uses `player->inventory_formspec`. + /// If ` fs_override`: Uses a temporary formspec until an update is received. + void showPlayerInventory(const std::string *fs_override); void showDeathFormspecLegacy(); // Shows the hardcoded "main" pause menu. void showPauseMenu(); diff --git a/src/client/localplayer.h b/src/client/localplayer.h index 93b768ceb0..2f108dcceb 100644 --- a/src/client/localplayer.h +++ b/src/client/localplayer.h @@ -99,6 +99,8 @@ public: std::string hotbar_image = ""; std::string hotbar_selected_image = ""; + /// Temporary player inventory formspec. Empty value = feature inactive. + std::string inventory_formspec_override; video::SColor light_color = video::SColor(255, 255, 255, 255); diff --git a/src/network/clientpackethandler.cpp b/src/network/clientpackethandler.cpp index bb1930d960..6cd7150c62 100644 --- a/src/network/clientpackethandler.cpp +++ b/src/network/clientpackethandler.cpp @@ -902,6 +902,7 @@ void Client::handleCommand_InventoryFormSpec(NetworkPacket* pkt) // Store formspec in LocalPlayer player->inventory_formspec = pkt->readLongString(); + player->inventory_formspec_override.clear(); } void Client::handleCommand_DetachedInventory(NetworkPacket* pkt) diff --git a/src/network/serveropcodes.cpp b/src/network/serveropcodes.cpp index b50e13082b..f75e1f5cde 100644 --- a/src/network/serveropcodes.cpp +++ b/src/network/serveropcodes.cpp @@ -178,6 +178,7 @@ const ClientCommandFactory clientCommandFactoryTable[TOCLIENT_NUM_MSG_TYPES] = { "TOCLIENT_STOP_SOUND", 0, true }, // 0x40 { "TOCLIENT_PRIVILEGES", 0, true }, // 0x41 { "TOCLIENT_INVENTORY_FORMSPEC", 0, true }, // 0x42 + // ^ `channel` MUST be the same as TOCLIENT_SHOW_FORMSPEC { "TOCLIENT_DETACHED_INVENTORY", 0, true }, // 0x43 { "TOCLIENT_SHOW_FORMSPEC", 0, true }, // 0x44 { "TOCLIENT_MOVEMENT", 0, true }, // 0x45 diff --git a/src/remoteplayer.h b/src/remoteplayer.h index 1f2f8df9c3..1f56bc517a 100644 --- a/src/remoteplayer.h +++ b/src/remoteplayer.h @@ -121,6 +121,8 @@ public: u16 protocol_version = 0; u16 formspec_version = 0; + bool inventory_formspec_overridden = false; + /// returns PEER_ID_INEXISTENT when PlayerSAO is not ready session_t getPeerId() const { return m_peer_id; } diff --git a/src/script/lua_api/l_object.cpp b/src/script/lua_api/l_object.cpp index 19c513dd31..eed2215ac1 100644 --- a/src/script/lua_api/l_object.cpp +++ b/src/script/lua_api/l_object.cpp @@ -1574,7 +1574,9 @@ int ObjectRef::l_set_inventory_formspec(lua_State *L) auto formspec = readParam(L, 2); - if (formspec != player->inventory_formspec) { + if (player->inventory_formspec_overridden + || formspec != player->inventory_formspec) { + player->inventory_formspec_overridden = false; player->inventory_formspec = formspec; getServer(L)->reportInventoryFormspecModified(player->getName()); } diff --git a/src/server.cpp b/src/server.cpp index e3854802dc..16434f4478 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -1595,11 +1595,10 @@ void Server::SendShowFormspecMessage(session_t peer_id, const std::string &forms (it->second == formname || formname.empty())) { m_formspec_state_data.erase(peer_id); } - pkt.putLongString(""); } else { m_formspec_state_data[peer_id] = formname; - pkt.putLongString(formspec); } + pkt.putLongString(formspec); pkt << formname; Send(&pkt); @@ -3397,6 +3396,9 @@ bool Server::showFormspec(const char *playername, const std::string &formspec, if (!player) return false; + // To allow re-sending the same inventory formspec. + player->inventory_formspec_overridden = formname.empty() && !formspec.empty(); + SendShowFormspecMessage(player->getPeerId(), formspec, formname); return true; } From 1297ccc537ccfeedb5bf281547608d23069f5709 Mon Sep 17 00:00:00 2001 From: Lars Date: Mon, 23 Jun 2025 13:27:40 -0700 Subject: [PATCH 18/31] Quick fix for TestMap name collision, introduced in #16274 --- src/benchmark/benchmark_map.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/benchmark/benchmark_map.cpp b/src/benchmark/benchmark_map.cpp index 0658b6ccb9..0a41fec5a7 100644 --- a/src/benchmark/benchmark_map.cpp +++ b/src/benchmark/benchmark_map.cpp @@ -7,6 +7,7 @@ #include "map.h" #include "mapsector.h" +namespace { class TestMap : public Map { public: TestMap(IGameDef *gamedef) : Map(gamedef) {} @@ -30,6 +31,7 @@ public: } }; +} static void fillMap(TestMap &map, s16 n) { From 48ef7fff232ed7b8614d05b1869f5bb228dd9112 Mon Sep 17 00:00:00 2001 From: DS Date: Tue, 24 Jun 2025 11:49:44 +0200 Subject: [PATCH 19/31] Revert "Detect mouse moving out of inventory slot" (#16281) This reverts commit 81d62d01d119a5d519b923b839ea4c08900e1d1f. --- src/gui/guiInventoryList.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/gui/guiInventoryList.cpp b/src/gui/guiInventoryList.cpp index 5a96a48187..82e7691023 100644 --- a/src/gui/guiInventoryList.cpp +++ b/src/gui/guiInventoryList.cpp @@ -203,10 +203,13 @@ bool GUIInventoryList::OnEvent(const SEvent &event) if (!hovered || hovered->getID() == -1) hovered = m_fs_menu; - IsVisible = was_visible; - bool ret = hovered->OnEvent(event); + // Set visible again *after* processing the event. Otherwise, hovered could + // be another GUIInventoryList, which will call this one again, resulting in + // an infinite loop. + IsVisible = was_visible; + return ret; } From da0f8cd6b54e2ddb3c2652fe88c0b9f8ca54ba78 Mon Sep 17 00:00:00 2001 From: Xeno333 <149852758+Xeno333@users.noreply.github.com> Date: Tue, 24 Jun 2025 04:49:59 -0500 Subject: [PATCH 20/31] Add documentation index for `doc/` as `README.md`, and small docs fixes (#16253) --- doc/README.md | 60 ++++++++++++++++++++++++ doc/developing/README.md | 11 +++-- doc/developing/{misc.md => profiling.md} | 2 +- doc/fst_api.txt | 10 ++-- doc/protocol.txt | 3 +- 5 files changed, 75 insertions(+), 11 deletions(-) create mode 100644 doc/README.md rename doc/developing/{misc.md => profiling.md} (99%) diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000000..894e5d75df --- /dev/null +++ b/doc/README.md @@ -0,0 +1,60 @@ +# Documentation + +This directory contains mostly reference documentation for the Luanti engine. +For a less prescriptive and more guiding documentation, also look at: +https://docs.luanti.org + +Note that the inner workings of the engine are not well documented. It's most +often better to read the code. + +Markdown files are written in a way that they can also be read in plain text. +When modifying, please keep it that way! + +Here is a list with descriptions of relevant files: + +## Server Modding + +- [lua_api.md](lua_api.md): Server Modding API reference. (Not only the Lua part, + but also file structure and everything else.) + If you want to make a mod or game, look here! + A rendered version is also available at . +- [builtin_entities.md](builtin_entities.md): Doc for entities predefined by the + engine (in builtin), i.e. dropped items and falling nodes. + +## Client-Side Content + +- [texture_packs.md](texture_packs.md): Layout and description of Luanti's + texture packs structure and configuration. +- [client_lua_api.md](client_lua_api.md): Client-Provided Client-Side Modding + (CPCSM) API reference. + +## Mainmenu scripting + +- [menu_lua_api.md](menu_lua_api.md): API reference for the mainmenu scripting + environment. +- [fst_api.txt](fst_api.txt): Formspec Toolkit API, included in builtin for the + main menu. + +## Formats and Protocols + +- [world_format.md](world_format.md): Structure of Luanti world directories and + format of the files therein. + Note: If you want to write your own deserializer, it will be easier to read + the `serialize()` and `deSerialize()` functions of the various structures in + C++, e.g. `MapBlock::deSerialize()`. +- [protocol.txt](protocol.txt): *Rough* outline of Luanti's network protocol. + +## Misc. + +- [compiling/](compiling/): Compilation instructions, and options. +- [ides/](ides/): Instructions for configuring certain IDEs for engine development. +- [developing/](developing/): Information about Luanti development. + Note: [developing/profiling.md](developing/profiling.md) can be useful for + modders and server owners! +- [android.md](android.md): Android quirks. +- [direction.md](direction.md): Information related to the future direction of + Luanti. Commonly referred to as the roadmap document. +- [breakages.md](breakages.md): List of planned breakages for the next major + release, i.e. 6.0.0. +- [docker_server.md](docker_server.md): Information about our Docker server + images in the ghcr. diff --git a/doc/developing/README.md b/doc/developing/README.md index 80e531bea3..a2d19cd9a7 100644 --- a/doc/developing/README.md +++ b/doc/developing/README.md @@ -2,7 +2,7 @@ ## Luanti Documentation -Some important development docs are found in the Luanti Documentation: https://docs.luanti.org/ +Some important development docs are found on the docs site: https://docs.luanti.org/ Notable pages: @@ -11,13 +11,14 @@ Notable pages: - [Changelog](https://docs.luanti.org/about/changelog/) - [Organisation](https://docs.luanti.org/for-engine-devs/organization/) - [Code style guidelines](https://docs.luanti.org/for-engine-devs/code-style-guidelines/) + and [Lua code style guidelines](https://docs.luanti.org/for-engine-devs/lua-code-style-guidelines/) ## In this folder -- [Developing minetestserver with Docker](docker.md) -- [Android tips & tricks](android.md) -- [OS/library compatibility policy](os-compatibility.md) -- [Miscellaneous](misc.md) +- [docker.md](docker.md): Developing minetestserver with Docker +- [android.md](android.md): Android tips & tricks +- [os-compatibility.md](os-compatibility.md): OS/library compatibility policy +- [profiling.md](profiling.md): Profiling instructions ## IRC diff --git a/doc/developing/misc.md b/doc/developing/profiling.md similarity index 99% rename from doc/developing/misc.md rename to doc/developing/profiling.md index 5992946cb2..2c334183ba 100644 --- a/doc/developing/misc.md +++ b/doc/developing/profiling.md @@ -1,4 +1,4 @@ -# Miscellaneous +# Profiling ## Profiling Luanti on Linux with perf diff --git a/doc/fst_api.txt b/doc/fst_api.txt index c12093d91c..9170ecf8eb 100644 --- a/doc/fst_api.txt +++ b/doc/fst_api.txt @@ -3,8 +3,10 @@ Formspec toolkit api 0.0.3 Formspec toolkit is a set of functions to create basic ui elements. +You can find the files in builtin/fstk/. -File: fst/ui.lua + +File: fstk/ui.lua ---------------- ui.lua adds base ui interface to add additional components to. @@ -25,7 +27,7 @@ ui.find_by_name(name) --> returns component or nil ^ find a component within ui ^ name: name of component to look for -File: fst/tabview.lua +File: fstk/tabview.lua --------------------- tabview_create(name, size, tabheaderpos) --> returns tabview component @@ -92,7 +94,7 @@ methods: * icon: path to icon * on_click(tabview): callback function -File: fst/dialog.lua +File: fstk/dialog.lua --------------------- Only one dialog can be shown at a time. If a dialog is closed it's parent is gonna be activated and shown again. @@ -129,7 +131,7 @@ members: - parent ^ parent component to return to on exit -File: fst/buttonbar.lua +File: fstk/buttonbar.lua ----------------------- buttonbar_create(name, pos, size, bgcolor, cbf_buttonhandler) diff --git a/doc/protocol.txt b/doc/protocol.txt index b0d4a78f76..7aafb0de91 100644 --- a/doc/protocol.txt +++ b/doc/protocol.txt @@ -3,7 +3,8 @@ Updated 2011-06-18 A custom protocol over UDP. Integers are big endian. -Refer to connection.{h,cpp} for further reference. +Refer to network/mtp/internal.h, network/networkprotocol.{h,cpp}, and +server/clientiface.h for further reference. Initialization: - A dummy reliable packet with peer_id=PEER_ID_INEXISTENT=0 is sent to the server: From 90c3479520fda8cc065c82ee1f5da63d8b91d5b0 Mon Sep 17 00:00:00 2001 From: grorp Date: Tue, 24 Jun 2025 11:50:12 +0200 Subject: [PATCH 21/31] ContentDB: Allow hitting "Install" before package info finishes loading (#16247) --- builtin/mainmenu/content/dlg_package.lua | 151 ++++++++++++----------- 1 file changed, 79 insertions(+), 72 deletions(-) diff --git a/builtin/mainmenu/content/dlg_package.lua b/builtin/mainmenu/content/dlg_package.lua index 500fb3f6c2..1103f42a75 100644 --- a/builtin/mainmenu/content/dlg_package.lua +++ b/builtin/mainmenu/content/dlg_package.lua @@ -3,18 +3,61 @@ -- SPDX-License-Identifier: LGPL-2.1-or-later -local function get_info_formspec(size, padding, text) - return table.concat({ - "formspec_version[6]", - "size[", size.x, ",", size.y, "]", - "padding[0,0]", - "bgcolor[;true]", +local function get_description_hypertext(package, info, loading_error) + -- Screenshots and description + local hypertext = "" .. core.hypertext_escape(package.short_description) .. "\n" - "label[4,4.35;", text, "]", - "container[", padding.x, ",", size.y - 0.8 - padding.y, "]", - "button[0,0;2,0.8;back;", fgettext("Back"), "]", - "container_end[]", - }) + local screenshots = info and info.screenshots or {{url = package.thumbnail}} + + local winfo = core.get_window_info() + local fs_to_px = winfo.size.x / winfo.max_formspec_size.x + for i, ss in ipairs(screenshots) do + local path = get_screenshot(package, ss.url, 2) + hypertext = hypertext .. "" + if i ~= #screenshots then + hypertext = hypertext .. "" + end + end + + if info then + hypertext = hypertext .. "\n" .. info.long_description.head + + local first = true + local function add_link_button(label, name) + if info[name] then + if not first then + hypertext = hypertext .. " | " + end + hypertext = hypertext .. "" .. label .. "" + info.long_description.links["link_" .. name] = info[name] + first = false + end + end + + add_link_button(hgettext("Donate"), "donate_url") + add_link_button(hgettext("Website"), "website") + add_link_button(hgettext("Source"), "repo") + add_link_button(hgettext("Issue Tracker"), "issue_tracker") + add_link_button(hgettext("Translate"), "translation_url") + add_link_button(hgettext("Forum Topic"), "forum_url") + + hypertext = hypertext .. "\n\n" .. info.long_description.body + + elseif loading_error then + hypertext = hypertext .. "\n\n" .. hgettext("Error loading package information") + else + hypertext = hypertext .. "\n\n" .. hgettext("Loading...") + end + + -- Fix the path to blank.png. This is needed for bullet indentation, + -- and also used for screenshot spacing. + hypertext = hypertext:gsub("\n" - local winfo = core.get_window_info() - local fs_to_px = winfo.size.x / winfo.max_formspec_size.x - for i, ss in ipairs(info.screenshots) do - local path = get_screenshot(package, ss.url, 2) - hypertext = hypertext .. "" - if i ~= #info.screenshots then - hypertext = hypertext .. "" - end - end - hypertext = hypertext .. "\n" .. info.long_description.head - - local first = true - local function add_link_button(label, name) - if info[name] then - if not first then - hypertext = hypertext .. " | " - end - hypertext = hypertext .. "" .. core.hypertext_escape(label) .. "" - info.long_description.links["link_" .. name] = info[name] - first = false - end - end - - add_link_button(fgettext("Donate"), "donate_url") - add_link_button(fgettext("Website"), "website") - add_link_button(fgettext("Source"), "repo") - add_link_button(fgettext("Issue Tracker"), "issue_tracker") - add_link_button(fgettext("Translate"), "translation_url") - add_link_button(fgettext("Forum Topic"), "forum_url") - - hypertext = hypertext .. "\n\n" .. info.long_description.body - - -- Fix the path to blank.png. This is needed for bullet indentation. - hypertext = hypertext:gsub(" Date: Tue, 24 Jun 2025 13:51:14 +0200 Subject: [PATCH 22/31] Build benchmarks in one CI run --- .github/workflows/linux.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 14d63f4fb3..88026cca00 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -76,6 +76,8 @@ jobs: env: CC: gcc-14 CXX: g++-14 + # just to check that they compile correctly + CMAKE_FLAGS: '-DBUILD_BENCHMARKS=1' - name: Test run: | From 8eceabd8122448e9fbf9957e509c78151701bce5 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Wed, 25 Jun 2025 15:05:22 +0200 Subject: [PATCH 23/31] Make `core.get_node_raw` a public API (#16265) Co-authored-by: Erich Schubert --- builtin/game/item.lua | 6 +- doc/lua_api.md | 5 + games/devtest/mods/benchmarks/init.lua | 126 +++++++++++++++++++++++-- 3 files changed, 125 insertions(+), 12 deletions(-) diff --git a/builtin/game/item.lua b/builtin/game/item.lua index 4d68f11360..3e810c3f06 100644 --- a/builtin/game/item.lua +++ b/builtin/game/item.lua @@ -740,16 +740,16 @@ core.noneitemdef_default = { -- This is used for the hand and unknown items -- local get_node_raw = core.get_node_raw -core.get_node_raw = nil +local get_name_from_content_id = core.get_name_from_content_id function core.get_node(pos) local content, param1, param2 = get_node_raw(pos.x, pos.y, pos.z) - return {name = core.get_name_from_content_id(content), param1 = param1, param2 = param2} + return {name = get_name_from_content_id(content), param1 = param1, param2 = param2} end function core.get_node_or_nil(pos) local content, param1, param2, pos_ok = get_node_raw(pos.x, pos.y, pos.z) return pos_ok and - {name = core.get_name_from_content_id(content), param1 = param1, param2 = param2} + {name = get_name_from_content_id(content), param1 = param1, param2 = param2} or nil end diff --git a/doc/lua_api.md b/doc/lua_api.md index b34e571c22..438769085e 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -6481,6 +6481,11 @@ Environment access * `core.get_node_or_nil(pos)` * Same as `get_node` but returns `nil` for unloaded areas. * Note that even loaded areas can contain "ignore" nodes. +* `core.get_node_raw(x, y, z)` + * Same as `get_node` but a faster low-level API + * Returns `content_id`, `param1`, `param2`, and `pos_ok` + * The `content_id` can be mapped to a name using `core.get_name_from_content_id()` + * If `pos_ok` is false, the area is unloaded and `content_id == core.CONTENT_IGNORE` * `core.get_node_light(pos[, timeofday])` * Gets the light value at the given position. Note that the light value "inside" the node at the given position is returned, so you usually want diff --git a/games/devtest/mods/benchmarks/init.lua b/games/devtest/mods/benchmarks/init.lua index 8f6bb1ee4a..9397f427d0 100644 --- a/games/devtest/mods/benchmarks/init.lua +++ b/games/devtest/mods/benchmarks/init.lua @@ -106,7 +106,7 @@ core.register_chatcommand("bench_bulk_set_node", { core.chat_send_player(name, "Warming up finished, now benchmarking ...") local start_time = core.get_us_time() - for i=1,#pos_list do + for i = 1, #pos_list do core.set_node(pos_list[i], {name = "mapgen_stone"}) end local middle_time = core.get_us_time() @@ -116,6 +116,7 @@ core.register_chatcommand("bench_bulk_set_node", { ((middle_time - start_time)) / 1000, ((end_time - middle_time)) / 1000 ) + print(msg) return true, msg end, }) @@ -129,16 +130,13 @@ core.register_chatcommand("bench_bulk_get_node", { return false, "No player." end local pos_list = get_positions_cube(player:get_pos()) + local dummy = 0 local function bench() local start_time = core.get_us_time() - for i=1,#pos_list do + for i = 1, #pos_list do local n = core.get_node(pos_list[i]) - -- Make sure the name lookup is never optimized away. - -- Table allocation might still be omitted. But only accessing - -- the name of a node is a common pattern anyways. - if n.name == "benchmarks:nonexistent_node" then - error("should never happen") - end + -- Make sure the name lookup is not optimized away (can this even happen?) + dummy = dummy + #n.name end return core.get_us_time() - start_time end @@ -151,6 +149,115 @@ core.register_chatcommand("bench_bulk_get_node", { local msg = string.format("Benchmark results: core.get_node loop 1: %.2f ms", result_us / 1000) + print(msg) + return true, msg + end, +}) + +core.register_chatcommand("bench_bulk_get_node_raw", { + params = "", + description = "Benchmark: Bulk-get 99×99×99 nodes with raw API", + func = function(name, param) + local player = core.get_player_by_name(name) + if not player then + return false, "No player." + end + local pos_list = get_positions_cube(player:get_pos()) + local dummy = 0 + local function bench() + local start_time = core.get_us_time() + for i = 1, #pos_list do + local pos_i = pos_list[i] + local nid = core.get_node_raw(pos_i.x, pos_i.y, pos_i.z) + -- Make sure the result is not optimized away + dummy = dummy + nid + end + return core.get_us_time() - start_time + end + + core.chat_send_player(name, "Benchmarking core.get_node_raw. Warming up ...") + bench() + + core.chat_send_player(name, "Warming up finished, now benchmarking ...") + local result_us = bench() + + local msg = string.format("Benchmark results: core.get_node_raw loop 1: %.2f ms", + result_us / 1000) + print(msg) + return true, msg + end, +}) + +core.register_chatcommand("bench_bulk_get_node_raw2", { + params = "", + description = "Benchmark: Bulk-get 99×99×99 nodes with raw API and lookup names", + func = function(name, param) + local player = core.get_player_by_name(name) + if not player then + return false, "No player." + end + local pos_list = get_positions_cube(player:get_pos()) + local dummy = 0 + local function bench() + local start_time = core.get_us_time() + for i = 1, #pos_list do + local pos_i = pos_list[i] + local nid = core.get_node_raw(pos_i.x, pos_i.y, pos_i.z) + local name = core.get_name_from_content_id(nid) + -- Make sure the name lookup is not optimized away + dummy = dummy + #name + end + return core.get_us_time() - start_time + end + + core.chat_send_player(name, "Benchmarking core.get_node_raw+get_name_from_content_id. Warming up ...") + bench() + + core.chat_send_player(name, "Warming up finished, now benchmarking ...") + local result_us = bench() + + local msg = string.format("Benchmark results: core.get_node_raw+get_name_from_content_id loop 1: %.2f ms", + result_us / 1000) + print(msg) + return true, msg + end, +}) + +core.register_chatcommand("bench_bulk_get_node_vm", { + params = "", + description = "Benchmark: Bulk-get 99×99×99 nodes with voxel manipulator", + func = function(name, param) + local player = core.get_player_by_name(name) + if not player then + return false, "No player." + end + local pos = player:get_pos() + local dummy = 0 + local function bench() + local start_time = core.get_us_time() + local vm = core.get_voxel_manip(pos:offset(1,1,1), pos:offset(100,100,100)) + local data = vm:get_data() + local mid_time = core.get_us_time() + -- Note that the VManip will actually retrieve more than just the 100³ nodes + -- and also we don't need to iterate pos_list here, so it's not an entirely + -- fair comparison. + for i = 1, 99*99*99 do + local nid = data[i] + -- Make sure the table lookup is not optimized away + dummy = dummy + nid + end + return core.get_us_time() - start_time, mid_time - start_time + end + + core.chat_send_player(name, "Benchmarking core.get_voxel_vmanip+get_data+loop . Warming up ...") + bench() + + core.chat_send_player(name, "Warming up finished, now benchmarking ...") + local result_us, get_data_us = bench() + + local msg = string.format("Benchmark results: core.get_voxel_vmanip+get_data+loop loop 1: %.2f ms of which get_data() %.2f ms", + result_us / 1000, get_data_us / 1000) + print(msg) return true, msg end, }) @@ -174,7 +281,7 @@ core.register_chatcommand("bench_bulk_swap_node", { core.chat_send_player(name, "Warming up finished, now benchmarking ...") local start_time = core.get_us_time() - for i=1,#pos_list do + for i = 1, #pos_list do core.swap_node(pos_list[i], {name = "mapgen_stone"}) end local middle_time = core.get_us_time() @@ -184,6 +291,7 @@ core.register_chatcommand("bench_bulk_swap_node", { ((middle_time - start_time)) / 1000, ((end_time - middle_time)) / 1000 ) + print(msg) return true, msg end, }) From 0ea89d411230b448adc183fe92f66e7b43033090 Mon Sep 17 00:00:00 2001 From: lhofhansl Date: Wed, 25 Jun 2025 06:05:34 -0700 Subject: [PATCH 24/31] Avoid copying empty blocks for mesh-generation (#16286) --- src/client/mesh_generator_thread.cpp | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/client/mesh_generator_thread.cpp b/src/client/mesh_generator_thread.cpp index 712c785ced..6765c6837a 100644 --- a/src/client/mesh_generator_thread.cpp +++ b/src/client/mesh_generator_thread.cpp @@ -11,18 +11,6 @@ #include "util/directiontables.h" #include "porting.h" -// Data placeholder used for copying from non-existent blocks -static struct BlockPlaceholder { - MapNode data[MapBlock::nodecount]; - - BlockPlaceholder() - { - for (std::size_t i = 0; i < MapBlock::nodecount; i++) - data[i] = MapNode(CONTENT_IGNORE); - } - -} block_placeholder; - /* QueuedMeshUpdate */ @@ -188,7 +176,8 @@ void MeshUpdateQueue::fillDataFromMapBlocks(QueuedMeshUpdate *q) for (pos.Z = q->p.Z - 1; pos.Z <= q->p.Z + mesh_grid.cell_size; pos.Z++) for (pos.Y = q->p.Y - 1; pos.Y <= q->p.Y + mesh_grid.cell_size; pos.Y++) { MapBlock *block = q->map_blocks[i++]; - data->fillBlockData(pos, block ? block->getData() : block_placeholder.data); + if (block) + data->fillBlockData(pos, block->getData()); } data->setCrack(q->crack_level, q->crack_pos); From 6545710c8e087d073504d7dfc5eee1a5e6cfe2e5 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Mon, 23 Jun 2025 12:30:44 +0200 Subject: [PATCH 25/31] Refactor meshgen-related code --- src/client/client.cpp | 7 +- src/client/content_mapblock.cpp | 3 +- src/client/content_mapblock.h | 2 +- src/client/mapblock_mesh.h | 16 ++++- src/client/mesh_generator_thread.cpp | 95 +++++++++++++++------------- src/client/mesh_generator_thread.h | 33 ++++++++-- 6 files changed, 95 insertions(+), 61 deletions(-) diff --git a/src/client/client.cpp b/src/client/client.cpp index 02e1805af2..3fd0983f05 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -612,12 +612,7 @@ void Client::step(float dtime) if (minimap_mapblocks.empty()) do_mapper_update = false; - bool is_empty = true; - for (int l = 0; l < MAX_TILE_LAYERS; l++) - if (r.mesh->getMesh(l)->getMeshBufferCount() != 0) - is_empty = false; - - if (is_empty) { + if (r.mesh->isEmpty()) { delete r.mesh; } else { // Replace with the new mesh diff --git a/src/client/content_mapblock.cpp b/src/client/content_mapblock.cpp index 6c1cfea33c..35da8b51a2 100644 --- a/src/client/content_mapblock.cpp +++ b/src/client/content_mapblock.cpp @@ -472,7 +472,6 @@ void MapblockMeshGenerator::drawSolidNode() if (!faces) return; u8 mask = faces ^ 0b0011'1111; // k-th bit is set if k-th face is to be *omitted*, as expected by cuboid drawing functions. - cur_node.origin = intToFloat(cur_node.p, BS); auto box = aabb3f(v3f(-0.5 * BS), v3f(0.5 * BS)); box.MinEdge += cur_node.origin; box.MaxEdge += cur_node.origin; @@ -1773,6 +1772,7 @@ void MapblockMeshGenerator::errorUnknownDrawtype() void MapblockMeshGenerator::drawNode() { + cur_node.origin = intToFloat(cur_node.p, BS); switch (cur_node.f->drawtype) { case NDT_AIRLIKE: // Not drawn at all return; @@ -1783,7 +1783,6 @@ void MapblockMeshGenerator::drawNode() default: break; } - cur_node.origin = intToFloat(cur_node.p, BS); if (data->m_smooth_lighting) { getSmoothLightFrame(); } else { diff --git a/src/client/content_mapblock.h b/src/client/content_mapblock.h index 9d51ba2bc9..8f938eab32 100644 --- a/src/client/content_mapblock.h +++ b/src/client/content_mapblock.h @@ -27,7 +27,7 @@ struct LightInfo { float light_night; float light_boosted; - LightPair getPair(float sunlight_boost = 0.0) const + LightPair getPair(float sunlight_boost = 0.0f) const { return LightPair( (1 - sunlight_boost) * light_day diff --git a/src/client/mapblock_mesh.h b/src/client/mapblock_mesh.h index 6d8ddc36c6..38db55b0f6 100644 --- a/src/client/mapblock_mesh.h +++ b/src/client/mapblock_mesh.h @@ -212,6 +212,20 @@ public: return minimap_mapblocks; } + /// @return true if the mesh contains nothing to draw + bool isEmpty() const + { + if (!m_transparent_triangles.empty()) + return false; + for (auto &mesh : m_mesh) { + for (u32 i = 0; i < mesh->getMeshBufferCount(); i++) { + if (mesh->getMeshBuffer(i)->getIndexCount() != 0) + return false; + } + } + return true; + } + bool isAnimationForced() const { return m_animation_force_timer == 0; @@ -242,7 +256,7 @@ public: /// get the list of transparent buffers const std::vector &getTransparentBuffers() const { - return this->m_transparent_buffers; + return m_transparent_buffers; } private: diff --git a/src/client/mesh_generator_thread.cpp b/src/client/mesh_generator_thread.cpp index 6765c6837a..4f7dd7fb50 100644 --- a/src/client/mesh_generator_thread.cpp +++ b/src/client/mesh_generator_thread.cpp @@ -20,6 +20,38 @@ QueuedMeshUpdate::~QueuedMeshUpdate() delete data; } +void QueuedMeshUpdate::retrieveBlocks(Map *map, u16 cell_size) +{ + const size_t total = (cell_size+2)*(cell_size+2)*(cell_size+2); + if (map_blocks.empty()) + map_blocks.resize(total); + else + assert(map_blocks.size() == total); // must not change + size_t i = 0; + v3s16 pos; + for (pos.X = p.X - 1; pos.X <= p.X + cell_size; pos.X++) + for (pos.Z = p.Z - 1; pos.Z <= p.Z + cell_size; pos.Z++) + for (pos.Y = p.Y - 1; pos.Y <= p.Y + cell_size; pos.Y++) { + if (!map_blocks[i]) { + MapBlock *block = map->getBlockNoCreateNoEx(pos); + if (block) { + block->refGrab(); + map_blocks[i] = block; + } + } + i++; + } +} + +void QueuedMeshUpdate::dropBlocks() +{ + for (auto *block : map_blocks) { + if (block) + block->refDrop(); + } + map_blocks.clear(); +} + /* MeshUpdateQueue */ @@ -36,26 +68,28 @@ MeshUpdateQueue::~MeshUpdateQueue() MutexAutoLock lock(m_mutex); for (QueuedMeshUpdate *q : m_queue) { - for (auto block : q->map_blocks) - if (block) - block->refDrop(); + q->dropBlocks(); delete q; } } -bool MeshUpdateQueue::addBlock(Map *map, v3s16 p, bool ack_block_to_server, bool urgent) +bool MeshUpdateQueue::addBlock(Map *map, v3s16 p, bool ack_block_to_server, + bool urgent, bool from_neighbor) { + // FIXME: with cell_size > 1 there isn't a "main block" and this check is + // probably incorrect and broken MapBlock *main_block = map->getBlockNoCreateNoEx(p); if (!main_block) return false; - MutexAutoLock lock(m_mutex); - MeshGrid mesh_grid = m_client->getMeshGrid(); // Mesh is placed at the corner block of a chunk // (where all coordinate are divisible by the chunk size) - v3s16 mesh_position(mesh_grid.getMeshPos(p)); + v3s16 mesh_position = mesh_grid.getMeshPos(p); + + MutexAutoLock lock(m_mutex); + /* Mark the block as urgent if requested */ @@ -68,57 +102,27 @@ bool MeshUpdateQueue::addBlock(Map *map, v3s16 p, bool ack_block_to_server, bool */ for (QueuedMeshUpdate *q : m_queue) { if (q->p == mesh_position) { - // NOTE: We are not adding a new position to the queue, thus - // refcount_from_queue stays the same. - if(ack_block_to_server) + if (ack_block_to_server) q->ack_list.push_back(p); q->crack_level = m_client->getCrackLevel(); q->crack_pos = m_client->getCrackPos(); q->urgent |= urgent; - v3s16 pos; - int i = 0; - for (pos.X = q->p.X - 1; pos.X <= q->p.X + mesh_grid.cell_size; pos.X++) - for (pos.Z = q->p.Z - 1; pos.Z <= q->p.Z + mesh_grid.cell_size; pos.Z++) - for (pos.Y = q->p.Y - 1; pos.Y <= q->p.Y + mesh_grid.cell_size; pos.Y++) { - if (!q->map_blocks[i]) { - MapBlock *block = map->getBlockNoCreateNoEx(pos); - if (block) { - block->refGrab(); - q->map_blocks[i] = block; - } - } - i++; - } + q->retrieveBlocks(map, mesh_grid.cell_size); return true; } } - /* - Make a list of blocks necessary for mesh generation and lock the blocks in memory. - */ - std::vector map_blocks; - map_blocks.reserve((mesh_grid.cell_size+2)*(mesh_grid.cell_size+2)*(mesh_grid.cell_size+2)); - v3s16 pos; - for (pos.X = mesh_position.X - 1; pos.X <= mesh_position.X + mesh_grid.cell_size; pos.X++) - for (pos.Z = mesh_position.Z - 1; pos.Z <= mesh_position.Z + mesh_grid.cell_size; pos.Z++) - for (pos.Y = mesh_position.Y - 1; pos.Y <= mesh_position.Y + mesh_grid.cell_size; pos.Y++) { - MapBlock *block = map->getBlockNoCreateNoEx(pos); - map_blocks.push_back(block); - if (block) - block->refGrab(); - } - /* Add the block */ QueuedMeshUpdate *q = new QueuedMeshUpdate; q->p = mesh_position; - if(ack_block_to_server) + if (ack_block_to_server) q->ack_list.push_back(p); q->crack_level = m_client->getCrackLevel(); q->crack_pos = m_client->getCrackPos(); q->urgent = urgent; - q->map_blocks = std::move(map_blocks); + q->retrieveBlocks(map, mesh_grid.cell_size); m_queue.push_back(q); return true; @@ -208,6 +212,7 @@ void MeshUpdateWorkerThread::doUpdate() ScopeProfiler sp(g_profiler, "Client: Mesh making (sum)"); + // This generates the mesh: MapBlockMesh *mesh_new = new MapBlockMesh(m_client, q->data); MeshUpdateResult r; @@ -216,7 +221,7 @@ void MeshUpdateWorkerThread::doUpdate() r.solid_sides = get_solid_sides(q->data); r.ack_list = std::move(q->ack_list); r.urgent = q->urgent; - r.map_blocks = q->map_blocks; + r.map_blocks = std::move(q->map_blocks); m_manager->putResult(r); m_queue_in->done(q->p); @@ -251,7 +256,7 @@ void MeshUpdateManager::updateBlock(Map *map, v3s16 p, bool ack_block_to_server, static thread_local const bool many_neighbors = g_settings->getBool("smooth_lighting") && !g_settings->getFlag("performance_tradeoffs"); - if (!m_queue_in.addBlock(map, p, ack_block_to_server, urgent)) { + if (!m_queue_in.addBlock(map, p, ack_block_to_server, urgent, false)) { warningstream << "Update requested for non-existent block at " << p << std::endl; return; @@ -259,10 +264,10 @@ void MeshUpdateManager::updateBlock(Map *map, v3s16 p, bool ack_block_to_server, if (update_neighbors) { if (many_neighbors) { for (v3s16 dp : g_26dirs) - m_queue_in.addBlock(map, p + dp, false, urgent); + m_queue_in.addBlock(map, p + dp, false, urgent, true); } else { for (v3s16 dp : g_6dirs) - m_queue_in.addBlock(map, p + dp, false, urgent); + m_queue_in.addBlock(map, p + dp, false, urgent, true); } } deferUpdate(); diff --git a/src/client/mesh_generator_thread.h b/src/client/mesh_generator_thread.h index c8db03d28c..133c54b3f1 100644 --- a/src/client/mesh_generator_thread.h +++ b/src/client/mesh_generator_thread.h @@ -22,11 +22,24 @@ struct QueuedMeshUpdate int crack_level = -1; v3s16 crack_pos; MeshMakeData *data = nullptr; // This is generated in MeshUpdateQueue::pop() - std::vector map_blocks; + std::vector map_blocks; bool urgent = false; QueuedMeshUpdate() = default; ~QueuedMeshUpdate(); + + /** + * Get blocks needed for this mesh update from the map. + * Blocks that were already loaded are skipped. + * @param map Map + * @param cell_size mesh grid cell size + */ + void retrieveBlocks(Map *map, u16 cell_size); + /** + * Drop block references. + * @note not done by destructor, since this is only safe on main thread + */ + void dropBlocks(); }; /* @@ -45,9 +58,16 @@ public: ~MeshUpdateQueue(); - // Caches the block at p and its neighbors (if needed) and queues a mesh - // update for the block at p - bool addBlock(Map *map, v3s16 p, bool ack_block_to_server, bool urgent); + /** + * Caches the block at p and its neighbors (if needed) and queues a mesh + * update for the block p. + * @param map Map + * @param p block position + * @param ack_to_server Should be acked to server when done? + * @param urget High-priority? + * @param from_neighbor was this update only necessary due to a neighbor change? + */ + bool addBlock(Map *map, v3s16 p, bool ack_to_server, bool urgent, bool from_neighbor); // Returned pointer must be deleted // Returns NULL if queue is empty @@ -56,7 +76,7 @@ public: // Marks a position as finished, unblocking the next update void done(v3s16 pos); - u32 size() + size_t size() { MutexAutoLock lock(m_mutex); return m_queue.size(); @@ -83,7 +103,7 @@ struct MeshUpdateResult u8 solid_sides; std::vector ack_list; bool urgent = false; - std::vector map_blocks; + std::vector map_blocks; MeshUpdateResult() = default; }; @@ -117,6 +137,7 @@ public: void updateBlock(Map *map, v3s16 p, bool ack_block_to_server, bool urgent, bool update_neighbors = false); void putResult(const MeshUpdateResult &r); + /// @note caller needs to refDrop() the affected map_blocks bool getNextResult(MeshUpdateResult &r); From 2733df78c49fe00f47564f72e62e813e12f31129 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Mon, 23 Jun 2025 12:33:10 +0200 Subject: [PATCH 26/31] Skip pointless meshgen updates for air blocks --- src/client/mesh_generator_thread.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/client/mesh_generator_thread.cpp b/src/client/mesh_generator_thread.cpp index 4f7dd7fb50..bfb739a4c6 100644 --- a/src/client/mesh_generator_thread.cpp +++ b/src/client/mesh_generator_thread.cpp @@ -112,6 +112,18 @@ bool MeshUpdateQueue::addBlock(Map *map, v3s16 p, bool ack_block_to_server, } } + /* + Air blocks won't suddenly become visible due to a neighbor update, so + skip those. + Note: this can be extended with more precise checks in the future + */ + if (from_neighbor && mesh_grid.cell_size == 1 && main_block->isAir()) { + assert(!ack_block_to_server); + m_urgents.erase(mesh_position); + g_profiler->add("MeshUpdateQueue: updates skipped", 1); + return true; + } + /* Add the block */ From 2d36d32da842c9be2d2b322e86678a70b8c04f01 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Mon, 23 Jun 2025 13:48:47 +0200 Subject: [PATCH 27/31] Tune mesh generation interval and thread count --- builtin/settingtypes.txt | 6 +++--- src/client/mesh_generator_thread.cpp | 21 ++++++++++++--------- src/profiler.cpp | 4 +++- src/profiler.h | 9 ++++++++- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/builtin/settingtypes.txt b/builtin/settingtypes.txt index 37cd7d5e2c..47cbe38f18 100644 --- a/builtin/settingtypes.txt +++ b/builtin/settingtypes.txt @@ -2009,11 +2009,11 @@ transparency_sorting_group_by_buffers (Transparency Sorting Group by Buffers) bo cloud_radius (Cloud radius) int 12 8 62 # Delay between mesh updates on the client in ms. Increasing this will slow -# down the rate of mesh updates, thus reducing jitter on slower clients. -mesh_generation_interval (Mapblock mesh generation delay) int 0 0 50 +# down the rate of mesh updates, which can help reduce jitter. +mesh_generation_interval (Mapblock mesh generation delay) int 0 0 25 # Number of threads to use for mesh generation. -# Value of 0 (default) will let Luanti autodetect the number of available threads. +# Value of 0 (default) will let Luanti automatically choose the number of threads. mesh_generation_threads (Mapblock mesh generation threads) int 0 0 8 # All mesh buffers with less than this number of vertices will be merged diff --git a/src/client/mesh_generator_thread.cpp b/src/client/mesh_generator_thread.cpp index bfb739a4c6..6f400b6b50 100644 --- a/src/client/mesh_generator_thread.cpp +++ b/src/client/mesh_generator_thread.cpp @@ -210,18 +210,13 @@ MeshUpdateWorkerThread::MeshUpdateWorkerThread(Client *client, MeshUpdateQueue * UpdateThread("Mesh"), m_client(client), m_queue_in(queue_in), m_manager(manager) { m_generation_interval = g_settings->getU16("mesh_generation_interval"); - m_generation_interval = rangelim(m_generation_interval, 0, 50); + m_generation_interval = rangelim(m_generation_interval, 0, 25); } void MeshUpdateWorkerThread::doUpdate() { QueuedMeshUpdate *q; while ((q = m_queue_in->pop())) { - if (m_generation_interval) - sleep_ms(m_generation_interval); - - porting::TriggerMemoryTrim(); - ScopeProfiler sp(g_profiler, "Client: Mesh making (sum)"); // This generates the mesh: @@ -238,6 +233,14 @@ void MeshUpdateWorkerThread::doUpdate() m_manager->putResult(r); m_queue_in->done(q->p); delete q; + sp.stop(); + + porting::TriggerMemoryTrim(); + + // do this after we're done so the interval is enforced without + // adding extra latency. + if (m_generation_interval) + sleep_ms(m_generation_interval); } } @@ -250,12 +253,12 @@ MeshUpdateManager::MeshUpdateManager(Client *client): { int number_of_threads = rangelim(g_settings->getS32("mesh_generation_threads"), 0, 8); - // Automatically use 33% of the system cores for mesh generation, max 4 + // Automatically use 25% of the system cores for mesh generation, max 3 if (number_of_threads == 0) - number_of_threads = MYMIN(4, Thread::getNumberOfProcessors() / 3); + number_of_threads = std::min(3U, Thread::getNumberOfProcessors() / 4); // use at least one thread - number_of_threads = MYMAX(1, number_of_threads); + number_of_threads = std::max(1, number_of_threads); infostream << "MeshUpdateManager: using " << number_of_threads << " threads" << std::endl; for (int i = 0; i < number_of_threads; i++) diff --git a/src/profiler.cpp b/src/profiler.cpp index 3516f4ef3f..e965f530ee 100644 --- a/src/profiler.cpp +++ b/src/profiler.cpp @@ -17,7 +17,7 @@ ScopeProfiler::ScopeProfiler(Profiler *profiler, const std::string &name, m_time1 = porting::getTime(prec); } -ScopeProfiler::~ScopeProfiler() +void ScopeProfiler::stop() noexcept { if (!m_profiler) return; @@ -38,6 +38,8 @@ ScopeProfiler::~ScopeProfiler() m_profiler->max(m_name, duration); break; } + + m_profiler = nullptr; // don't stop a second time } Profiler::Profiler() diff --git a/src/profiler.h b/src/profiler.h index 1ecd5ad1c6..157d66ef7c 100644 --- a/src/profiler.h +++ b/src/profiler.h @@ -13,6 +13,8 @@ #include "threading/mutex_auto_lock.h" #include "util/timetaker.h" #include "util/numeric.h" // paging() +/* FIXME: ^ move this to the .cpp file, it's not needed here */ +#include "util/basic_macros.h" // Global profiler class Profiler; @@ -108,7 +110,12 @@ public: ScopeProfiler(Profiler *profiler, const std::string &name, ScopeProfilerType type = SPT_ADD, TimePrecision precision = PRECISION_MILLI); - ~ScopeProfiler(); + inline ~ScopeProfiler() { stop(); } + + // End profiled scope early + void stop() noexcept; + + DISABLE_CLASS_COPY(ScopeProfiler) private: Profiler *m_profiler = nullptr; From fcbf05fc3030dbf5519f2ba748c89604a669a3a6 Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 26 Jun 2025 13:16:17 -0700 Subject: [PATCH 28/31] Use MapBlock::copyTo to fill MeshMakeData --- src/client/mapblock_mesh.cpp | 9 --------- src/client/mapblock_mesh.h | 1 - src/client/mesh_generator_thread.cpp | 2 +- 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/client/mapblock_mesh.cpp b/src/client/mapblock_mesh.cpp index 07381b4739..51d61b489e 100644 --- a/src/client/mapblock_mesh.cpp +++ b/src/client/mapblock_mesh.cpp @@ -51,15 +51,6 @@ void MeshMakeData::fillBlockDataBegin(const v3s16 &blockpos) m_vmanip.addArea(voxel_area); } -void MeshMakeData::fillBlockData(const v3s16 &bp, MapNode *data) -{ - v3s16 data_size(MAP_BLOCKSIZE, MAP_BLOCKSIZE, MAP_BLOCKSIZE); - VoxelArea data_area(v3s16(0,0,0), data_size - v3s16(1,1,1)); - - v3s16 blockpos_nodes = bp * MAP_BLOCKSIZE; - m_vmanip.copyFrom(data, data_area, v3s16(0,0,0), blockpos_nodes, data_size); -} - void MeshMakeData::fillSingleNode(MapNode data, MapNode padding) { m_blockpos = {0, 0, 0}; diff --git a/src/client/mapblock_mesh.h b/src/client/mapblock_mesh.h index 38db55b0f6..aa0942cdd3 100644 --- a/src/client/mapblock_mesh.h +++ b/src/client/mapblock_mesh.h @@ -60,7 +60,6 @@ struct MeshMakeData Copy block data manually (to allow optimizations by the caller) */ void fillBlockDataBegin(const v3s16 &blockpos); - void fillBlockData(const v3s16 &bp, MapNode *data); /* Prepare block data for rendering a single node located at (0,0,0). diff --git a/src/client/mesh_generator_thread.cpp b/src/client/mesh_generator_thread.cpp index 6f400b6b50..fe2e3eb79f 100644 --- a/src/client/mesh_generator_thread.cpp +++ b/src/client/mesh_generator_thread.cpp @@ -193,7 +193,7 @@ void MeshUpdateQueue::fillDataFromMapBlocks(QueuedMeshUpdate *q) for (pos.Y = q->p.Y - 1; pos.Y <= q->p.Y + mesh_grid.cell_size; pos.Y++) { MapBlock *block = q->map_blocks[i++]; if (block) - data->fillBlockData(pos, block->getData()); + block->copyTo(data->m_vmanip); } data->setCrack(q->crack_level, q->crack_pos); From fd0ca20ce9a1c9a0f338908dcc0bde29d7deec9b Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sun, 29 Jun 2025 17:19:40 +0200 Subject: [PATCH 29/31] Add core.get_mapgen_chunksize() (#16289) --- doc/lua_api.md | 3 +++ .../mods/unittests/inside_mapgen_env.lua | 4 ++-- src/script/lua_api/l_mapgen.cpp | 23 ++++++++++++++++++- src/script/lua_api/l_mapgen.h | 3 +++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/doc/lua_api.md b/doc/lua_api.md index 438769085e..7e703c666a 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -6666,6 +6666,9 @@ Environment access of the *active* mapgen setting `"mapgen_limit"`. * `chunksize` is an optional number. If it is absent, its value is that of the *active* mapgen setting `"chunksize"`. +* `core.get_mapgen_chunksize()` + * Returns the currently active chunksize of the mapgen, as a vector. + The size is specified in blocks. * `core.get_mapgen_setting(name)` * Gets the *active* mapgen setting (or nil if none exists) in string format with the following order of precedence: diff --git a/games/devtest/mods/unittests/inside_mapgen_env.lua b/games/devtest/mods/unittests/inside_mapgen_env.lua index f6f8513ce1..f92465cc57 100644 --- a/games/devtest/mods/unittests/inside_mapgen_env.lua +++ b/games/devtest/mods/unittests/inside_mapgen_env.lua @@ -29,6 +29,6 @@ if core.ipc_cas("unittests:mg_once", nil, true) then end core.register_on_generated(function(vm, pos1, pos2, blockseed) - local n = tonumber(core.get_mapgen_setting("chunksize")) * 16 - 1 - assert(pos2:subtract(pos1) == vector.new(n, n, n)) + local cs = core.get_mapgen_chunksize() + assert(pos2:subtract(pos1) == cs:multiply(core.MAP_BLOCKSIZE):subtract(1)) end) diff --git a/src/script/lua_api/l_mapgen.cpp b/src/script/lua_api/l_mapgen.cpp index 49c28172b1..16fa1760e6 100644 --- a/src/script/lua_api/l_mapgen.cpp +++ b/src/script/lua_api/l_mapgen.cpp @@ -865,7 +865,7 @@ int ModApiMapgen::l_get_mapgen_edges(lua_State *L) } else { std::string chunksize_str; settingsmgr->getMapSetting("chunksize", &chunksize_str); - chunksize = stoi(chunksize_str, -32768, 32767); + chunksize = stoi(chunksize_str, 1, 10); } std::pair edges = get_mapgen_edges(mapgen_limit, chunksize); @@ -874,6 +874,25 @@ int ModApiMapgen::l_get_mapgen_edges(lua_State *L) return 2; } +// get_mapgen_chunksize() +int ModApiMapgen::l_get_mapgen_chunksize(lua_State *L) +{ + NO_MAP_LOCK_REQUIRED; + + const MapSettingsManager *settingsmgr = getEmergeManager(L)->map_settings_mgr; + + // MapSettingsManager::makeMapgenParams cannot be used here because it would + // make mapgen settings immutable from then on. Mapgen settings should stay + // mutable until after mod loading ends. + + std::string chunksize_str; + settingsmgr->getMapSetting("chunksize", &chunksize_str); + s16 chunksize = stoi(chunksize_str, 1, 10); + + push_v3s16(L, {chunksize, chunksize, chunksize}); + return 1; +} + // get_mapgen_setting(name) int ModApiMapgen::l_get_mapgen_setting(lua_State *L) { @@ -2025,6 +2044,7 @@ void ModApiMapgen::Initialize(lua_State *L, int top) API_FCT(get_mapgen_params); API_FCT(set_mapgen_params); API_FCT(get_mapgen_edges); + API_FCT(get_mapgen_chunksize); API_FCT(get_mapgen_setting); API_FCT(set_mapgen_setting); API_FCT(get_mapgen_setting_noiseparams); @@ -2067,6 +2087,7 @@ void ModApiMapgen::InitializeEmerge(lua_State *L, int top) API_FCT(get_seed); API_FCT(get_mapgen_params); API_FCT(get_mapgen_edges); + API_FCT(get_mapgen_chunksize); API_FCT(get_mapgen_setting); API_FCT(get_mapgen_setting_noiseparams); API_FCT(get_noiseparams); diff --git a/src/script/lua_api/l_mapgen.h b/src/script/lua_api/l_mapgen.h index 860daa7e80..67c48a0b09 100644 --- a/src/script/lua_api/l_mapgen.h +++ b/src/script/lua_api/l_mapgen.h @@ -56,6 +56,9 @@ private: // get_mapgen_edges([mapgen_limit[, chunksize]]) static int l_get_mapgen_edges(lua_State *L); + // get_mapgen_chunksize() + static int l_get_mapgen_chunksize(lua_State *L); + // get_seed([add]) static int l_get_seed(lua_State *L); From 43aad3711b21cde371089b1e6136bf5472724019 Mon Sep 17 00:00:00 2001 From: lhofhansl Date: Sun, 29 Jun 2025 13:36:47 -0700 Subject: [PATCH 30/31] MapBlock::getData be gone (#16292) * Remove Mapblock::getData and all its uses * Do not leak ystride, zstride, and nodecount --- src/dummymap.h | 8 +++++--- src/mapblock.cpp | 5 ++--- src/mapblock.h | 13 +++++++------ src/unittest/test_mapblock.cpp | 28 +++++++++++++++++++--------- 4 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/dummymap.h b/src/dummymap.h index 408f91341d..9426126043 100644 --- a/src/dummymap.h +++ b/src/dummymap.h @@ -31,9 +31,11 @@ public: for (s16 y = bpmin.Y; y <= bpmax.Y; y++) { MapBlock *block = getBlockNoCreateNoEx({x, y, z}); if (block) { - auto *data = block->getData(); - for (size_t i = 0; i < MapBlock::nodecount; i++) - data[i] = n; + for (s16 zn=0; zn < MAP_BLOCKSIZE; zn++) + for (s16 yn=0; yn < MAP_BLOCKSIZE; yn++) + for (s16 xn=0; xn < MAP_BLOCKSIZE; xn++) { + block->setNodeNoCheck(xn, yn, zn, n); + } block->expireIsAirCache(); } } diff --git a/src/mapblock.cpp b/src/mapblock.cpp index 348c02a1e0..68cc0aeec5 100644 --- a/src/mapblock.cpp +++ b/src/mapblock.cpp @@ -12,7 +12,6 @@ #include "gamedef.h" #include "irrlicht_changes/printing.h" #include "log.h" -#include "nameidmapping.h" #include "content_mapnode.h" // For legacy name-id mapping #include "content_nodemeta.h" // For legacy deserialization #include "serialization.h" @@ -258,7 +257,7 @@ void MapBlock::expireIsAirCache() // Renumbers the content IDs (starting at 0 and incrementing) // Note that there's no technical reason why we *have to* renumber the IDs, // but we do it anyway as it also helps compressability. -static void getBlockNodeIdMapping(NameIdMapping *nimap, MapNode *nodes, +void MapBlock::getBlockNodeIdMapping(NameIdMapping *nimap, MapNode *nodes, const NodeDefManager *nodedef) { IdIdMapping &mapping = IdIdMapping::giveClearedThreadLocalInstance(); @@ -288,7 +287,7 @@ static void getBlockNodeIdMapping(NameIdMapping *nimap, MapNode *nodes, // Correct ids in the block to match nodedef based on names. // Unknown ones are added to nodedef. // Will not update itself to match id-name pairs in nodedef. -static void correctBlockNodeIds(const NameIdMapping *nimap, MapNode *nodes, +void MapBlock::correctBlockNodeIds(const NameIdMapping *nimap, MapNode *nodes, IGameDef *gamedef) { const NodeDefManager *nodedef = gamedef->ndef(); diff --git a/src/mapblock.h b/src/mapblock.h index 9a13b686f4..9778b98b25 100644 --- a/src/mapblock.h +++ b/src/mapblock.h @@ -21,6 +21,7 @@ class NodeMetadataList; class IGameDef; class MapBlockMesh; class VoxelManipulator; +class NameIdMapping; #define BLOCK_TIMESTAMP_UNDEFINED 0xffffffff @@ -80,11 +81,6 @@ public: raiseModified(MOD_STATE_WRITE_NEEDED, MOD_REASON_REALLOCATE); } - MapNode* getData() - { - return data; - } - //// //// Modification tracking methods //// @@ -427,18 +423,23 @@ public: // clearObject and return removed objects count u32 clearObjects(); +private: static const u32 ystride = MAP_BLOCKSIZE; static const u32 zstride = MAP_BLOCKSIZE * MAP_BLOCKSIZE; static const u32 nodecount = MAP_BLOCKSIZE * MAP_BLOCKSIZE * MAP_BLOCKSIZE; -private: /* Private methods */ void deSerialize_pre22(std::istream &is, u8 version, bool disk); + static void getBlockNodeIdMapping(NameIdMapping *nimap, MapNode *nodes, + const NodeDefManager *nodedef); + static void correctBlockNodeIds(const NameIdMapping *nimap, MapNode *nodes, + IGameDef *gamedef); + /* * PLEASE NOTE: When adding something here be mindful of position and size * of member variables! This is also the reason for the weird public-private diff --git a/src/unittest/test_mapblock.cpp b/src/unittest/test_mapblock.cpp index e4071efd53..1d26b40a8f 100644 --- a/src/unittest/test_mapblock.cpp +++ b/src/unittest/test_mapblock.cpp @@ -68,10 +68,12 @@ void TestMapBlock::testSaveLoad(IGameDef *gamedef, const u8 version) MapBlock block({}, gamedef); // Fill with data PcgRandom r(seed); - for (size_t i = 0; i < MapBlock::nodecount; ++i) { + for (s16 z=0; z < MAP_BLOCKSIZE; z++) + for (s16 y=0; y < MAP_BLOCKSIZE; y++) + for (s16 x=0; x < MAP_BLOCKSIZE; x++) { u32 rval = r.next(); - block.getData()[i] = - MapNode(rval % max, (rval >> 16) & 0xff, (rval >> 24) & 0xff); + block.setNodeNoCheck(x, y, z, + MapNode(rval % max, (rval >> 16) & 0xff, (rval >> 24) & 0xff)); } // Serialize @@ -85,11 +87,13 @@ void TestMapBlock::testSaveLoad(IGameDef *gamedef, const u8 version) // Check data PcgRandom r(seed); - for (size_t i = 0; i < MapBlock::nodecount; ++i) { + for (s16 z=0; z < MAP_BLOCKSIZE; z++) + for (s16 y=0; y < MAP_BLOCKSIZE; y++) + for (s16 x=0; x < MAP_BLOCKSIZE; x++) { u32 rval = r.next(); auto expect = MapNode(rval % max, (rval >> 16) & 0xff, (rval >> 24) & 0xff); - UASSERT(block.getData()[i] == expect); + UASSERT(block.getNodeNoCheck(x, y, z) == expect); } } } @@ -104,8 +108,11 @@ void TestMapBlock::testSave29(IGameDef *gamedef) { // Prepare test block MapBlock block({}, gamedef); - for (size_t i = 0; i < MapBlock::nodecount; ++i) - block.getData()[i] = MapNode(CONTENT_AIR); + for (s16 z=0; z < MAP_BLOCKSIZE; z++) + for (s16 y=0; y < MAP_BLOCKSIZE; y++) + for (s16 x=0; x < MAP_BLOCKSIZE; x++) { + block.setNodeNoCheck(x, y, z, MapNode(CONTENT_AIR)); + } block.setNode({0, 0, 0}, MapNode(t_CONTENT_STONE)); block.serialize(ss, 29, true, -1); @@ -294,8 +301,11 @@ void TestMapBlock::testLoad20(IGameDef *gamedef) UASSERTEQ(auto, get_node(10, 6, 4), "air"); UASSERTEQ(auto, get_node(11, 6, 3), "default:furnace"); - for (size_t i = 0; i < MapBlock::nodecount; ++i) - UASSERT(block.getData()[i].getContent() != CONTENT_IGNORE); + for (s16 z=0; z < MAP_BLOCKSIZE; z++) + for (s16 y=0; y < MAP_BLOCKSIZE; y++) + for (s16 x=0; x < MAP_BLOCKSIZE; x++) { + UASSERT(block.getNodeNoCheck(x, y, z).getContent() != CONTENT_IGNORE); + } // metadata is also translated auto *meta = block.m_node_metadata.get({11, 6, 3}); From 80be9bf76ed710cc8b5cb25702417dba074e0bfb Mon Sep 17 00:00:00 2001 From: DragonWrangler1 <146014546+DragonWrangler1@users.noreply.github.com> Date: Tue, 1 Jul 2025 07:31:26 -0500 Subject: [PATCH 31/31] Add keybinding for world close key (#16250) Fix according to Lua code style guidelines (grorp) Fix order in defaultsettings.cpp (grorp) remove unrequired comment, and whitespace Co-authored-by: y5nw Co-authored-by: grorp --- builtin/common/settings/components.lua | 29 ++++++++++++++++++++------ builtin/settingtypes.txt | 4 ++++ src/client/inputhandler.cpp | 12 +++++++++-- src/client/inputhandler.h | 3 +++ src/defaultsettings.cpp | 1 + 5 files changed, 41 insertions(+), 8 deletions(-) diff --git a/builtin/common/settings/components.lua b/builtin/common/settings/components.lua index 99fb0cd76c..38af1b0862 100644 --- a/builtin/common/settings/components.lua +++ b/builtin/common/settings/components.lua @@ -438,11 +438,28 @@ function make.key(setting) if value == "" then return height end + + local critical_keys = { + keymap_drop = true, + keymap_dig = true, + keymap_place = true, + } + for _, o in ipairs(core.full_settingtypes) do - if o.type == "key" and o.name ~= setting.name and core.are_keycodes_equal(core.settings:get(o.name), value) then - table.insert(fs, ("label[0,%f;%s]"):format(height + 0.3, - core.colorize(mt_color_orange, fgettext([[Conflicts with "$1"]], fgettext(o.readable_name))))) - height = height + 0.6 + if o.type == "key" and o.name ~= setting.name and + core.are_keycodes_equal(core.settings:get(o.name), value) then + + local is_current_close_world = setting.name == "keymap_close_world" + local is_other_close_world = o.name == "keymap_close_world" + local is_current_critical = critical_keys[setting.name] + local is_other_critical = critical_keys[o.name] + + if (is_other_critical or is_current_critical) or + (not is_current_close_world and not is_other_close_world) then + table.insert(fs, ("label[0,%f;%s]"):format(height + 0.3, + core.colorize(mt_color_orange, fgettext([[Conflicts with "$1"]], fgettext(o.readable_name))))) + height = height + 0.6 + end end end return height @@ -454,12 +471,12 @@ function make.key(setting) get_formspec = function(self, avail_w) self.resettable = core.settings:has(setting.name) - local btn_bind_width = math.max(2.5, avail_w/2) + local btn_bind_width = math.max(2.5, avail_w / 2) local value = core.settings:get(setting.name) local fs = { ("label[0,0.4;%s]"):format(get_label(setting)), ("button_key[%f,0;%f,0.8;%s;%s]"):format( - btn_bind_width, btn_bind_width-0.8, + btn_bind_width, btn_bind_width - 0.8, btn_bind, core.formspec_escape(value)), ("image_button[%f,0;0.8,0.8;%s;%s;]"):format(avail_w - 0.8, core.formspec_escape(defaulttexturedir .. "clear.png"), diff --git a/builtin/settingtypes.txt b/builtin/settingtypes.txt index 47cbe38f18..e4bd8522fb 100644 --- a/builtin/settingtypes.txt +++ b/builtin/settingtypes.txt @@ -273,6 +273,10 @@ keymap_increase_viewing_range_min (Increase view range) key SYSTEM_SCANCODE_46 keymap_decrease_viewing_range_min (Decrease view range) key SYSTEM_SCANCODE_45 +# Modifier key bind for closing your world. +# Requires ESC + the selected key to work. +keymap_close_world (Return to Main Menu) key + keymap_slot1 (Hotbar slot 1) key SYSTEM_SCANCODE_30 keymap_slot2 (Hotbar slot 2) key SYSTEM_SCANCODE_31 diff --git a/src/client/inputhandler.cpp b/src/client/inputhandler.cpp index d79bee4d62..56bdb184e2 100644 --- a/src/client/inputhandler.cpp +++ b/src/client/inputhandler.cpp @@ -137,6 +137,7 @@ bool MyEventReceiver::OnEvent(const SEvent &event) // This is separate from other keyboard handling so that it also works in menus. if (event.EventType == EET_KEY_INPUT_EVENT) { KeyPress keyCode(event.KeyInput); + if (keyCode == getKeySetting("keymap_fullscreen")) { if (event.KeyInput.PressedDown && !fullscreen_is_down) { IrrlichtDevice *device = RenderingEngine::get_raw_device(); @@ -150,8 +151,15 @@ bool MyEventReceiver::OnEvent(const SEvent &event) } fullscreen_is_down = event.KeyInput.PressedDown; return true; - } else if (keyCode == EscapeKey && - event.KeyInput.PressedDown && event.KeyInput.Shift) { + + } else if (keyCode == getKeySetting("keymap_close_world")) { + close_world_down = event.KeyInput.PressedDown; + + } else if (keyCode == EscapeKey) { + esc_down = event.KeyInput.PressedDown; + } + + if (esc_down && close_world_down) { g_gamecallback->disconnect(); return true; } diff --git a/src/client/inputhandler.h b/src/client/inputhandler.h index 85da30ff83..ce2c41a637 100644 --- a/src/client/inputhandler.h +++ b/src/client/inputhandler.h @@ -128,6 +128,9 @@ private: // Intentionally not reset by clearInput/releaseAllKeys. bool fullscreen_is_down = false; + bool close_world_down = false; + bool esc_down = false; + PointerType last_pointer_type = PointerType::Mouse; }; diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp index 916ce9e51f..c27da7988e 100644 --- a/src/defaultsettings.cpp +++ b/src/defaultsettings.cpp @@ -187,6 +187,7 @@ void set_default_settings() USEKEY2("keymap_fullscreen", "SYSTEM_SCANCODE_68", "KEY_F11"); USEKEY2("keymap_increase_viewing_range_min", "SYSTEM_SCANCODE_46", "+"); USEKEY2("keymap_decrease_viewing_range_min", "SYSTEM_SCANCODE_45", "-"); + settings->setDefault("keymap_close_world", ""); USEKEY2("keymap_slot1", "SYSTEM_SCANCODE_30", "KEY_KEY_1"); USEKEY2("keymap_slot2", "SYSTEM_SCANCODE_31", "KEY_KEY_2"); USEKEY2("keymap_slot3", "SYSTEM_SCANCODE_32", "KEY_KEY_3");