]> git.lizzy.rs Git - minetest.git/blobdiff - src/client/game.cpp
Add minetest.get_player_window_information() (#12367)
[minetest.git] / src / client / game.cpp
index 4337d308e457bd600c891ae241959d5358a90607..c5dc8f6d9d8337f809b2111595c12066d6184145 100644 (file)
@@ -43,7 +43,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "gameparams.h"
 #include "gettext.h"
 #include "gui/guiChatConsole.h"
-#include "gui/guiConfirmRegistration.h"
 #include "gui/guiFormSpecMenu.h"
 #include "gui/guiKeyChangeMenu.h"
 #include "gui/guiPasswordChange.h"
@@ -72,6 +71,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "version.h"
 #include "script/scripting_client.h"
 #include "hud.h"
+#include "clientdynamicinfo.h"
 
 #if USE_SOUND
        #include "client/sound_openal.h"
@@ -113,7 +113,7 @@ struct TextDestPlayerInventory : public TextDest
        TextDestPlayerInventory(Client *client)
        {
                m_client = client;
-               m_formname = "";
+               m_formname.clear();
        }
        TextDestPlayerInventory(Client *client, const std::string &formname)
        {
@@ -240,7 +240,7 @@ class PlayerInventoryFormSource: public IFormSource
        Client *m_client;
 };
 
-class NodeDugEvent: public MtEvent
+class NodeDugEvent : public MtEvent
 {
 public:
        v3s16 p;
@@ -250,16 +250,14 @@ class NodeDugEvent: public MtEvent
                p(p),
                n(n)
        {}
-       MtEvent::Type getType() const
-       {
-               return MtEvent::NODE_DUG;
-       }
+       Type getType() const { return NODE_DUG; }
 };
 
 class SoundMaker
 {
        ISoundManager *m_sound;
        const NodeDefManager *m_ndef;
+
 public:
        bool makes_footstep_sound;
        float m_player_step_timer;
@@ -267,6 +265,8 @@ class SoundMaker
 
        SimpleSoundSpec m_player_step_sound;
        SimpleSoundSpec m_player_leftpunch_sound;
+       // Second sound made on left punch, currently used for item 'use' sound
+       SimpleSoundSpec m_player_leftpunch_sound2;
        SimpleSoundSpec m_player_rightpunch_sound;
 
        SoundMaker(ISoundManager *sound, const NodeDefManager *ndef):
@@ -283,7 +283,7 @@ class SoundMaker
                if (m_player_step_timer <= 0 && m_player_step_sound.exists()) {
                        m_player_step_timer = 0.03;
                        if (makes_footstep_sound)
-                               m_sound->playSound(m_player_step_sound, false);
+                               m_sound->playSound(m_player_step_sound);
                }
        }
 
@@ -291,7 +291,7 @@ class SoundMaker
        {
                if (m_player_jump_timer <= 0.0f) {
                        m_player_jump_timer = 0.2f;
-                       m_sound->playSound(SimpleSoundSpec("player_jump", 0.5f), false);
+                       m_sound->playSound(SimpleSoundSpec("player_jump", 0.5f));
                }
        }
 
@@ -316,32 +316,33 @@ class SoundMaker
        static void cameraPunchLeft(MtEvent *e, void *data)
        {
                SoundMaker *sm = (SoundMaker *)data;
-               sm->m_sound->playSound(sm->m_player_leftpunch_sound, false);
+               sm->m_sound->playSound(sm->m_player_leftpunch_sound);
+               sm->m_sound->playSound(sm->m_player_leftpunch_sound2);
        }
 
        static void cameraPunchRight(MtEvent *e, void *data)
        {
                SoundMaker *sm = (SoundMaker *)data;
-               sm->m_sound->playSound(sm->m_player_rightpunch_sound, false);
+               sm->m_sound->playSound(sm->m_player_rightpunch_sound);
        }
 
        static void nodeDug(MtEvent *e, void *data)
        {
                SoundMaker *sm = (SoundMaker *)data;
                NodeDugEvent *nde = (NodeDugEvent *)e;
-               sm->m_sound->playSound(sm->m_ndef->get(nde->n).sound_dug, false);
+               sm->m_sound->playSound(sm->m_ndef->get(nde->n).sound_dug);
        }
 
        static void playerDamage(MtEvent *e, void *data)
        {
                SoundMaker *sm = (SoundMaker *)data;
-               sm->m_sound->playSound(SimpleSoundSpec("player_damage", 0.5), false);
+               sm->m_sound->playSound(SimpleSoundSpec("player_damage", 0.5));
        }
 
        static void playerFallingDamage(MtEvent *e, void *data)
        {
                SoundMaker *sm = (SoundMaker *)data;
-               sm->m_sound->playSound(SimpleSoundSpec("player_falling_damage", 0.5), false);
+               sm->m_sound->playSound(SimpleSoundSpec("player_falling_damage", 0.5));
        }
 
        void registerReceiver(MtEventManager *mgr)
@@ -406,6 +407,7 @@ typedef s32 SamplerLayer_t;
 class GameGlobalShaderConstantSetter : public IShaderConstantSetter
 {
        Sky *m_sky;
+       Client *m_client;
        bool *m_force_fog_off;
        f32 *m_fog_range;
        bool m_fog_enabled;
@@ -413,6 +415,8 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter
        CachedPixelShaderSetting<float> m_fog_distance;
        CachedVertexShaderSetting<float> m_animation_timer_vertex;
        CachedPixelShaderSetting<float> m_animation_timer_pixel;
+       CachedVertexShaderSetting<float> m_animation_timer_delta_vertex;
+       CachedPixelShaderSetting<float> m_animation_timer_delta_pixel;
        CachedPixelShaderSetting<float, 3> m_day_light;
        CachedPixelShaderSetting<float, 4> m_star_color;
        CachedPixelShaderSetting<float, 3> m_eye_position_pixel;
@@ -420,15 +424,36 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter
        CachedPixelShaderSetting<float, 3> m_minimap_yaw;
        CachedPixelShaderSetting<float, 3> m_camera_offset_pixel;
        CachedPixelShaderSetting<float, 3> m_camera_offset_vertex;
-       CachedPixelShaderSetting<SamplerLayer_t> m_base_texture;
-       CachedPixelShaderSetting<SamplerLayer_t> m_normal_texture;
-       Client *m_client;
+       CachedPixelShaderSetting<SamplerLayer_t> m_texture0;
+       CachedPixelShaderSetting<SamplerLayer_t> m_texture1;
+       CachedPixelShaderSetting<SamplerLayer_t> m_texture2;
+       CachedPixelShaderSetting<SamplerLayer_t> m_texture3;
+       CachedPixelShaderSetting<float, 2> m_texel_size0;
+       std::array<float, 2> m_texel_size0_values;
+       CachedStructPixelShaderSetting<float, 7> m_exposure_params_pixel;
+       float m_user_exposure_compensation;
+       bool m_bloom_enabled;
+       CachedPixelShaderSetting<float> m_bloom_intensity_pixel;
+       float m_bloom_intensity;
+       CachedPixelShaderSetting<float> m_bloom_strength_pixel;
+       float m_bloom_strength;
+       CachedPixelShaderSetting<float> m_bloom_radius_pixel;
+       float m_bloom_radius;
+       CachedPixelShaderSetting<float> m_saturation_pixel;
 
 public:
        void onSettingsChange(const std::string &name)
        {
                if (name == "enable_fog")
                        m_fog_enabled = g_settings->getBool("enable_fog");
+               if (name == "exposure_compensation")
+                       m_user_exposure_compensation = g_settings->getFloat("exposure_compensation", -1.0f, 1.0f);
+               if (name == "bloom_intensity")
+                       m_bloom_intensity = g_settings->getFloat("bloom_intensity", 0.01f, 1.0f);
+               if (name == "bloom_strength_factor")
+                       m_bloom_strength = RenderingEngine::BASE_BLOOM_STRENGTH * g_settings->getFloat("bloom_strength_factor", 0.1f, 10.0f);
+               if (name == "bloom_radius")
+                       m_bloom_radius = g_settings->getFloat("bloom_radius", 0.1f, 8.0f);
        }
 
        static void settingsCallback(const std::string &name, void *userdata)
@@ -441,12 +466,15 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter
        GameGlobalShaderConstantSetter(Sky *sky, bool *force_fog_off,
                        f32 *fog_range, Client *client) :
                m_sky(sky),
+               m_client(client),
                m_force_fog_off(force_fog_off),
                m_fog_range(fog_range),
                m_sky_bg_color("skyBgColor"),
                m_fog_distance("fogDistance"),
                m_animation_timer_vertex("animationTimer"),
                m_animation_timer_pixel("animationTimer"),
+               m_animation_timer_delta_vertex("animationTimerDelta"),
+               m_animation_timer_delta_pixel("animationTimerDelta"),
                m_day_light("dayLight"),
                m_star_color("starColor"),
                m_eye_position_pixel("eyePosition"),
@@ -454,12 +482,33 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter
                m_minimap_yaw("yawVec"),
                m_camera_offset_pixel("cameraOffset"),
                m_camera_offset_vertex("cameraOffset"),
-               m_base_texture("baseTexture"),
-               m_normal_texture("normalTexture"),
-               m_client(client)
+               m_texture0("texture0"),
+               m_texture1("texture1"),
+               m_texture2("texture2"),
+               m_texture3("texture3"),
+               m_texel_size0("texelSize0"),
+               m_exposure_params_pixel("exposureParams",
+                               std::array<const char*, 7> {
+                                               "luminanceMin", "luminanceMax", "exposureCorrection",
+                                               "speedDarkBright", "speedBrightDark", "centerWeightPower", "compensationFactor"
+                               }),
+               m_bloom_intensity_pixel("bloomIntensity"),
+               m_bloom_strength_pixel("bloomStrength"),
+               m_bloom_radius_pixel("bloomRadius"),
+               m_saturation_pixel("saturation")
        {
                g_settings->registerChangedCallback("enable_fog", settingsCallback, this);
+               g_settings->registerChangedCallback("exposure_compensation", settingsCallback, this);
+               g_settings->registerChangedCallback("bloom_intensity", settingsCallback, this);
+               g_settings->registerChangedCallback("bloom_strength_factor", settingsCallback, this);
+               g_settings->registerChangedCallback("bloom_radius", settingsCallback, this);
+               g_settings->registerChangedCallback("saturation", settingsCallback, this);
                m_fog_enabled = g_settings->getBool("enable_fog");
+               m_user_exposure_compensation = g_settings->getFloat("exposure_compensation", -1.0f, 1.0f);
+               m_bloom_enabled = g_settings->getBool("enable_bloom");
+               m_bloom_intensity = g_settings->getFloat("bloom_intensity", 0.01f, 1.0f);
+               m_bloom_strength = RenderingEngine::BASE_BLOOM_STRENGTH * g_settings->getFloat("bloom_strength_factor", 0.1f, 10.0f);
+               m_bloom_radius = g_settings->getFloat("bloom_radius", 0.1f, 8.0f);
        }
 
        ~GameGlobalShaderConstantSetter()
@@ -501,11 +550,15 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter
                float clr[4] = {star_color.r, star_color.g, star_color.b, star_color.a};
                m_star_color.set(clr, services);
 
-               u32 animation_timer = porting::getTimeMs() % 1000000;
+               u32 animation_timer = m_client->getEnv().getFrameTime() % 1000000;
                float animation_timer_f = (float)animation_timer / 100000.f;
                m_animation_timer_vertex.set(&animation_timer_f, services);
                m_animation_timer_pixel.set(&animation_timer_f, services);
 
+               float animation_timer_delta_f = (float)m_client->getEnv().getFrameTimeDelta() / 100000.f;
+               m_animation_timer_delta_vertex.set(&animation_timer_delta_f, services);
+               m_animation_timer_delta_pixel.set(&animation_timer_delta_f, services);
+
                float eye_position_array[3];
                v3f epos = m_client->getEnv().getLocalPlayer()->getEyePosition();
                epos.getAs3Values(eye_position_array);
@@ -525,9 +578,51 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter
                m_camera_offset_pixel.set(camera_offset_array, services);
                m_camera_offset_vertex.set(camera_offset_array, services);
 
-               SamplerLayer_t base_tex = 0, normal_tex = 1;
-               m_base_texture.set(&base_tex, services);
-               m_normal_texture.set(&normal_tex, services);
+               SamplerLayer_t tex_id;
+               tex_id = 0;
+               m_texture0.set(&tex_id, services);
+               tex_id = 1;
+               m_texture1.set(&tex_id, services);
+               tex_id = 2;
+               m_texture2.set(&tex_id, services);
+               tex_id = 3;
+               m_texture3.set(&tex_id, services);
+
+               m_texel_size0.set(m_texel_size0_values.data(), services);
+
+               const AutoExposure &exposure_params = m_client->getEnv().getLocalPlayer()->getLighting().exposure;
+               std::array<float, 7> exposure_buffer = {
+                       std::pow(2.0f, exposure_params.luminance_min),
+                       std::pow(2.0f, exposure_params.luminance_max),
+                       exposure_params.exposure_correction,
+                       exposure_params.speed_dark_bright,
+                       exposure_params.speed_bright_dark,
+                       exposure_params.center_weight_power,
+                       powf(2.f, m_user_exposure_compensation)
+               };
+               m_exposure_params_pixel.set(exposure_buffer.data(), services);
+
+               if (m_bloom_enabled) {
+                       m_bloom_intensity_pixel.set(&m_bloom_intensity, services);
+                       m_bloom_radius_pixel.set(&m_bloom_radius, services);
+                       m_bloom_strength_pixel.set(&m_bloom_strength, services);
+               }
+               float saturation = m_client->getEnv().getLocalPlayer()->getLighting().saturation;
+               m_saturation_pixel.set(&saturation, services);
+       }
+
+       void onSetMaterial(const video::SMaterial &material)
+       {
+               video::ITexture *texture = material.getTexture(0);
+               if (texture) {
+                       core::dimension2du size = texture->getSize();
+                       m_texel_size0_values[0] = 1.f / size.Width;
+                       m_texel_size0_values[1] = 1.f / size.Height;
+               }
+               else {
+                       m_texel_size0_values[0] = 0.f;
+                       m_texel_size0_values[1] = 0.f;
+               }
        }
 };
 
@@ -615,9 +710,13 @@ struct GameRunData {
        float time_from_last_punch;
        ClientActiveObject *selected_object;
 
-       float jump_timer;
+       float jump_timer_up;          // from key up until key down
+       float jump_timer_down;        // since last key down
+       float jump_timer_down_before; // from key down until key down again
+
        float damage_flash;
        float update_draw_list_timer;
+       float touch_blocks_timer;
 
        f32 fog_range;
 
@@ -719,11 +818,11 @@ class Game {
        void updateCameraDirection(CameraOrientation *cam, float dtime);
        void updateCameraOrientation(CameraOrientation *cam, float dtime);
        void updatePlayerControl(const CameraOrientation &cam);
-       void step(f32 *dtime);
+       void step(f32 dtime);
        void processClientEvents(CameraOrientation *cam);
        void updateCamera(f32 dtime);
        void updateSound(f32 dtime);
-       void processPlayerInteraction(f32 dtime, bool show_hud, bool show_debug);
+       void processPlayerInteraction(f32 dtime, bool show_hud);
        /*!
         * Returns the object or node the player is pointing at.
         * Also updates the selected thing in the Hud.
@@ -814,17 +913,21 @@ class Game {
        void updateChat(f32 dtime);
 
        bool nodePlacement(const ItemDefinition &selected_def, const ItemStack &selected_item,
-               const v3s16 &nodepos, const v3s16 &neighbourpos, const PointedThing &pointed,
+               const v3s16 &nodepos, const v3s16 &neighborpos, const PointedThing &pointed,
                const NodeMetadata *meta);
        static const ClientEventHandler clientEventHandler[CLIENTEVENT_MAX];
 
        f32 getSensitivityScaleFactor() const;
+       ClientDynamicInfo getCurrentDynamicInfo() const;
 
        InputHandler *input = nullptr;
 
        Client *client = nullptr;
        Server *server = nullptr;
 
+       ClientDynamicInfo client_display_info{};
+       float dynamic_info_send_timer = 0;
+
        IWritableTextureSource *texture_src = nullptr;
        IWritableShaderSource *shader_src = nullptr;
 
@@ -842,7 +945,6 @@ class Game {
 
        EventManager *eventmgr = nullptr;
        QuicktuneShortcutter *quicktune = nullptr;
-       bool registration_confirmation_shown = false;
 
        std::unique_ptr<GameUI> m_game_ui;
        GUIChatConsole *gui_chat_console = nullptr; // Free using ->Drop()
@@ -907,9 +1009,16 @@ class Game {
 
        bool m_does_lost_focus_pause_game = false;
 
-       int m_reset_HW_buffer_counter = 0;
+       // if true, (almost) the whole game is paused
+       // this happens in pause menu in singleplayer
+       bool m_is_paused = false;
+
 #ifdef HAVE_TOUCHSCREENGUI
        bool m_cache_hold_aux1;
+       bool m_touch_use_crosshair;
+       inline bool isNoCrosshairAllowed() {
+               return !m_touch_use_crosshair && camera->getCameraMode() == CAMERA_MODE_FIRST;
+       }
 #endif
 #ifdef __ANDROID__
        bool m_android_chat_open;
@@ -1041,6 +1150,10 @@ bool Game::startup(bool *kill,
        m_invert_mouse = g_settings->getBool("invert_mouse");
        m_first_loop_after_window_activation = true;
 
+#ifdef HAVE_TOUCHSCREENGUI
+       m_touch_use_crosshair = g_settings->getBool("touch_use_crosshair");
+#endif
+
        g_client_translations->clear();
 
        // address can change if simple_singleplayer_mode
@@ -1060,9 +1173,9 @@ bool Game::startup(bool *kill,
 void Game::run()
 {
        ProfilerGraph graph;
-       RunStats stats              = { 0 };
-       CameraOrientation cam_view_target  = { 0 };
-       CameraOrientation cam_view  = { 0 };
+       RunStats stats = {};
+       CameraOrientation cam_view_target = {};
+       CameraOrientation cam_view = {};
        FpsControl draw_times;
        f32 dtime; // in seconds
 
@@ -1079,31 +1192,44 @@ void Game::run()
                        && client->checkPrivilege("fast");
 #endif
 
-       irr::core::dimension2d<u32> previous_screen_size(g_settings->getU16("screen_w"),
+       v2u32 previous_screen_size(g_settings->getU16("screen_w"),
                g_settings->getU16("screen_h"));
 
        while (m_rendering_engine->run()
                        && !(*kill || g_gamecallback->shutdown_requested
                        || (server && server->isShutdownRequested()))) {
 
-               const irr::core::dimension2d<u32> &current_screen_size =
-                       m_rendering_engine->get_video_driver()->getScreenSize();
+               // Calculate dtime =
+               //    m_rendering_engine->run() from this iteration
+               //  + Sleep time until the wanted FPS are reached
+               draw_times.limit(device, &dtime);
+
+               const auto current_dynamic_info = getCurrentDynamicInfo();
+               if (!current_dynamic_info.equal(client_display_info)) {
+                       client_display_info = current_dynamic_info;
+                       dynamic_info_send_timer = 0.2f;
+               }
+
+               if (dynamic_info_send_timer > 0) {
+                       dynamic_info_send_timer -= dtime;
+                       if (dynamic_info_send_timer <= 0) {
+                               client->sendUpdateClientInfo(current_dynamic_info);
+                       }
+               }
+
+               const auto &current_screen_size = current_dynamic_info.render_target_size;
+
                // Verify if window size has changed and save it if it's the case
                // Ensure evaluating settings->getBool after verifying screensize
                // First condition is cheaper
                if (previous_screen_size != current_screen_size &&
                                current_screen_size != irr::core::dimension2d<u32>(0,0) &&
                                g_settings->getBool("autosave_screensize")) {
-                       g_settings->setU16("screen_w", current_screen_size.Width);
-                       g_settings->setU16("screen_h", current_screen_size.Height);
+                       g_settings->setU16("screen_w", current_screen_size.X);
+                       g_settings->setU16("screen_h", current_screen_size.Y);
                        previous_screen_size = current_screen_size;
                }
 
-               // Calculate dtime =
-               //    m_rendering_engine->run() from this iteration
-               //  + Sleep time until the wanted FPS are reached
-               draw_times.limit(device, &dtime);
-
                // Prepare render data for next iteration
 
                updateStats(&stats, draw_times, dtime);
@@ -1117,8 +1243,6 @@ void Game::run()
                processQueues();
 
                m_game_ui->clearInfoText();
-               hud->resizeHotbar();
-
 
                updateProfilers(stats, draw_times, dtime);
                processUserInput(dtime);
@@ -1129,13 +1253,26 @@ void Game::run()
                cam_view.camera_pitch += (cam_view_target.camera_pitch -
                                cam_view.camera_pitch) * m_cache_cam_smoothing;
                updatePlayerControl(cam_view);
-               step(&dtime);
+
+               {
+                       bool was_paused = m_is_paused;
+                       m_is_paused = simple_singleplayer_mode && g_menumgr.pausesGame();
+                       if (m_is_paused)
+                               dtime = 0.0f;
+
+                       if (!was_paused && m_is_paused)
+                               pauseAnimation();
+                       else if (was_paused && !m_is_paused)
+                               resumeAnimation();
+               }
+
+               if (!m_is_paused)
+                       step(dtime);
                processClientEvents(&cam_view_target);
                updateDebugState();
                updateCamera(dtime);
                updateSound(dtime);
-               processPlayerInteraction(dtime, m_game_ui->m_flags.show_hud,
-                       m_game_ui->m_flags.show_basic_debug);
+               processPlayerInteraction(dtime, m_game_ui->m_flags.show_hud);
                updateFrame(&graph, &stats, dtime, cam_view);
                updateProfilerGraphs(&graph);
 
@@ -1152,11 +1289,7 @@ void Game::run()
 void Game::shutdown()
 {
        m_rendering_engine->finalize();
-#if IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR <= 8
-       if (g_settings->get("3d_mode") == "pageflip") {
-               driver->setRenderTarget(irr::video::ERT_STEREO_BOTH_BUFFERS);
-       }
-#endif
+
        auto formspec = m_game_ui->getFormspecGUI();
        if (formspec)
                formspec->quitMenu();
@@ -1351,6 +1484,11 @@ bool Game::createClient(const GameStartData &start_data)
        if (client->modsLoaded())
                client->getScript()->on_camera_ready(camera);
        client->setCamera(camera);
+#ifdef HAVE_TOUCHSCREENGUI
+       if (g_touchscreengui) {
+               g_touchscreengui->setUseCrosshair(!isNoCrosshairAllowed());
+       }
+#endif
 
        /* Clouds
         */
@@ -1478,7 +1616,8 @@ bool Game::connectToServer(const GameStartData &start_data,
                                start_data.password, start_data.address,
                                *draw_control, texture_src, shader_src,
                                itemdef_manager, nodedef_manager, sound, eventmgr,
-                               m_rendering_engine, connect_address.isIPv6(), m_game_ui.get());
+                               m_rendering_engine, connect_address.isIPv6(), m_game_ui.get(),
+                               start_data.allow_login_or_register);
                client->migrateModStorage();
        } catch (const BaseException &e) {
                *error_message = fmtgettext("Error creating client: %s", e.what());
@@ -1489,7 +1628,7 @@ bool Game::connectToServer(const GameStartData &start_data,
        client->m_simple_singleplayer_mode = simple_singleplayer_mode;
 
        infostream << "Connecting to server at ";
-       connect_address.print(&infostream);
+       connect_address.print(infostream);
        infostream << std::endl;
 
        client->connect(connect_address,
@@ -1541,28 +1680,16 @@ bool Game::connectToServer(const GameStartData &start_data,
                                break;
                        }
 
-                       if (client->m_is_registration_confirmation_state) {
-                               if (registration_confirmation_shown) {
-                                       // Keep drawing the GUI
-                                       m_rendering_engine->draw_menu_scene(guienv, dtime, true);
-                               } else {
-                                       registration_confirmation_shown = true;
-                                       (new GUIConfirmRegistration(guienv, guienv->getRootGUIElement(), -1,
-                                                  &g_menumgr, client, start_data.name, start_data.password,
-                                                  connection_aborted, texture_src))->drop();
-                               }
-                       } else {
-                               wait_time += dtime;
-                               // Only time out if we aren't waiting for the server we started
-                               if (!start_data.address.empty() && wait_time > 10) {
-                                       *error_message = gettext("Connection timed out.");
-                                       errorstream << *error_message << std::endl;
-                                       break;
-                               }
-
-                               // Update status
-                               showOverlayMessage(N_("Connecting to server..."), dtime, 20);
+                       wait_time += dtime;
+                       // Only time out if we aren't waiting for the server we started
+                       if (!start_data.address.empty() && wait_time > 10) {
+                               *error_message = gettext("Connection timed out.");
+                               errorstream << *error_message << std::endl;
+                               break;
                        }
+
+                       // Update status
+                       showOverlayMessage(N_("Connecting to server..."), dtime, 20);
                }
        } catch (con::PeerNotFoundException &e) {
                // TODO: Should something be done here? At least an info/error
@@ -1740,22 +1867,26 @@ void Game::processQueues()
 
 void Game::updateDebugState()
 {
-       const bool has_basic_debug = true;
+       LocalPlayer *player = client->getEnv().getLocalPlayer();
+
+       // debug UI and wireframe
        bool has_debug = client->checkPrivilege("debug");
+       bool has_basic_debug = has_debug || (player->hud_flags & HUD_FLAG_BASIC_DEBUG);
 
        if (m_game_ui->m_flags.show_basic_debug) {
-               if (!has_basic_debug) {
+               if (!has_basic_debug)
                        m_game_ui->m_flags.show_basic_debug = false;
-               }
        } else if (m_game_ui->m_flags.show_minimal_debug) {
-               if (has_basic_debug) {
+               if (has_basic_debug)
                        m_game_ui->m_flags.show_basic_debug = true;
-               }
        }
        if (!has_basic_debug)
                hud->disableBlockBounds();
        if (!has_debug)
                draw_control->show_wireframe = false;
+
+       // noclip
+       draw_control->allow_noclip = m_cache_enable_noclip && client->checkPrivilege("noclip");
 }
 
 void Game::updateProfilers(const RunStats &stats, const FpsControl &draw_times,
@@ -1877,8 +2008,10 @@ void Game::processUserInput(f32 dtime)
 #endif
 
        // Increase timer for double tap of "keymap_jump"
-       if (m_cache_doubletap_jump && runData.jump_timer <= 0.2f)
-               runData.jump_timer += dtime;
+       if (m_cache_doubletap_jump && runData.jump_timer_up <= 0.2f)
+               runData.jump_timer_up += dtime;
+       if (m_cache_doubletap_jump && runData.jump_timer_down <= 0.4f)
+               runData.jump_timer_down += dtime;
 
        processKeyInput();
        processItemSelection(&runData.new_playeritem);
@@ -1911,7 +2044,7 @@ void Game::processKeyInput()
                if (client->modsLoaded())
                        openConsole(0.2, L".");
                else
-                       m_game_ui->showStatusText(wgettext("Client side scripting is disabled"));
+                       m_game_ui->showTranslatedStatusText("Client side scripting is disabled");
        } else if (wasKeyDown(KeyType::CONSOLE)) {
                openConsole(core::clamp(g_settings->getFloat("console_height"), 0.1f, 1.0f));
        } else if (wasKeyDown(KeyType::FREEMOVE)) {
@@ -1938,7 +2071,7 @@ void Game::processKeyInput()
                }
        } else if (wasKeyDown(KeyType::INC_VOLUME)) {
                if (g_settings->getBool("enable_sound")) {
-                       float new_volume = rangelim(g_settings->getFloat("sound_volume") + 0.1f, 0.0f, 1.0f);
+                       float new_volume = g_settings->getFloat("sound_volume", 0.0f, 0.9f) + 0.1f;
                        g_settings->setFloat("sound_volume", new_volume);
                        std::wstring msg = fwgettext("Volume changed to %d%%", myround(new_volume * 100));
                        m_game_ui->showStatusText(msg);
@@ -1947,7 +2080,7 @@ void Game::processKeyInput()
                }
        } else if (wasKeyDown(KeyType::DEC_VOLUME)) {
                if (g_settings->getBool("enable_sound")) {
-                       float new_volume = rangelim(g_settings->getFloat("sound_volume") - 0.1f, 0.0f, 1.0f);
+                       float new_volume = g_settings->getFloat("sound_volume", 0.1f, 1.0f) - 0.1f;
                        g_settings->setFloat("sound_volume", new_volume);
                        std::wstring msg = fwgettext("Volume changed to %d%%", myround(new_volume * 100));
                        m_game_ui->showStatusText(msg);
@@ -1970,7 +2103,7 @@ void Game::processKeyInput()
        } else if (wasKeyDown(KeyType::MINIMAP)) {
                toggleMinimap(isKeyDown(KeyType::SNEAK));
        } else if (wasKeyDown(KeyType::TOGGLE_CHAT)) {
-               m_game_ui->toggleChat();
+               m_game_ui->toggleChat(client);
        } else if (wasKeyDown(KeyType::TOGGLE_FOG)) {
                toggleFog();
        } else if (wasKeyDown(KeyType::TOGGLE_UPDATE_CAMERA)) {
@@ -1999,7 +2132,7 @@ void Game::processKeyInput()
 
        if (!isKeyDown(KeyType::JUMP) && runData.reset_jump_timer) {
                runData.reset_jump_timer = false;
-               runData.jump_timer = 0.0f;
+               runData.jump_timer_up = 0.0f;
        }
 
        if (quicktune->hasMessage()) {
@@ -2014,7 +2147,6 @@ void Game::processItemSelection(u16 *new_playeritem)
        /* Item selection using mouse wheel
         */
        *new_playeritem = player->getWieldIndex();
-
        s32 wheel = input->getMouseWheel();
        u16 max_item = MYMIN(PLAYER_INVENTORY_SIZE - 1,
                    player->hud_hotbar_itemcount - 1);
@@ -2041,6 +2173,9 @@ void Game::processItemSelection(u16 *new_playeritem)
                        break;
                }
        }
+
+       // Clamp selection again in case it wasn't changed but max_item was
+       *new_playeritem = MYMIN(*new_playeritem, max_item);
 }
 
 
@@ -2140,7 +2275,14 @@ void Game::toggleFreeMove()
 
 void Game::toggleFreeMoveAlt()
 {
-       if (m_cache_doubletap_jump && runData.jump_timer < 0.2f)
+       if (!runData.reset_jump_timer) {
+               runData.jump_timer_down_before = runData.jump_timer_down;
+               runData.jump_timer_down = 0.0f;
+       }
+
+       // key down (0.2 s max.), then key up (0.2 s max.), then key down
+       if (m_cache_doubletap_jump && runData.jump_timer_up < 0.2f &&
+                       runData.jump_timer_down_before < 0.4f) // 0.2 + 0.2
                toggleFreeMove();
 
        runData.reset_jump_timer = true;
@@ -2211,27 +2353,27 @@ void Game::toggleCinematic()
 
 void Game::toggleBlockBounds()
 {
-       if (true /* basic_debug */) {
-               enum Hud::BlockBoundsMode newmode = hud->toggleBlockBounds();
-               switch (newmode) {
-                       case Hud::BLOCK_BOUNDS_OFF:
-                               m_game_ui->showTranslatedStatusText("Block bounds hidden");
-                               break;
-                       case Hud::BLOCK_BOUNDS_CURRENT:
-                               m_game_ui->showTranslatedStatusText("Block bounds shown for current block");
-                               break;
-                       case Hud::BLOCK_BOUNDS_NEAR:
-                               m_game_ui->showTranslatedStatusText("Block bounds shown for nearby blocks");
-                               break;
-                       case Hud::BLOCK_BOUNDS_MAX:
-                               m_game_ui->showTranslatedStatusText("Block bounds shown for all blocks");
-                               break;
-                       default:
-                               break;
-               }
-
-       } else {
-               m_game_ui->showTranslatedStatusText("Can't show block bounds (need 'basic_debug' privilege)");
+       LocalPlayer *player = client->getEnv().getLocalPlayer();
+       if (!(client->checkPrivilege("debug") || (player->hud_flags & HUD_FLAG_BASIC_DEBUG))) {
+               m_game_ui->showTranslatedStatusText("Can't show block bounds (disabled by mod or game)");
+               return;
+       }
+       enum Hud::BlockBoundsMode newmode = hud->toggleBlockBounds();
+       switch (newmode) {
+               case Hud::BLOCK_BOUNDS_OFF:
+                       m_game_ui->showTranslatedStatusText("Block bounds hidden");
+                       break;
+               case Hud::BLOCK_BOUNDS_CURRENT:
+                       m_game_ui->showTranslatedStatusText("Block bounds shown for current block");
+                       break;
+               case Hud::BLOCK_BOUNDS_NEAR:
+                       m_game_ui->showTranslatedStatusText("Block bounds shown for nearby blocks");
+                       break;
+               case Hud::BLOCK_BOUNDS_MAX:
+                       m_game_ui->showTranslatedStatusText("Block bounds shown for all blocks");
+                       break;
+               default:
+                       break;
        }
 }
 
@@ -2298,6 +2440,9 @@ void Game::toggleFog()
 
 void Game::toggleDebug()
 {
+       LocalPlayer *player = client->getEnv().getLocalPlayer();
+       bool has_debug = client->checkPrivilege("debug");
+       bool has_basic_debug = has_debug || (player->hud_flags & HUD_FLAG_BASIC_DEBUG);
        // Initial: No debug info
        // 1x toggle: Debug text
        // 2x toggle: Debug text with profiler graph
@@ -2307,9 +2452,8 @@ void Game::toggleDebug()
        // The debug text can be in 2 modes: minimal and basic.
        // * Minimal: Only technical client info that not gameplay-relevant
        // * Basic: Info that might give gameplay advantage, e.g. pos, angle
-       // Basic mode is always used.
-
-       const bool has_basic_debug = true;
+       // Basic mode is used when player has the debug HUD flag set,
+       // otherwise the Minimal mode is used.
        if (!m_game_ui->m_flags.show_minimal_debug) {
                m_game_ui->m_flags.show_minimal_debug = true;
                if (has_basic_debug)
@@ -2333,7 +2477,7 @@ void Game::toggleDebug()
                m_game_ui->m_flags.show_basic_debug = false;
                m_game_ui->m_flags.show_profiler_graph = false;
                draw_control->show_wireframe = false;
-               if (client->checkPrivilege("debug")) {
+               if (has_debug) {
                        m_game_ui->showTranslatedStatusText("Debug info, profiler graph, and wireframe hidden");
                } else {
                        m_game_ui->showTranslatedStatusText("Debug info and profiler graph hidden");
@@ -2405,6 +2549,13 @@ void Game::checkZoomEnabled()
 
 void Game::updateCameraDirection(CameraOrientation *cam, float dtime)
 {
+#if !defined(__ANDROID__) && IRRLICHT_VERSION_MT_REVISION >= 9
+       if (isMenuActive())
+               device->getCursorControl()->setRelativeMode(false);
+       else
+               device->getCursorControl()->setRelativeMode(true);
+#endif
+
        if ((device->isWindowActive() && device->isWindowFocused()
                        && !isMenuActive()) || input->isRandom()) {
 
@@ -2450,6 +2601,19 @@ f32 Game::getSensitivityScaleFactor() const
        return tan(fov_y / 2.0f) * 1.3763818698f;
 }
 
+ClientDynamicInfo Game::getCurrentDynamicInfo() const
+{
+       v2u32 screen_size = RenderingEngine::getWindowSize();
+       f32 density = RenderingEngine::getDisplayDensity();
+       f32 gui_scaling = g_settings->getFloat("gui_scaling") * density;
+       f32 hud_scaling = g_settings->getFloat("hud_scaling") * density;
+
+       return {
+               screen_size, gui_scaling, hud_scaling,
+               ClientDynamicInfo::calculateMaxFSSize(screen_size)
+       };
+}
+
 void Game::updateCameraOrientation(CameraOrientation *cam, float dtime)
 {
 #ifdef HAVE_TOUCHSCREENGUI
@@ -2509,11 +2673,13 @@ void Game::updatePlayerControl(const CameraOrientation &cam)
                input->getMovementDirection()
        );
 
-       // autoforward if set: move towards pointed position at maximum speed
+       // autoforward if set: move at maximum speed
        if (player->getPlayerSettings().continuous_forward &&
                        client->activeObjectsReceived() && !player->isDead()) {
                control.movement_speed = 1.0f;
-               control.movement_direction = 0.0f;
+               // sideways movement only
+               float dx = sin(control.movement_direction);
+               control.movement_direction = atan2(dx, 1.0f);
        }
 
 #ifdef HAVE_TOUCHSCREENGUI
@@ -2533,22 +2699,12 @@ void Game::updatePlayerControl(const CameraOrientation &cam)
 }
 
 
-inline void Game::step(f32 *dtime)
+inline void Game::step(f32 dtime)
 {
-       bool can_be_and_is_paused =
-                       (simple_singleplayer_mode && g_menumgr.pausesGame());
+       if (server)
+               server->step(dtime);
 
-       if (can_be_and_is_paused) { // This is for a singleplayer server
-               *dtime = 0;             // No time passes
-       } else {
-               if (simple_singleplayer_mode && !paused_animated_nodes.empty())
-                       resumeAnimation();
-
-               if (server)
-                       server->step(*dtime);
-
-               client->step(*dtime);
-       }
+       client->step(dtime);
 }
 
 static void pauseNodeAnimation(PausedNodesList &paused, scene::ISceneNode *node) {
@@ -2609,6 +2765,9 @@ void Game::handleClientEvent_PlayerDamage(ClientEvent *event, CameraOrientation
        if (client->modsLoaded())
                client->getScript()->on_damage_taken(event->player_damage.amount);
 
+       if (!event->player_damage.effect)
+               return;
+
        // Damage flash and hurt tilt are not used at death
        if (client->getHP() > 0) {
                LocalPlayer *player = client->getEnv().getLocalPlayer();
@@ -2848,6 +3007,9 @@ void Game::handleClientEvent_SetSky(ClientEvent *event, CameraOrientation *cam)
                );
        }
 
+       // Orbit Tilt:
+       sky->setBodyOrbitTilt(event->set_sky->body_orbit_tilt);
+
        delete event->set_sky;
 }
 
@@ -2877,6 +3039,7 @@ void Game::handleClientEvent_SetStars(ClientEvent *event, CameraOrientation *cam
        sky->setStarCount(event->star_params->count);
        sky->setStarColor(event->star_params->starcolor);
        sky->setStarScale(event->star_params->scale);
+       sky->setStarDayOpacity(event->star_params->day_opacity);
        delete event->star_params;
 }
 
@@ -2967,6 +3130,11 @@ void Game::updateCamera(f32 dtime)
 
                camera->toggleCameraMode();
 
+#ifdef HAVE_TOUCHSCREENGUI
+               if (g_touchscreengui)
+                       g_touchscreengui->setUseCrosshair(!isNoCrosshairAllowed());
+#endif
+
                // Make the player visible depending on camera mode.
                playercao->updateMeshCulling();
                playercao->setChildrenVisible(camera->getCameraMode() > CAMERA_MODE_FIRST);
@@ -2979,14 +3147,15 @@ void Game::updateCamera(f32 dtime)
        camera->update(player, dtime, tool_reload_ratio);
        camera->step(dtime);
 
-       v3f camera_position = camera->getPosition();
-       v3f camera_direction = camera->getDirection();
        f32 camera_fov = camera->getFovMax();
        v3s16 camera_offset = camera->getOffset();
 
        m_camera_offset_changed = (camera_offset != old_camera_offset);
 
        if (!m_flags.disable_camera_update) {
+               v3f camera_position = camera->getPosition();
+               v3f camera_direction = camera->getDirection();
+
                client->getEnv().getClientMap().updateCamera(camera_position,
                                camera_direction, camera_fov, camera_offset);
 
@@ -3039,7 +3208,7 @@ void Game::updateSound(f32 dtime)
 }
 
 
-void Game::processPlayerInteraction(f32 dtime, bool show_hud, bool show_debug)
+void Game::processPlayerInteraction(f32 dtime, bool show_hud)
 {
        LocalPlayer *player = client->getEnv().getLocalPlayer();
 
@@ -3076,16 +3245,14 @@ void Game::processPlayerInteraction(f32 dtime, bool show_hud, bool show_debug)
        shootline.end = shootline.start + camera_direction * BS * d;
 
 #ifdef HAVE_TOUCHSCREENGUI
-
-       if ((g_settings->getBool("touchtarget")) && (g_touchscreengui)) {
+       if (g_touchscreengui && isNoCrosshairAllowed()) {
                shootline = g_touchscreengui->getShootline();
                // Scale shootline to the acual distance the player can reach
-               shootline.end = shootline.start
-                       + shootline.getVector().normalize() * BS * d;
+               shootline.end = shootline.start +
+                               shootline.getVector().normalize() * BS * d;
                shootline.start += intToFloat(camera_offset, BS);
                shootline.end += intToFloat(camera_offset, BS);
        }
-
 #endif
 
        PointedThing pointed = updatePointedThing(shootline,
@@ -3093,10 +3260,12 @@ void Game::processPlayerInteraction(f32 dtime, bool show_hud, bool show_debug)
                        !runData.btn_down_for_dig,
                        camera_offset);
 
-       if (pointed != runData.pointed_old) {
+       if (pointed != runData.pointed_old)
                infostream << "Pointing at " << pointed.dump() << std::endl;
-               hud->updateSelectionMesh(camera_offset);
-       }
+
+       // Note that updating the selection mesh every frame is not particularly efficient,
+       // but the halo rendering code is already inefficient so there's no point in optimizing it here
+       hud->updateSelectionMesh(camera_offset);
 
        // Allow digging again if button is not pressed
        if (runData.digging_blocked && !isKeyDown(KeyType::DIG))
@@ -3141,7 +3310,9 @@ void Game::processPlayerInteraction(f32 dtime, bool show_hud, bool show_debug)
 
        runData.punching = false;
 
-       soundmaker->m_player_leftpunch_sound.name = "";
+       soundmaker->m_player_leftpunch_sound = SimpleSoundSpec();
+       soundmaker->m_player_leftpunch_sound2 = pointed.type != POINTEDTHING_NOTHING ?
+               selected_def.sound_use : selected_def.sound_use_air;
 
        // Prepare for repeating, unless we're not supposed to
        if (isKeyDown(KeyType::PLACE) && !g_settings->getBool("safe_dig_and_place"))
@@ -3157,7 +3328,9 @@ void Game::processPlayerInteraction(f32 dtime, bool show_hud, bool show_debug)
                handlePointingAtNode(pointed, selected_item, hand_item, dtime);
        } else if (pointed.type == POINTEDTHING_OBJECT) {
                v3f player_position  = player->getPosition();
-               handlePointingAtObject(pointed, tool_item, player_position, show_debug);
+               bool basic_debug_allowed = client->checkPrivilege("debug") || (player->hud_flags & HUD_FLAG_BASIC_DEBUG);
+               handlePointingAtObject(pointed, tool_item, player_position,
+                               m_game_ui->m_flags.show_basic_debug && basic_debug_allowed);
        } else if (isKeyDown(KeyType::DIG)) {
                // When button is held down in air, show continuous animation
                runData.punching = true;
@@ -3195,7 +3368,7 @@ PointedThing Game::updatePointedThing(
 {
        std::vector<aabb3f> *selectionboxes = hud->getSelectionBoxes();
        selectionboxes->clear();
-       hud->setSelectedFaceNormal(v3f(0.0, 0.0, 0.0));
+       hud->setSelectedFaceNormal(v3f());
        static thread_local const bool show_entity_selectionbox = g_settings->getBool(
                "show_entity_selectionbox");
 
@@ -3219,7 +3392,13 @@ PointedThing Game::updatePointedThing(
                        v3f pos = runData.selected_object->getPosition();
                        selectionboxes->push_back(aabb3f(selection_box));
                        hud->setSelectionPos(pos, camera_offset);
+                       GenericCAO* gcao = dynamic_cast<GenericCAO*>(runData.selected_object);
+                       if (gcao != nullptr && gcao->getProperties().rotate_selectionbox)
+                               hud->setSelectionRotation(gcao->getSceneNode()->getAbsoluteTransformation().getRotationDegrees());
+                       else
+                               hud->setSelectionRotation(v3f());
                }
+               hud->setSelectedFaceNormal(result.raw_intersection_normal);
        } else if (result.type == POINTEDTHING_NODE) {
                // Update selection boxes
                MapNode n = map.getNode(result.node_undersurface);
@@ -3237,10 +3416,8 @@ PointedThing Game::updatePointedThing(
                }
                hud->setSelectionPos(intToFloat(result.node_undersurface, BS),
                        camera_offset);
-               hud->setSelectedFaceNormal(v3f(
-                       result.intersection_normal.X,
-                       result.intersection_normal.Y,
-                       result.intersection_normal.Z));
+               hud->setSelectionRotation(v3f());
+               hud->setSelectedFaceNormal(result.intersection_normal);
        }
 
        // Update selection mesh light level and vertex colors
@@ -3265,7 +3442,7 @@ PointedThing Game::updatePointedThing(
                final_color_blend(&c, light_level, daynight_ratio);
 
                // Modify final color a bit with time
-               u32 timer = porting::getTimeMs() % 5000;
+               u32 timer = client->getEnv().getFrameTime() % 5000;
                float timerf = (float) (irr::core::PI * ((timer / 2500.0) - 0.5));
                float sin_r = 0.08f * std::sin(timerf);
                float sin_g = 0.08f * std::sin(timerf + irr::core::PI * 0.5f);
@@ -3294,7 +3471,7 @@ void Game::handlePointingAtNode(const PointedThing &pointed,
        const ItemStack &selected_item, const ItemStack &hand_item, f32 dtime)
 {
        v3s16 nodepos = pointed.node_undersurface;
-       v3s16 neighbourpos = pointed.node_abovesurface;
+       v3s16 neighborpos = pointed.node_abovesurface;
 
        /*
                Check information text of node
@@ -3338,7 +3515,7 @@ void Game::handlePointingAtNode(const PointedThing &pointed,
                // And also set the sound and send the interact
                // But first check for meta formspec and rightclickable
                auto &def = selected_item.getDefinition(itemdef_manager);
-               bool placed = nodePlacement(def, selected_item, nodepos, neighbourpos,
+               bool placed = nodePlacement(def, selected_item, nodepos, neighborpos,
                        pointed, meta);
 
                if (placed && client->modsLoaded())
@@ -3347,7 +3524,7 @@ void Game::handlePointingAtNode(const PointedThing &pointed,
 }
 
 bool Game::nodePlacement(const ItemDefinition &selected_def,
-       const ItemStack &selected_item, const v3s16 &nodepos, const v3s16 &neighbourpos,
+       const ItemStack &selected_item, const v3s16 &nodepos, const v3s16 &neighborpos,
        const PointedThing &pointed, const NodeMetadata *meta)
 {
        const auto &prediction = selected_def.node_placement_prediction;
@@ -3397,7 +3574,7 @@ bool Game::nodePlacement(const ItemDefinition &selected_def,
 
        verbosestream << "Node placement prediction for "
                << selected_def.name << " is " << prediction << std::endl;
-       v3s16 p = neighbourpos;
+       v3s16 p = neighborpos;
 
        // Place inside node itself if buildable_to
        MapNode n_under = map.getNode(nodepos, &is_valid_position);
@@ -3431,53 +3608,61 @@ bool Game::nodePlacement(const ItemDefinition &selected_def,
 
        const ContentFeatures &predicted_f = nodedef->get(id);
 
-       // Predict param2 for facedir and wallmounted nodes
-       // Compare core.item_place_node() for what the server does
-       u8 param2 = 0;
+       // Compare core.item_place_node() for what the server does with param2
+       MapNode predicted_node(id, 0, 0);
 
        const u8 place_param2 = selected_def.place_param2;
 
        if (place_param2) {
-               param2 = place_param2;
+               predicted_node.setParam2(place_param2);
        } else if (predicted_f.param_type_2 == CPT2_WALLMOUNTED ||
                        predicted_f.param_type_2 == CPT2_COLORED_WALLMOUNTED) {
-               v3s16 dir = nodepos - neighbourpos;
+               v3s16 dir = nodepos - neighborpos;
 
                if (abs(dir.Y) > MYMAX(abs(dir.X), abs(dir.Z))) {
-                       param2 = dir.Y < 0 ? 1 : 0;
+                       predicted_node.setParam2(dir.Y < 0 ? 1 : 0);
                } else if (abs(dir.X) > abs(dir.Z)) {
-                       param2 = dir.X < 0 ? 3 : 2;
+                       predicted_node.setParam2(dir.X < 0 ? 3 : 2);
                } else {
-                       param2 = dir.Z < 0 ? 5 : 4;
+                       predicted_node.setParam2(dir.Z < 0 ? 5 : 4);
                }
        } else if (predicted_f.param_type_2 == CPT2_FACEDIR ||
-                       predicted_f.param_type_2 == CPT2_COLORED_FACEDIR) {
+                       predicted_f.param_type_2 == CPT2_COLORED_FACEDIR ||
+                       predicted_f.param_type_2 == CPT2_4DIR ||
+                       predicted_f.param_type_2 == CPT2_COLORED_4DIR) {
                v3s16 dir = nodepos - floatToInt(client->getEnv().getLocalPlayer()->getPosition(), BS);
 
                if (abs(dir.X) > abs(dir.Z)) {
-                       param2 = dir.X < 0 ? 3 : 1;
+                       predicted_node.setParam2(dir.X < 0 ? 3 : 1);
                } else {
-                       param2 = dir.Z < 0 ? 2 : 0;
+                       predicted_node.setParam2(dir.Z < 0 ? 2 : 0);
                }
        }
 
        // Check attachment if node is in group attached_node
-       if (itemgroup_get(predicted_f.groups, "attached_node") != 0) {
-               const static v3s16 wallmounted_dirs[8] = {
-                       v3s16(0, 1, 0),
-                       v3s16(0, -1, 0),
-                       v3s16(1, 0, 0),
-                       v3s16(-1, 0, 0),
-                       v3s16(0, 0, 1),
-                       v3s16(0, 0, -1),
-               };
+       int an = itemgroup_get(predicted_f.groups, "attached_node");
+       if (an != 0) {
                v3s16 pp;
 
-               if (predicted_f.param_type_2 == CPT2_WALLMOUNTED ||
-                               predicted_f.param_type_2 == CPT2_COLORED_WALLMOUNTED)
-                       pp = p + wallmounted_dirs[param2];
-               else
+               if (an == 3) {
+                       pp = p + v3s16(0, -1, 0);
+               } else if (an == 4) {
+                       pp = p + v3s16(0, 1, 0);
+               } else if (an == 2) {
+                       if (predicted_f.param_type_2 == CPT2_FACEDIR ||
+                                       predicted_f.param_type_2 == CPT2_COLORED_FACEDIR ||
+                                       predicted_f.param_type_2 == CPT2_4DIR ||
+                                       predicted_f.param_type_2 == CPT2_COLORED_4DIR) {
+                               pp = p + facedir_dirs[predicted_node.getFaceDir(nodedef)];
+                       } else {
+                               pp = p;
+                       }
+               } else if (predicted_f.param_type_2 == CPT2_WALLMOUNTED ||
+                               predicted_f.param_type_2 == CPT2_COLORED_WALLMOUNTED) {
+                       pp = p + predicted_node.getWallMountedDir(nodedef);
+               } else {
                        pp = p + v3s16(0, -1, 0);
+               }
 
                if (!nodedef->get(map.getNode(pp)).walkable) {
                        soundmaker->m_player_rightpunch_sound = selected_def.sound_place_failed;
@@ -3490,39 +3675,41 @@ bool Game::nodePlacement(const ItemDefinition &selected_def,
        // Apply color
        if (!place_param2 && (predicted_f.param_type_2 == CPT2_COLOR
                        || predicted_f.param_type_2 == CPT2_COLORED_FACEDIR
+                       || predicted_f.param_type_2 == CPT2_COLORED_4DIR
                        || predicted_f.param_type_2 == CPT2_COLORED_WALLMOUNTED)) {
                const auto &indexstr = selected_item.metadata.
                        getString("palette_index", 0);
                if (!indexstr.empty()) {
                        s32 index = mystoi(indexstr);
                        if (predicted_f.param_type_2 == CPT2_COLOR) {
-                               param2 = index;
+                               predicted_node.setParam2(index);
                        } else if (predicted_f.param_type_2 == CPT2_COLORED_WALLMOUNTED) {
                                // param2 = pure palette index + other
-                               param2 = (index & 0xf8) | (param2 & 0x07);
+                               predicted_node.setParam2((index & 0xf8) | (predicted_node.getParam2() & 0x07));
                        } else if (predicted_f.param_type_2 == CPT2_COLORED_FACEDIR) {
                                // param2 = pure palette index + other
-                               param2 = (index & 0xe0) | (param2 & 0x1f);
+                               predicted_node.setParam2((index & 0xe0) | (predicted_node.getParam2() & 0x1f));
+                       } else if (predicted_f.param_type_2 == CPT2_COLORED_4DIR) {
+                               // param2 = pure palette index + other
+                               predicted_node.setParam2((index & 0xfc) | (predicted_node.getParam2() & 0x03));
                        }
                }
        }
 
        // Add node to client map
-       MapNode n(id, 0, param2);
-
        try {
                LocalPlayer *player = client->getEnv().getLocalPlayer();
 
-               // Dont place node when player would be inside new node
+               // Don't place node when player would be inside new node
                // NOTE: This is to be eventually implemented by a mod as client-side Lua
-               if (!nodedef->get(n).walkable ||
+               if (!predicted_f.walkable ||
                                g_settings->getBool("enable_build_where_you_stand") ||
                                (client->checkPrivilege("noclip") && g_settings->getBool("noclip")) ||
-                               (nodedef->get(n).walkable &&
-                                       neighbourpos != player->getStandingNodePos() + v3s16(0, 1, 0) &&
-                                       neighbourpos != player->getStandingNodePos() + v3s16(0, 2, 0))) {
+                               (predicted_f.walkable &&
+                                       neighborpos != player->getStandingNodePos() + v3s16(0, 1, 0) &&
+                                       neighborpos != player->getStandingNodePos() + v3s16(0, 2, 0))) {
                        // This triggers the required mesh update too
-                       client->addNode(p, n);
+                       client->addNode(p, predicted_node);
                        // Report to server
                        client->interact(INTERACT_PLACE, pointed);
                        // A node is predicted, also play a sound
@@ -3599,18 +3786,19 @@ void Game::handleDigging(const PointedThing &pointed, const v3s16 &nodepos,
        // See also: serverpackethandle.cpp, action == 2
        LocalPlayer *player = client->getEnv().getLocalPlayer();
        ClientMap &map = client->getEnv().getClientMap();
-       MapNode n = client->getEnv().getClientMap().getNode(nodepos);
+       MapNode n = map.getNode(nodepos);
+       const auto &features = nodedef_manager->get(n);
 
        // NOTE: Similar piece of code exists on the server side for
        // cheat detection.
        // Get digging parameters
-       DigParams params = getDigParams(nodedef_manager->get(n).groups,
+       DigParams params = getDigParams(features.groups,
                        &selected_item.getToolCapabilities(itemdef_manager),
                        selected_item.wear);
 
        // If can't dig, try hand
        if (!params.diggable) {
-               params = getDigParams(nodedef_manager->get(n).groups,
+               params = getDigParams(features.groups,
                                &hand_item.getToolCapabilities(itemdef_manager));
        }
 
@@ -3621,7 +3809,6 @@ void Game::handleDigging(const PointedThing &pointed, const v3s16 &nodepos,
                runData.dig_time_complete = params.time;
 
                if (m_cache_enable_particles) {
-                       const ContentFeatures &features = client->getNodeDefManager()->get(n);
                        client->getParticleManager()->addNodeParticle(client,
                                        player, nodepos, n, features);
                }
@@ -3632,6 +3819,7 @@ void Game::handleDigging(const PointedThing &pointed, const v3s16 &nodepos,
                runData.dig_instantly = runData.dig_time_complete == 0;
                if (client->modsLoaded() && client->getScript()->on_punchnode(nodepos, n))
                        return;
+
                client->interact(INTERACT_START_DIGGING, pointed);
                runData.digging = true;
                runData.btn_down_for_dig = true;
@@ -3646,7 +3834,7 @@ void Game::handleDigging(const PointedThing &pointed, const v3s16 &nodepos,
                runData.dig_index = crack_animation_length;
        }
 
-       SimpleSoundSpec sound_dig = nodedef_manager->get(n).sound_dig;
+       const auto &sound_dig = features.sound_dig;
 
        if (sound_dig.exists() && params.diggable) {
                if (sound_dig.name == "__group") {
@@ -3664,8 +3852,6 @@ void Game::handleDigging(const PointedThing &pointed, const v3s16 &nodepos,
        // Don't show cracks if not diggable
        if (runData.dig_time_complete >= 100000.0) {
        } else if (runData.dig_index < crack_animation_length) {
-               //TimeTaker timer("client.setTempMod");
-               //infostream<<"dig_index="<<dig_index<<std::endl;
                client->setCrack(runData.dig_index, nodepos);
        } else {
                infostream << "Digging completed" << std::endl;
@@ -3687,38 +3873,31 @@ void Game::handleDigging(const PointedThing &pointed, const v3s16 &nodepos,
                else if (runData.dig_instantly)
                        runData.nodig_delay_timer = 0.15;
 
-               bool is_valid_position;
-               MapNode wasnode = map.getNode(nodepos, &is_valid_position);
-               if (is_valid_position) {
-                       if (client->modsLoaded() &&
-                                       client->getScript()->on_dignode(nodepos, wasnode)) {
-                               return;
-                       }
+               if (client->modsLoaded() &&
+                               client->getScript()->on_dignode(nodepos, n)) {
+                       return;
+               }
 
-                       const ContentFeatures &f = client->ndef()->get(wasnode);
-                       if (f.node_dig_prediction == "air") {
-                               client->removeNode(nodepos);
-                       } else if (!f.node_dig_prediction.empty()) {
-                               content_t id;
-                               bool found = client->ndef()->getId(f.node_dig_prediction, id);
-                               if (found)
-                                       client->addNode(nodepos, id, true);
-                       }
-                       // implicit else: no prediction
+               if (features.node_dig_prediction == "air") {
+                       client->removeNode(nodepos);
+               } else if (!features.node_dig_prediction.empty()) {
+                       content_t id;
+                       bool found = nodedef_manager->getId(features.node_dig_prediction, id);
+                       if (found)
+                               client->addNode(nodepos, id, true);
                }
+               // implicit else: no prediction
 
                client->interact(INTERACT_DIGGING_COMPLETED, pointed);
 
                if (m_cache_enable_particles) {
-                       const ContentFeatures &features =
-                               client->getNodeDefManager()->get(wasnode);
                        client->getParticleManager()->addDiggingParticles(client,
-                               player, nodepos, wasnode, features);
+                               player, nodepos, n, features);
                }
 
 
                // Send event to trigger sound
-               client->getEventManager()->put(new NodeDugEvent(nodepos, wasnode));
+               client->getEventManager()->put(new NodeDugEvent(nodepos, n));
        }
 
        if (runData.dig_time_complete < 100000.0) {
@@ -3737,6 +3916,12 @@ void Game::updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime,
        TimeTaker tt_update("Game::updateFrame()");
        LocalPlayer *player = client->getEnv().getLocalPlayer();
 
+       /*
+               Frame time
+       */
+
+       client->getEnv().updateFrameTime(m_is_paused);
+
        /*
                Fog range
        */
@@ -3755,7 +3940,10 @@ void Game::updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime,
        float direct_brightness;
        bool sunlight_seen;
 
-       if (m_cache_enable_noclip && m_cache_enable_free_move) {
+       // When in noclip mode force same sky brightness as above ground so you
+       // can see properly
+       if (draw_control->allow_noclip && m_cache_enable_free_move &&
+               client->checkPrivilege("fly")) {
                direct_brightness = time_brightness;
                sunlight_seen = true;
        } else {
@@ -3895,6 +4083,9 @@ void Game::updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime,
                changed much
        */
        runData.update_draw_list_timer += dtime;
+       runData.touch_blocks_timer += dtime;
+
+       bool draw_list_updated = false;
 
        float update_draw_list_delta = 0.2f;
 
@@ -3906,6 +4097,12 @@ void Game::updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime,
                runData.update_draw_list_timer = 0;
                client->getEnv().getClientMap().updateDrawList();
                runData.update_draw_list_last_cam_dir = camera_direction;
+               draw_list_updated = true;
+       }
+
+       if (runData.touch_blocks_timer > update_draw_list_delta && !draw_list_updated) {
+               client->getEnv().getClientMap().touchMapBlocks();
+               runData.touch_blocks_timer = 0;
        }
 
        if (RenderingEngine::get_shadow_renderer()) {
@@ -3957,10 +4154,8 @@ void Game::updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime,
                        (player->hud_flags & HUD_FLAG_CROSSHAIR_VISIBLE) &&
                        (camera->getCameraMode() != CAMERA_MODE_THIRD_FRONT));
 #ifdef HAVE_TOUCHSCREENGUI
-       try {
-               draw_crosshair = !g_settings->getBool("touchtarget");
-       } catch (SettingNotFoundException) {
-       }
+       if (isNoCrosshairAllowed())
+               draw_crosshair = false;
 #endif
        m_rendering_engine->draw_scene(skycolor, m_game_ui->m_flags.show_hud,
                        m_game_ui->m_flags.show_minimap, draw_wield_tool, draw_crosshair);
@@ -3988,27 +4183,6 @@ void Game::updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime,
        /*
                ==================== End scene ====================
        */
-       if (++m_reset_HW_buffer_counter > 500) {
-               /*
-                 Periodically remove all mesh HW buffers.
-
-                 Work around for a quirk in Irrlicht where a HW buffer is only
-                 released after 20000 iterations (triggered from endScene()).
-
-                 Without this, all loaded but unused meshes will retain their HW
-                 buffers for at least 5 minutes, at which point looking up the HW buffers
-                 becomes a bottleneck and the framerate drops (as much as 30%).
-
-                 Tests showed that numbers between 50 and 1000 are good, so picked 500.
-                 There are no other public Irrlicht APIs that allow interacting with the
-                 HW buffers without tracking the status of every individual mesh.
-
-                 The HW buffers for _visible_ meshes will be reinitialized in the next frame.
-               */
-               infostream << "Game::updateFrame(): Removing all HW buffers." << std::endl;
-               driver->removeAllHardwareBuffers();
-               m_reset_HW_buffer_counter = 0;
-       }
 
        driver->endScene();
 
@@ -4036,13 +4210,15 @@ void Game::updateShadows()
 
        float in_timeofday = fmod(runData.time_of_day_smooth, 1.0f);
 
-       float timeoftheday = fmod(getWickedTimeOfDay(in_timeofday) + 0.75f, 0.5f) + 0.25f;
+       float timeoftheday = getWickedTimeOfDay(in_timeofday);
+       bool is_day = timeoftheday > 0.25 && timeoftheday < 0.75;
+       bool is_shadow_visible = is_day ? sky->getSunVisible() : sky->getMoonVisible();
+       shadow->setShadowIntensity(is_shadow_visible ? client->getEnv().getLocalPlayer()->getLighting().shadow_intensity : 0.0f);
+
+       timeoftheday = fmod(timeoftheday + 0.75f, 0.5f) + 0.25f;
        const float offset_constant = 10000.0f;
 
-       v3f light(0.0f, 0.0f, -1.0f);
-       light.rotateXZBy(90);
-       light.rotateXYBy(timeoftheday * 360 - 90);
-       light.rotateYZBy(sky->getSkyBodyOrbitTilt());
+       v3f light = is_day ? sky->getSunDirection() : sky->getMoonDirection();
 
        v3f sun_pos = light * offset_constant;
 
@@ -4068,10 +4244,10 @@ void FpsControl::reset()
  */
 void FpsControl::limit(IrrlichtDevice *device, f32 *dtime)
 {
-       const u64 frametime_min = 1000000.0f / (
-               device->isWindowFocused() && !g_menumgr.pausesGame()
+       const float fps_limit = (device->isWindowFocused() && !g_menumgr.pausesGame())
                        ? g_settings->getFloat("fps_max")
-                       : g_settings->getFloat("fps_max_unfocused"));
+                       : g_settings->getFloat("fps_max_unfocused");
+       const u64 frametime_min = 1000000.0f / std::max(fps_limit, 1.0f);
 
        u64 time = porting::getTimeUs();
 
@@ -4120,9 +4296,9 @@ void Game::readSettings()
        m_cache_enable_joysticks             = g_settings->getBool("enable_joysticks");
        m_cache_enable_particles             = g_settings->getBool("enable_particles");
        m_cache_enable_fog                   = g_settings->getBool("enable_fog");
-       m_cache_mouse_sensitivity            = g_settings->getFloat("mouse_sensitivity");
-       m_cache_joystick_frustum_sensitivity = g_settings->getFloat("joystick_frustum_sensitivity");
-       m_repeat_place_time                  = g_settings->getFloat("repeat_place_time");
+       m_cache_mouse_sensitivity            = g_settings->getFloat("mouse_sensitivity", 0.001f, 10.0f);
+       m_cache_joystick_frustum_sensitivity = std::max(g_settings->getFloat("joystick_frustum_sensitivity"), 0.001f);
+       m_repeat_place_time                  = g_settings->getFloat("repeat_place_time", 0.16f, 2.0);
 
        m_cache_enable_noclip                = g_settings->getBool("noclip");
        m_cache_enable_free_move             = g_settings->getBool("free_move");
@@ -4304,10 +4480,8 @@ void Game::showPauseMenu()
        GUIFormSpecMenu::create(formspec, client, m_rendering_engine->get_gui_env(),
                        &input->joystick, fs_src, txt_dst, client->getFormspecPrepend(), sound);
        formspec->setFocus("btn_continue");
+       // game will be paused in next step, if in singleplayer (see m_is_paused)
        formspec->doPause = true;
-
-       if (simple_singleplayer_mode)
-               pauseAnimation();
 }
 
 /****************************************************************************/