]> 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 31c782c51cc14702c6d0ea9987f0029fe14b78ce..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)
@@ -400,17 +401,13 @@ class GameOnDemandSoundFetcher: public OnDemandSoundFetcher
 };
 
 
-// before 1.8 there isn't a "integer interface", only float
-#if (IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR < 8)
-typedef f32 SamplerLayer_t;
-#else
 typedef s32 SamplerLayer_t;
-#endif
 
 
 class GameGlobalShaderConstantSetter : public IShaderConstantSetter
 {
        Sky *m_sky;
+       Client *m_client;
        bool *m_force_fog_off;
        f32 *m_fog_range;
        bool m_fog_enabled;
@@ -418,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;
@@ -425,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)
@@ -446,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"),
@@ -459,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()
@@ -506,51 +550,79 @@ 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();
-#if (IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR < 8)
-               eye_position_array[0] = epos.X;
-               eye_position_array[1] = epos.Y;
-               eye_position_array[2] = epos.Z;
-#else
                epos.getAs3Values(eye_position_array);
-#endif
                m_eye_position_pixel.set(eye_position_array, services);
                m_eye_position_vertex.set(eye_position_array, services);
 
                if (m_client->getMinimap()) {
                        float minimap_yaw_array[3];
                        v3f minimap_yaw = m_client->getMinimap()->getYawVec();
-#if (IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR < 8)
-                       minimap_yaw_array[0] = minimap_yaw.X;
-                       minimap_yaw_array[1] = minimap_yaw.Y;
-                       minimap_yaw_array[2] = minimap_yaw.Z;
-#else
                        minimap_yaw.getAs3Values(minimap_yaw_array);
-#endif
                        m_minimap_yaw.set(minimap_yaw_array, services);
                }
 
                float camera_offset_array[3];
                v3f offset = intToFloat(m_client->getCamera()->getOffset(), BS);
-#if (IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR < 8)
-               camera_offset_array[0] = offset.X;
-               camera_offset_array[1] = offset.Y;
-               camera_offset_array[2] = offset.Z;
-#else
                offset.getAs3Values(camera_offset_array);
-#endif
                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;
+               }
        }
 };
 
@@ -589,7 +661,7 @@ class GameGlobalShaderConstantSetterFactory : public IShaderConstantSetterFactor
        }
 };
 
-#ifdef __ANDROID__
+#ifdef HAVE_TOUCHSCREENGUI
 #define SIZE_TAG "size[11,5.5]"
 #else
 #define SIZE_TAG "size[11,5.5,true]" // Fixed size on desktop
@@ -598,10 +670,19 @@ class GameGlobalShaderConstantSetterFactory : public IShaderConstantSetterFactor
 /****************************************************************************
  ****************************************************************************/
 
-const float object_hit_delay = 0.2;
+const static float object_hit_delay = 0.2;
 
 struct FpsControl {
-       u32 last_time, busy_time, sleep_time;
+       FpsControl() : last_time(0), busy_time(0), sleep_time(0) {}
+
+       void reset();
+
+       void limit(IrrlichtDevice *device, f32 *dtime);
+
+       u32 getBusyMs() const { return busy_time / 1000; }
+
+       // all values in microseconds (us)
+       u64 last_time, busy_time, sleep_time;
 };
 
 
@@ -629,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;
 
@@ -665,6 +750,7 @@ class Game {
 
        bool startup(bool *kill,
                        InputHandler *input,
+                       RenderingEngine *rendering_engine,
                        const GameStartData &game_params,
                        std::string &error_message,
                        bool *reconnect,
@@ -675,8 +761,6 @@ class Game {
 
 protected:
 
-       void extendedResourceCleanup();
-
        // Basic initialisation
        bool init(const std::string &map_dir, const std::string &address,
                        u16 port, const SubgameSpec &gamespec);
@@ -700,6 +784,7 @@ class Game {
        bool handleCallbacks();
        void processQueues();
        void updateProfilers(const RunStats &stats, const FpsControl &draw_times, f32 dtime);
+       void updateDebugState();
        void updateStats(RunStats *stats, const FpsControl &draw_times, f32 dtime);
        void updateProfilerGraphs(ProfilerGraph *graph);
 
@@ -717,6 +802,7 @@ class Game {
        void toggleFast();
        void toggleNoClip();
        void toggleCinematic();
+       void toggleBlockBounds();
        void toggleAutoforward();
 
        void toggleMinimap(bool shift_pressed);
@@ -732,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(u32 busy_time, f32 dtime);
+       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.
@@ -762,10 +848,9 @@ class Game {
                        const ItemStack &selected_item, const ItemStack &hand_item, f32 dtime);
        void updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime,
                        const CameraOrientation &cam);
+       void updateShadows();
 
        // Misc
-       void limitFps(FpsControl *fps_timings, f32 *dtime);
-
        void showOverlayMessage(const char *msg, float dtime, int percent,
                        bool draw_clouds = true);
 
@@ -825,20 +910,24 @@ class Game {
                CameraOrientation *cam);
        void handleClientEvent_CloudParams(ClientEvent *event, CameraOrientation *cam);
 
-       void updateChat(f32 dtime, const v2u32 &screensize);
+       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;
 
@@ -856,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()
@@ -878,6 +966,7 @@ class Game {
           these items (e.g. device)
        */
        IrrlichtDevice *device;
+       RenderingEngine *m_rendering_engine;
        video::IVideoDriver *driver;
        scene::ISceneManager *smgr;
        bool *kill;
@@ -920,9 +1009,18 @@ class Game {
 
        bool m_does_lost_focus_pause_game = false;
 
-       int m_reset_HW_buffer_counter = 0;
-#ifdef __ANDROID__
+       // 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;
 #endif
 };
@@ -960,7 +1058,7 @@ Game::Game() :
 
        readSettings();
 
-#ifdef __ANDROID__
+#ifdef HAVE_TOUCHSCREENGUI
        m_cache_hold_aux1 = false;      // This is initialised properly later
 #endif
 
@@ -991,7 +1089,7 @@ Game::~Game()
        delete itemdef_manager;
        delete draw_control;
 
-       extendedResourceCleanup();
+       clearTextureNameCache();
 
        g_settings->deregisterChangedCallback("doubletap_jump",
                &settingChangedCallback, this);
@@ -1019,6 +1117,7 @@ Game::~Game()
 
 bool Game::startup(bool *kill,
                InputHandler *input,
+               RenderingEngine *rendering_engine,
                const GameStartData &start_data,
                std::string &error_message,
                bool *reconnect,
@@ -1026,21 +1125,21 @@ bool Game::startup(bool *kill,
 {
 
        // "cache"
-       this->device              = RenderingEngine::get_raw_device();
+       m_rendering_engine        = rendering_engine;
+       device                    = m_rendering_engine->get_raw_device();
        this->kill                = kill;
        this->error_message       = &error_message;
-       this->reconnect_requested = reconnect;
+       reconnect_requested       = reconnect;
        this->input               = input;
        this->chat_backend        = chat_backend;
-       this->simple_singleplayer_mode = start_data.isSinglePlayer();
+       simple_singleplayer_mode  = start_data.isSinglePlayer();
 
        input->keycache.populate();
 
        driver = device->getVideoDriver();
-       smgr = RenderingEngine::get_scene_manager();
+       smgr = m_rendering_engine->get_scene_manager();
 
-       RenderingEngine::get_scene_manager()->getParameters()->
-               setAttribute(scene::OBJ_LOADER_IGNORE_MATERIAL_FILES, true);
+       smgr->getParameters()->setAttribute(scene::OBJ_LOADER_IGNORE_MATERIAL_FILES, true);
 
        // Reinit runData
        runData = GameRunData();
@@ -1051,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
@@ -1061,7 +1164,7 @@ bool Game::startup(bool *kill,
        if (!createClient(start_data))
                return false;
 
-       RenderingEngine::initialize(client, hud);
+       m_rendering_engine->initialize(client, hud);
 
        return true;
 }
@@ -1070,50 +1173,63 @@ bool Game::startup(bool *kill,
 void Game::run()
 {
        ProfilerGraph graph;
-       RunStats stats              = { 0 };
-       CameraOrientation cam_view_target  = { 0 };
-       CameraOrientation cam_view  = { 0 };
-       FpsControl draw_times       = { 0 };
+       RunStats stats = {};
+       CameraOrientation cam_view_target = {};
+       CameraOrientation cam_view = {};
+       FpsControl draw_times;
        f32 dtime; // in seconds
 
        /* Clear the profiler */
        Profiler::GraphValues dummyvalues;
        g_profiler->graphGet(dummyvalues);
 
-       draw_times.last_time = RenderingEngine::get_timer_time();
+       draw_times.reset();
 
        set_light_table(g_settings->getFloat("display_gamma"));
 
-#ifdef __ANDROID__
+#ifdef HAVE_TOUCHSCREENGUI
        m_cache_hold_aux1 = g_settings->getBool("fast_move")
                        && 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 (RenderingEngine::run()
+       while (m_rendering_engine->run()
                        && !(*kill || g_gamecallback->shutdown_requested
                        || (server && server->isShutdownRequested()))) {
 
-               const irr::core::dimension2d<u32> &current_screen_size =
-                       RenderingEngine::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 =
-               //    RenderingEngine::run() from this iteration
-               //  + Sleep time until the wanted FPS are reached
-               limitFps(&draw_times, &dtime);
-
                // Prepare render data for next iteration
 
                updateStats(&stats, draw_times, dtime);
@@ -1127,7 +1243,6 @@ void Game::run()
                processQueues();
 
                m_game_ui->clearInfoText();
-               hud->resizeHotbar();
 
                updateProfilers(stats, draw_times, dtime);
                processUserInput(dtime);
@@ -1138,12 +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);
-               updateCamera(draw_times.busy_time, dtime);
+               updateDebugState();
+               updateCamera(dtime);
                updateSound(dtime);
-               processPlayerInteraction(dtime, m_game_ui->m_flags.show_hud,
-                       m_game_ui->m_flags.show_debug);
+               processPlayerInteraction(dtime, m_game_ui->m_flags.show_hud);
                updateFrame(&graph, &stats, dtime, cam_view);
                updateProfilerGraphs(&graph);
 
@@ -1159,12 +1288,8 @@ void Game::run()
 
 void Game::shutdown()
 {
-       RenderingEngine::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
+       m_rendering_engine->finalize();
+
        auto formspec = m_game_ui->getFormspecGUI();
        if (formspec)
                formspec->quitMenu();
@@ -1297,9 +1422,8 @@ bool Game::createSingleplayerServer(const std::string &map_dir,
        }
 
        if (bind_addr.isIPv6() && !g_settings->getBool("enable_ipv6")) {
-               *error_message = "Unable to listen on " +
-                               bind_addr.serializeString() +
-                               " because IPv6 is disabled";
+               *error_message = fmtgettext("Unable to listen on %s because IPv6 is disabled",
+                       bind_addr.serializeString().c_str());
                errorstream << *error_message << std::endl;
                return false;
        }
@@ -1315,7 +1439,7 @@ bool Game::createClient(const GameStartData &start_data)
 {
        showOverlayMessage(N_("Creating client..."), 0, 10);
 
-       draw_control = new MapDrawControl;
+       draw_control = new MapDrawControl();
        if (!draw_control)
                return false;
 
@@ -1332,7 +1456,7 @@ bool Game::createClient(const GameStartData &start_data)
        if (!could_connect) {
                if (error_message->empty() && !connect_aborted) {
                        // Should not happen if error messages are set properly
-                       *error_message = "Connection failed for unknown reason";
+                       *error_message = gettext("Connection failed for unknown reason");
                        errorstream << *error_message << std::endl;
                }
                return false;
@@ -1341,7 +1465,7 @@ bool Game::createClient(const GameStartData &start_data)
        if (!getServerContent(&connect_aborted)) {
                if (error_message->empty() && !connect_aborted) {
                        // Should not happen if error messages are set properly
-                       *error_message = "Connection failed for unknown reason";
+                       *error_message = gettext("Connection failed for unknown reason");
                        errorstream << *error_message << std::endl;
                }
                return false;
@@ -1356,10 +1480,15 @@ bool Game::createClient(const GameStartData &start_data)
 
        /* Camera
         */
-       camera = new Camera(*draw_control, client);
-       if (!camera->successfullyCreated(*error_message))
-               return false;
+       camera = new Camera(*draw_control, client, m_rendering_engine);
+       if (client->modsLoaded())
+               client->getScript()->on_camera_ready(camera);
        client->setCamera(camera);
+#ifdef HAVE_TOUCHSCREENGUI
+       if (g_touchscreengui) {
+               g_touchscreengui->setUseCrosshair(!isNoCrosshairAllowed());
+       }
+#endif
 
        /* Clouds
         */
@@ -1368,7 +1497,7 @@ bool Game::createClient(const GameStartData &start_data)
 
        /* Skybox
         */
-       sky = new Sky(-1, texture_src, shader_src);
+       sky = new Sky(-1, m_rendering_engine, texture_src, shader_src);
        scsf->setSky(sky);
        skybox = NULL;  // This is used/set later on in the main run loop
 
@@ -1390,16 +1519,28 @@ bool Game::createClient(const GameStartData &start_data)
        std::wstring str = utf8_to_wide(PROJECT_NAME_C);
        str += L" ";
        str += utf8_to_wide(g_version_hash);
+       {
+               const wchar_t *text = nullptr;
+               if (simple_singleplayer_mode)
+                       text = wgettext("Singleplayer");
+               else
+                       text = wgettext("Multiplayer");
+               str += L" [";
+               str += text;
+               str += L"]";
+               delete[] text;
+       }
        str += L" [";
        str += driver->getName();
        str += L"]";
+
        device->setWindowCaption(str.c_str());
 
        LocalPlayer *player = client->getEnv().getLocalPlayer();
        player->hurt_tilt_timer = 0;
        player->hurt_tilt_strength = 0;
 
-       hud = new Hud(guienv, client, player, &player->inventory);
+       hud = new Hud(client, player, &player->inventory);
 
        mapper = client->getMinimap();
 
@@ -1448,7 +1589,6 @@ bool Game::connectToServer(const GameStartData &start_data,
                connect_address.Resolve(start_data.address.c_str());
 
                if (connect_address.isZero()) { // i.e. INADDR_ANY, IN6ADDR_ANY
-                       //connect_address.Resolve("localhost");
                        if (connect_address.isIPv6()) {
                                IPv6AddressBytes addr_bytes;
                                addr_bytes.bytes[15] = 1;
@@ -1459,29 +1599,36 @@ bool Game::connectToServer(const GameStartData &start_data,
                        local_server_mode = true;
                }
        } catch (ResolveError &e) {
-               *error_message = std::string("Couldn't resolve address: ") + e.what();
+               *error_message = fmtgettext("Couldn't resolve address: %s", e.what());
+
                errorstream << *error_message << std::endl;
                return false;
        }
 
        if (connect_address.isIPv6() && !g_settings->getBool("enable_ipv6")) {
-               *error_message = "Unable to connect to " +
-                               connect_address.serializeString() +
-                               " because IPv6 is disabled";
+               *error_message = fmtgettext("Unable to connect to %s because IPv6 is disabled", connect_address.serializeString().c_str());
                errorstream << *error_message << std::endl;
                return false;
        }
 
-       client = new Client(start_data.name.c_str(),
-                       start_data.password, start_data.address,
-                       *draw_control, texture_src, shader_src,
-                       itemdef_manager, nodedef_manager, sound, eventmgr,
-                       connect_address.isIPv6(), m_game_ui.get());
+       try {
+               client = new Client(start_data.name.c_str(),
+                               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(),
+                               start_data.allow_login_or_register);
+               client->migrateModStorage();
+       } catch (const BaseException &e) {
+               *error_message = fmtgettext("Error creating client: %s", e.what());
+               errorstream << *error_message << std::endl;
+               return false;
+       }
 
        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,
@@ -1494,15 +1641,15 @@ bool Game::connectToServer(const GameStartData &start_data,
        try {
                input->clear();
 
-               FpsControl fps_control = { 0 };
+               FpsControl fps_control;
                f32 dtime;
                f32 wait_time = 0; // in seconds
 
-               fps_control.last_time = RenderingEngine::get_timer_time();
+               fps_control.reset();
 
-               while (RenderingEngine::run()) {
+               while (m_rendering_engine->run()) {
 
-                       limitFps(&fps_control, &dtime);
+                       fps_control.limit(device, &dtime);
 
                        // Update client and server
                        client->step(dtime);
@@ -1521,8 +1668,7 @@ bool Game::connectToServer(const GameStartData &start_data,
                                break;
 
                        if (client->accessDenied()) {
-                               *error_message = "Access denied. Reason: "
-                                               + client->accessDeniedReason();
+                               *error_message = fmtgettext("Access denied. Reason: %s", client->accessDeniedReason().c_str());
                                *reconnect_requested = client->reconnectRequested();
                                errorstream << *error_message << std::endl;
                                break;
@@ -1534,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
-                                       RenderingEngine::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.isSinglePlayer() && wait_time > 10) {
-                                       *error_message = "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
@@ -1570,14 +1704,14 @@ bool Game::getServerContent(bool *aborted)
 {
        input->clear();
 
-       FpsControl fps_control = { 0 };
+       FpsControl fps_control;
        f32 dtime; // in seconds
 
-       fps_control.last_time = RenderingEngine::get_timer_time();
+       fps_control.reset();
 
-       while (RenderingEngine::run()) {
+       while (m_rendering_engine->run()) {
 
-               limitFps(&fps_control, &dtime);
+               fps_control.limit(device, &dtime);
 
                // Update client and server
                client->step(dtime);
@@ -1596,7 +1730,7 @@ bool Game::getServerContent(bool *aborted)
                        return false;
 
                if (client->getState() < LC_Init) {
-                       *error_message = "Client disconnected";
+                       *error_message = gettext("Client disconnected");
                        errorstream << *error_message << std::endl;
                        return false;
                }
@@ -1613,17 +1747,17 @@ bool Game::getServerContent(bool *aborted)
                if (!client->itemdefReceived()) {
                        const wchar_t *text = wgettext("Item definitions...");
                        progress = 25;
-                       RenderingEngine::draw_load_screen(text, guienv, texture_src,
+                       m_rendering_engine->draw_load_screen(text, guienv, texture_src,
                                dtime, progress);
                        delete[] text;
                } else if (!client->nodedefReceived()) {
                        const wchar_t *text = wgettext("Node definitions...");
                        progress = 30;
-                       RenderingEngine::draw_load_screen(text, guienv, texture_src,
+                       m_rendering_engine->draw_load_screen(text, guienv, texture_src,
                                dtime, progress);
                        delete[] text;
                } else {
-                       std::stringstream message;
+                       std::ostringstream message;
                        std::fixed(message);
                        message.precision(0);
                        float receive = client->mediaReceiveProgress() * 100;
@@ -1646,7 +1780,7 @@ bool Game::getServerContent(bool *aborted)
                        }
 
                        progress = 30 + client->mediaReceiveProgress() * 35 + 0.5;
-                       RenderingEngine::draw_load_screen(utf8_to_wide(message.str()), guienv,
+                       m_rendering_engine->draw_load_screen(utf8_to_wide(message.str()), guienv,
                                texture_src, dtime, progress);
                }
        }
@@ -1678,8 +1812,7 @@ inline void Game::updateInteractTimers(f32 dtime)
 inline bool Game::checkConnection()
 {
        if (client->accessDenied()) {
-               *error_message = "Access denied. Reason: "
-                               + client->accessDeniedReason();
+               *error_message = fmtgettext("Access denied. Reason: %s", client->accessDeniedReason().c_str());
                *reconnect_requested = client->reconnectRequested();
                errorstream << *error_message << std::endl;
                return false;
@@ -1732,6 +1865,29 @@ void Game::processQueues()
        shader_src->processQueue();
 }
 
+void Game::updateDebugState()
+{
+       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)
+                       m_game_ui->m_flags.show_basic_debug = false;
+       } else if (m_game_ui->m_flags.show_minimal_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,
                f32 dtime)
@@ -1756,10 +1912,10 @@ void Game::updateProfilers(const RunStats &stats, const FpsControl &draw_times,
        }
 
        // Update update graphs
-       g_profiler->graphAdd("Time non-rendering [ms]",
+       g_profiler->graphAdd("Time non-rendering [us]",
                draw_times.busy_time - stats.drawtime);
 
-       g_profiler->graphAdd("Sleep [ms]", draw_times.sleep_time);
+       g_profiler->graphAdd("Sleep [us]", draw_times.sleep_time);
        g_profiler->graphAdd("FPS", 1.0f / dtime);
 }
 
@@ -1792,9 +1948,9 @@ void Game::updateStats(RunStats *stats, const FpsControl &draw_times,
        /* Busytime average and jitter calculation
         */
        jp = &stats->busy_time_jitter;
-       jp->avg = jp->avg + draw_times.busy_time * 0.02;
+       jp->avg = jp->avg + draw_times.getBusyMs() * 0.02;
 
-       jitter = draw_times.busy_time - jp->avg;
+       jitter = draw_times.getBusyMs() - jp->avg;
 
        if (jitter > jp->max)
                jp->max = jitter;
@@ -1831,6 +1987,7 @@ void Game::processUserInput(f32 dtime)
        else if (g_touchscreengui) {
                /* on touchscreengui step may generate own input events which ain't
                 * what we want in case we just did clear them */
+               g_touchscreengui->show();
                g_touchscreengui->step(dtime);
        }
 #endif
@@ -1851,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);
@@ -1885,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)) {
@@ -1912,25 +2071,19 @@ 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);
-                       wchar_t buf[100];
+                       float new_volume = g_settings->getFloat("sound_volume", 0.0f, 0.9f) + 0.1f;
                        g_settings->setFloat("sound_volume", new_volume);
-                       const wchar_t *str = wgettext("Volume changed to %d%%");
-                       swprintf(buf, sizeof(buf) / sizeof(wchar_t), str, myround(new_volume * 100));
-                       delete[] str;
-                       m_game_ui->showStatusText(buf);
+                       std::wstring msg = fwgettext("Volume changed to %d%%", myround(new_volume * 100));
+                       m_game_ui->showStatusText(msg);
                } else {
                        m_game_ui->showTranslatedStatusText("Sound system is disabled");
                }
        } 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);
-                       wchar_t buf[100];
+                       float new_volume = g_settings->getFloat("sound_volume", 0.1f, 1.0f) - 0.1f;
                        g_settings->setFloat("sound_volume", new_volume);
-                       const wchar_t *str = wgettext("Volume changed to %d%%");
-                       swprintf(buf, sizeof(buf) / sizeof(wchar_t), str, myround(new_volume * 100));
-                       delete[] str;
-                       m_game_ui->showStatusText(buf);
+                       std::wstring msg = fwgettext("Volume changed to %d%%", myround(new_volume * 100));
+                       m_game_ui->showStatusText(msg);
                } else {
                        m_game_ui->showTranslatedStatusText("Sound system is disabled");
                }
@@ -1943,12 +2096,14 @@ void Game::processKeyInput()
                toggleCinematic();
        } else if (wasKeyDown(KeyType::SCREENSHOT)) {
                client->makeScreenshot();
+       } else if (wasKeyDown(KeyType::TOGGLE_BLOCK_BOUNDS)) {
+               toggleBlockBounds();
        } else if (wasKeyDown(KeyType::TOGGLE_HUD)) {
                m_game_ui->toggleHud();
        } 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)) {
@@ -1977,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()) {
@@ -1992,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);
@@ -2019,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);
 }
 
 
@@ -2051,15 +2208,22 @@ void Game::openInventory()
        InventoryLocation inventoryloc;
        inventoryloc.setCurrentPlayer();
 
-       if (!client->modsLoaded()
-                       || !client->getScript()->on_inventory_open(fs_src->m_client->getInventory(inventoryloc))) {
-               TextDest *txt_dst = new TextDestPlayerInventory(client);
-               auto *&formspec = m_game_ui->updateFormspec("");
-               GUIFormSpecMenu::create(formspec, client, &input->joystick, fs_src,
-                       txt_dst, client->getFormspecPrepend(), sound);
+       if (client->modsLoaded() && client->getScript()->on_inventory_open(fs_src->m_client->getInventory(inventoryloc))) {
+               delete fs_src;
+               return;
+       }
 
-               formspec->setFormSpec(fs_src->getForm(), inventoryloc);
+       if (fs_src->getForm().empty()) {
+               delete fs_src;
+               return;
        }
+
+       TextDest *txt_dst = new TextDestPlayerInventory(client);
+       auto *&formspec = m_game_ui->updateFormspec("");
+       GUIFormSpecMenu::create(formspec, client, m_rendering_engine->get_gui_env(),
+               &input->joystick, fs_src, txt_dst, client->getFormspecPrepend(), sound);
+
+       formspec->setFormSpec(fs_src->getForm(), inventoryloc);
 }
 
 
@@ -2111,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;
@@ -2147,7 +2318,7 @@ void Game::toggleFast()
                m_game_ui->showTranslatedStatusText("Fast mode disabled");
        }
 
-#ifdef __ANDROID__
+#ifdef HAVE_TOUCHSCREENGUI
        m_cache_hold_aux1 = fast_move && has_fast_privs;
 #endif
 }
@@ -2180,6 +2351,32 @@ void Game::toggleCinematic()
                m_game_ui->showTranslatedStatusText("Cinematic mode disabled");
 }
 
+void Game::toggleBlockBounds()
+{
+       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;
+       }
+}
+
 // Autoforward by toggling continuous forward.
 void Game::toggleAutoforward()
 {
@@ -2243,27 +2440,44 @@ void Game::toggleFog()
 
 void Game::toggleDebug()
 {
-       // Initial / 4x toggle: Chat only
-       // 1x toggle: Debug text with chat
+       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
-       // 3x toggle: Debug text and wireframe
-       if (!m_game_ui->m_flags.show_debug) {
-               m_game_ui->m_flags.show_debug = true;
+       // 3x toggle: Debug text and wireframe (needs "debug" priv)
+       // Next toggle: Back to initial
+       //
+       // 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 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)
+                       m_game_ui->m_flags.show_basic_debug = true;
                m_game_ui->m_flags.show_profiler_graph = false;
                draw_control->show_wireframe = false;
                m_game_ui->showTranslatedStatusText("Debug info shown");
        } else if (!m_game_ui->m_flags.show_profiler_graph && !draw_control->show_wireframe) {
+               if (has_basic_debug)
+                       m_game_ui->m_flags.show_basic_debug = true;
                m_game_ui->m_flags.show_profiler_graph = true;
                m_game_ui->showTranslatedStatusText("Profiler graph shown");
        } else if (!draw_control->show_wireframe && client->checkPrivilege("debug")) {
+               if (has_basic_debug)
+                       m_game_ui->m_flags.show_basic_debug = true;
                m_game_ui->m_flags.show_profiler_graph = false;
                draw_control->show_wireframe = true;
                m_game_ui->showTranslatedStatusText("Wireframe shown");
        } else {
-               m_game_ui->m_flags.show_debug = false;
+               m_game_ui->m_flags.show_minimal_debug = false;
+               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");
@@ -2287,20 +2501,13 @@ void Game::increaseViewRange()
        s16 range = g_settings->getS16("viewing_range");
        s16 range_new = range + 10;
 
-       wchar_t buf[255];
-       const wchar_t *str;
        if (range_new > 4000) {
                range_new = 4000;
-               str = wgettext("Viewing range is at maximum: %d");
-               swprintf(buf, sizeof(buf) / sizeof(wchar_t), str, range_new);
-               delete[] str;
-               m_game_ui->showStatusText(buf);
-
+               std::wstring msg = fwgettext("Viewing range is at maximum: %d", range_new);
+               m_game_ui->showStatusText(msg);
        } else {
-               str = wgettext("Viewing range changed to %d");
-               swprintf(buf, sizeof(buf) / sizeof(wchar_t), str, range_new);
-               delete[] str;
-               m_game_ui->showStatusText(buf);
+               std::wstring msg = fwgettext("Viewing range changed to %d", range_new);
+               m_game_ui->showStatusText(msg);
        }
        g_settings->set("viewing_range", itos(range_new));
 }
@@ -2311,19 +2518,13 @@ void Game::decreaseViewRange()
        s16 range = g_settings->getS16("viewing_range");
        s16 range_new = range - 10;
 
-       wchar_t buf[255];
-       const wchar_t *str;
        if (range_new < 20) {
                range_new = 20;
-               str = wgettext("Viewing range is at minimum: %d");
-               swprintf(buf, sizeof(buf) / sizeof(wchar_t), str, range_new);
-               delete[] str;
-               m_game_ui->showStatusText(buf);
+               std::wstring msg = fwgettext("Viewing range is at minimum: %d", range_new);
+               m_game_ui->showStatusText(msg);
        } else {
-               str = wgettext("Viewing range changed to %d");
-               swprintf(buf, sizeof(buf) / sizeof(wchar_t), str, range_new);
-               delete[] str;
-               m_game_ui->showStatusText(buf);
+               std::wstring msg = fwgettext("Viewing range changed to %d", range_new);
+               m_game_ui->showStatusText(msg);
        }
        g_settings->set("viewing_range", itos(range_new));
 }
@@ -2348,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()) {
 
@@ -2393,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
@@ -2420,7 +2641,7 @@ void Game::updateCameraOrientation(CameraOrientation *cam, float dtime)
 
        if (m_cache_enable_joysticks) {
                f32 sens_scale = getSensitivityScaleFactor();
-               f32 c = m_cache_joystick_frustum_sensitivity * (1.f / 32767.f) * dtime * sens_scale;
+               f32 c = m_cache_joystick_frustum_sensitivity * dtime * sens_scale;
                cam->camera_yaw -= input->joystick.getAxisWithoutDead(JA_FRUSTUM_HORIZONTAL) * c;
                cam->camera_pitch += input->joystick.getAxisWithoutDead(JA_FRUSTUM_VERTICAL) * c;
        }
@@ -2431,18 +2652,16 @@ void Game::updateCameraOrientation(CameraOrientation *cam, float dtime)
 
 void Game::updatePlayerControl(const CameraOrientation &cam)
 {
-       //TimeTaker tt("update player control", NULL, PRECISION_NANO);
+       LocalPlayer *player = client->getEnv().getLocalPlayer();
 
-       // DO NOT use the isKeyDown method for the forward, backward, left, right
-       // buttons, as the code that uses the controls needs to be able to
-       // distinguish between the two in order to know when to use joysticks.
+       //TimeTaker tt("update player control", NULL, PRECISION_NANO);
 
        PlayerControl control(
-               input->isKeyDown(KeyType::FORWARD),
-               input->isKeyDown(KeyType::BACKWARD),
-               input->isKeyDown(KeyType::LEFT),
-               input->isKeyDown(KeyType::RIGHT),
-               isKeyDown(KeyType::JUMP),
+               isKeyDown(KeyType::FORWARD),
+               isKeyDown(KeyType::BACKWARD),
+               isKeyDown(KeyType::LEFT),
+               isKeyDown(KeyType::RIGHT),
+               isKeyDown(KeyType::JUMP) || player->getAutojump(),
                isKeyDown(KeyType::AUX1),
                isKeyDown(KeyType::SNEAK),
                isKeyDown(KeyType::ZOOM),
@@ -2450,73 +2669,42 @@ void Game::updatePlayerControl(const CameraOrientation &cam)
                isKeyDown(KeyType::PLACE),
                cam.camera_pitch,
                cam.camera_yaw,
-               input->joystick.getAxisWithoutDead(JA_SIDEWARD_MOVE),
-               input->joystick.getAxisWithoutDead(JA_FORWARD_MOVE)
+               input->getMovementSpeed(),
+               input->getMovementDirection()
        );
 
-       u32 keypress_bits = (
-                       ( (u32)(isKeyDown(KeyType::FORWARD)                       & 0x1) << 0) |
-                       ( (u32)(isKeyDown(KeyType::BACKWARD)                      & 0x1) << 1) |
-                       ( (u32)(isKeyDown(KeyType::LEFT)                          & 0x1) << 2) |
-                       ( (u32)(isKeyDown(KeyType::RIGHT)                         & 0x1) << 3) |
-                       ( (u32)(isKeyDown(KeyType::JUMP)                          & 0x1) << 4) |
-                       ( (u32)(isKeyDown(KeyType::AUX1)                          & 0x1) << 5) |
-                       ( (u32)(isKeyDown(KeyType::SNEAK)                         & 0x1) << 6) |
-                       ( (u32)(isKeyDown(KeyType::DIG)                           & 0x1) << 7) |
-                       ( (u32)(isKeyDown(KeyType::PLACE)                         & 0x1) << 8) |
-                       ( (u32)(isKeyDown(KeyType::ZOOM)                          & 0x1) << 9)
-               );
+       // autoforward if set: move at maximum speed
+       if (player->getPlayerSettings().continuous_forward &&
+                       client->activeObjectsReceived() && !player->isDead()) {
+               control.movement_speed = 1.0f;
+               // sideways movement only
+               float dx = sin(control.movement_direction);
+               control.movement_direction = atan2(dx, 1.0f);
+       }
 
-#ifdef ANDROID
-       /* For Android, simulate holding down AUX1 (fast move) if the user has
+#ifdef HAVE_TOUCHSCREENGUI
+       /* For touch, simulate holding down AUX1 (fast move) if the user has
         * the fast_move setting toggled on. If there is an aux1 key defined for
-        * Android then its meaning is inverted (i.e. holding aux1 means walk and
+        * touch then its meaning is inverted (i.e. holding aux1 means walk and
         * not fast)
         */
        if (m_cache_hold_aux1) {
                control.aux1 = control.aux1 ^ true;
-               keypress_bits ^= ((u32)(1U << 5));
        }
 #endif
 
-       LocalPlayer *player = client->getEnv().getLocalPlayer();
-
-       // autojump if set: simulate "jump" key
-       if (player->getAutojump()) {
-               control.jump = true;
-               keypress_bits |= 1U << 4;
-       }
-
-       // autoforward if set: simulate "up" key
-       if (player->getPlayerSettings().continuous_forward &&
-                       client->activeObjectsReceived() && !player->isDead()) {
-               control.up = true;
-               keypress_bits |= 1U << 0;
-       }
-
        client->setPlayerControl(control);
-       player->keyPressed = keypress_bits;
 
        //tt.stop();
 }
 
 
-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) {
@@ -2577,16 +2765,23 @@ 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) {
-               runData.damage_flash += 95.0f + 3.2f * event->player_damage.amount;
-               runData.damage_flash = MYMIN(runData.damage_flash, 127.0f);
-
                LocalPlayer *player = client->getEnv().getLocalPlayer();
 
+               f32 hp_max = player->getCAO() ?
+                       player->getCAO()->getProperties().hp_max : PLAYER_MAX_HP_DEFAULT;
+               f32 damage_ratio = event->player_damage.amount / hp_max;
+
+               runData.damage_flash += 95.0f + 64.f * damage_ratio;
+               runData.damage_flash = MYMIN(runData.damage_flash, 127.0f);
+
                player->hurt_tilt_timer = 1.5f;
                player->hurt_tilt_strength =
-                       rangelim(event->player_damage.amount / 4.0f, 1.0f, 4.0f);
+                       rangelim(damage_ratio * 5.0f, 1.0f, 4.0f);
        }
 
        // Play damage sound
@@ -2630,8 +2825,8 @@ void Game::handleClientEvent_ShowFormSpec(ClientEvent *event, CameraOrientation
                        new TextDestPlayerInventory(client, *(event->show_formspec.formname));
 
                auto *&formspec = m_game_ui->updateFormspec(*(event->show_formspec.formname));
-               GUIFormSpecMenu::create(formspec, client, &input->joystick,
-                       fs_src, txt_dst, client->getFormspecPrepend(), sound);
+               GUIFormSpecMenu::create(formspec, client, m_rendering_engine->get_gui_env(),
+                       &input->joystick, fs_src, txt_dst, client->getFormspecPrepend(), sound);
        }
 
        delete event->show_formspec.formspec;
@@ -2643,8 +2838,8 @@ void Game::handleClientEvent_ShowLocalFormSpec(ClientEvent *event, CameraOrienta
        FormspecFormSource *fs_src = new FormspecFormSource(*event->show_formspec.formspec);
        LocalFormspecHandler *txt_dst =
                new LocalFormspecHandler(*event->show_formspec.formname, client);
-       GUIFormSpecMenu::create(m_game_ui->getFormspecGUI(), client, &input->joystick,
-                       fs_src, txt_dst, client->getFormspecPrepend(), sound);
+       GUIFormSpecMenu::create(m_game_ui->getFormspecGUI(), client, m_rendering_engine->get_gui_env(),
+                       &input->joystick, fs_src, txt_dst, client->getFormspecPrepend(), sound);
 
        delete event->show_formspec.formspec;
        delete event->show_formspec.formname;
@@ -2684,6 +2879,7 @@ void Game::handleClientEvent_HudAdd(ClientEvent *event, CameraOrientation *cam)
        e->size      = event->hudadd->size;
        e->z_index   = event->hudadd->z_index;
        e->text2     = event->hudadd->text2;
+       e->style     = event->hudadd->style;
        m_hud_server_to_client[server_id] = player->addHud(e);
 
        delete event->hudadd;
@@ -2749,6 +2945,8 @@ void Game::handleClientEvent_HudChange(ClientEvent *event, CameraOrientation *ca
                CASE_SET(HUD_STAT_Z_INDEX, z_index, data);
 
                CASE_SET(HUD_STAT_TEXT2, text2, sdata);
+
+               CASE_SET(HUD_STAT_STYLE, style, data);
        }
 
 #undef CASE_SET
@@ -2808,6 +3006,10 @@ void Game::handleClientEvent_SetSky(ClientEvent *event, CameraOrientation *cam)
                        "custom"
                );
        }
+
+       // Orbit Tilt:
+       sky->setBodyOrbitTilt(event->set_sky->body_orbit_tilt);
+
        delete event->set_sky;
 }
 
@@ -2834,9 +3036,10 @@ void Game::handleClientEvent_SetMoon(ClientEvent *event, CameraOrientation *cam)
 void Game::handleClientEvent_SetStars(ClientEvent *event, CameraOrientation *cam)
 {
        sky->setStarsVisible(event->star_params->visible);
-       sky->setStarCount(event->star_params->count, false);
+       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;
 }
 
@@ -2871,7 +3074,7 @@ void Game::processClientEvents(CameraOrientation *cam)
        }
 }
 
-void Game::updateChat(f32 dtime, const v2u32 &screensize)
+void Game::updateChat(f32 dtime)
 {
        // Get new messages from error log buffer
        while (!m_chat_log_buf.empty())
@@ -2887,11 +3090,17 @@ void Game::updateChat(f32 dtime, const v2u32 &screensize)
        chat_backend->step(dtime);
 
        // Display all messages in a static text element
-       m_game_ui->setChatText(chat_backend->getRecentChat(),
-               chat_backend->getRecentBuffer().getLineCount());
+       auto &buf = chat_backend->getRecentBuffer();
+       if (buf.getLinesModified()) {
+               buf.resetLinesModified();
+               m_game_ui->setChatText(chat_backend->getRecentChat(), buf.getLineCount());
+       }
+
+       // Make sure that the size is still correct
+       m_game_ui->updateChatSize();
 }
 
-void Game::updateCamera(u32 busy_time, f32 dtime)
+void Game::updateCamera(f32 dtime)
 {
        LocalPlayer *player = client->getEnv().getLocalPlayer();
 
@@ -2921,6 +3130,11 @@ void Game::updateCamera(u32 busy_time, 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);
@@ -2930,17 +3144,18 @@ void Game::updateCamera(u32 busy_time, f32 dtime)
        float tool_reload_ratio = runData.time_from_last_punch / full_punch_interval;
 
        tool_reload_ratio = MYMIN(tool_reload_ratio, 1.0);
-       camera->update(player, dtime, busy_time / 1000.0f, tool_reload_ratio);
+       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);
 
@@ -2993,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();
 
@@ -3030,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,
@@ -3047,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))
@@ -3095,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"))
@@ -3111,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;
@@ -3149,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");
 
@@ -3173,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);
@@ -3191,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
@@ -3219,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);
@@ -3248,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
@@ -3271,9 +3494,8 @@ void Game::handlePointingAtNode(const PointedThing &pointed,
        } else {
                MapNode n = map.getNode(nodepos);
 
-               if (nodedef_manager->get(n).tiledef[0].name == "unknown_node.png") {
-                       m_game_ui->setInfoText(L"Unknown node: " +
-                               utf8_to_wide(nodedef_manager->get(n).name));
+               if (nodedef_manager->get(n).name == "unknown") {
+                       m_game_ui->setInfoText(L"Unknown node");
                }
        }
 
@@ -3293,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())
@@ -3302,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;
@@ -3335,8 +3557,8 @@ bool Game::nodePlacement(const ItemDefinition &selected_def,
                TextDest *txt_dst = new TextDestNodeMetadata(nodepos, client);
 
                auto *&formspec = m_game_ui->updateFormspec("");
-               GUIFormSpecMenu::create(formspec, client, &input->joystick, fs_src,
-                       txt_dst, client->getFormspecPrepend(), sound);
+               GUIFormSpecMenu::create(formspec, client, m_rendering_engine->get_gui_env(),
+                       &input->joystick, fs_src, txt_dst, client->getFormspecPrepend(), sound);
 
                formspec->setFormSpec(meta->getString("formspec"), inventoryloc);
                return false;
@@ -3352,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);
@@ -3386,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;
@@ -3445,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
@@ -3554,17 +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,
-                       &selected_item.getToolCapabilities(itemdef_manager));
+       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));
        }
 
@@ -3575,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);
                }
@@ -3586,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;
@@ -3600,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") {
@@ -3618,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;
@@ -3641,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) {
@@ -3691,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
        */
@@ -3709,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 {
@@ -3807,12 +4041,28 @@ void Game::updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime,
        }
 
        /*
-               Get chat messages from client
+               Damage camera tilt
        */
+       if (player->hurt_tilt_timer > 0.0f) {
+               player->hurt_tilt_timer -= dtime * 6.0f;
 
-       v2u32 screensize = driver->getScreenSize();
+               if (player->hurt_tilt_timer < 0.0f)
+                       player->hurt_tilt_strength = 0.0f;
+       }
 
-       updateChat(dtime, screensize);
+       /*
+               Update minimap pos and rotation
+       */
+       if (mapper && m_game_ui->m_flags.show_hud) {
+               mapper->setPos(floatToInt(player->getPosition(), BS));
+               mapper->setAngle(player->getYaw());
+       }
+
+       /*
+               Get chat messages from client
+       */
+
+       updateChat(dtime);
 
        /*
                Inventory
@@ -3833,14 +4083,30 @@ 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;
 
        v3f camera_direction = camera->getDirection();
-       if (runData.update_draw_list_timer >= 0.2
+       if (runData.update_draw_list_timer >= update_draw_list_delta
                        || runData.update_draw_list_last_cam_dir.getDistanceFrom(camera_direction) > 0.2
-                       || m_camera_offset_changed) {
+                       || m_camera_offset_changed
+                       || client->getEnv().getClientMap().needsUpdateDrawList()) {
                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()) {
+               updateShadows();
        }
 
        m_game_ui->update(*stats, client, draw_control, cam, runData.pointed_old, gui_chat_console, dtime);
@@ -3874,11 +4140,11 @@ void Game::updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime,
        } while (false);
 
        /*
-               Drawing begins
+               ==================== Drawing begins ====================
        */
-       const video::SColor &skycolor = sky->getSkyColor();
+       const video::SColor skycolor = sky->getSkyColor();
 
-       TimeTaker tt_draw("Draw scene");
+       TimeTaker tt_draw("Draw scene", nullptr, PRECISION_MICRO);
        driver->beginScene(true, true, skycolor);
 
        bool draw_wield_tool = (m_game_ui->m_flags.show_hud &&
@@ -3888,17 +4154,17 @@ 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
-       RenderingEngine::draw_scene(skycolor, m_game_ui->m_flags.show_hud,
+       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);
 
        /*
                Profiler graph
        */
+       v2u32 screensize = driver->getScreenSize();
+
        if (m_game_ui->m_flags.show_profiler_graph)
                graph->draw(10, screensize.Y - 10, driver, g_fontengine->getFont());
 
@@ -3915,52 +4181,14 @@ void Game::updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime,
        }
 
        /*
-               Damage camera tilt
-       */
-       if (player->hurt_tilt_timer > 0.0f) {
-               player->hurt_tilt_timer -= dtime * 6.0f;
-
-               if (player->hurt_tilt_timer < 0.0f)
-                       player->hurt_tilt_strength = 0.0f;
-       }
-
-       /*
-               Update minimap pos and rotation
+               ==================== End scene ====================
        */
-       if (mapper && m_game_ui->m_flags.show_hud) {
-               mapper->setPos(floatToInt(player->getPosition(), BS));
-               mapper->setAngle(player->getYaw());
-       }
-
-       /*
-               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();
 
        stats->drawtime = tt_draw.stop(true);
-       g_profiler->avg("Game::updateFrame(): draw scene [ms]", stats->drawtime);
-       g_profiler->graphAdd("Update frame [ms]", tt_update.stop(true));
+       g_profiler->graphAdd("Draw scene [us]", stats->drawtime);
+       g_profiler->avg("Game::updateFrame(): update frame [ms]", tt_update.stop(true));
 }
 
 /* Log times and stuff for visualization */
@@ -3971,59 +4199,87 @@ inline void Game::updateProfilerGraphs(ProfilerGraph *graph)
        graph->put(values);
 }
 
+/****************************************************************************
+ * Shadows
+ *****************************************************************************/
+void Game::updateShadows()
+{
+       ShadowRenderer *shadow = RenderingEngine::get_shadow_renderer();
+       if (!shadow)
+               return;
+
+       float in_timeofday = fmod(runData.time_of_day_smooth, 1.0f);
+
+       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 = is_day ? sky->getSunDirection() : sky->getMoonDirection();
+
+       v3f sun_pos = light * offset_constant;
+
+       if (shadow->getDirectionalLightCount() == 0)
+               shadow->addDirectionalLight();
+       shadow->getDirectionalLight().setDirection(sun_pos);
+       shadow->setTimeOfDay(in_timeofday);
+
+       shadow->getDirectionalLight().update_frustum(camera, client, m_camera_offset_changed);
+}
 
 /****************************************************************************
  Misc
  ****************************************************************************/
 
-/* On some computers framerate doesn't seem to be automatically limited
+void FpsControl::reset()
+{
+       last_time = porting::getTimeUs();
+}
+
+/*
+ * On some computers framerate doesn't seem to be automatically limited
  */
-inline void Game::limitFps(FpsControl *fps_timings, f32 *dtime)
+void FpsControl::limit(IrrlichtDevice *device, f32 *dtime)
 {
-       // not using getRealTime is necessary for wine
-       device->getTimer()->tick(); // Maker sure device time is up-to-date
-       u32 time = device->getTimer()->getTime();
-       u32 last_time = fps_timings->last_time;
+       const float fps_limit = (device->isWindowFocused() && !g_menumgr.pausesGame())
+                       ? g_settings->getFloat("fps_max")
+                       : g_settings->getFloat("fps_max_unfocused");
+       const u64 frametime_min = 1000000.0f / std::max(fps_limit, 1.0f);
 
-       if (time > last_time)  // Make sure time hasn't overflowed
-               fps_timings->busy_time = time - last_time;
-       else
-               fps_timings->busy_time = 0;
+       u64 time = porting::getTimeUs();
 
-       u32 frametime_min = 1000 / (
-               device->isWindowFocused() && !g_menumgr.pausesGame()
-                       ? g_settings->getFloat("fps_max")
-                       : g_settings->getFloat("fps_max_unfocused"));
+       if (time > last_time) // Make sure time hasn't overflowed
+               busy_time = time - last_time;
+       else
+               busy_time = 0;
 
-       if (fps_timings->busy_time < frametime_min) {
-               fps_timings->sleep_time = frametime_min - fps_timings->busy_time;
-               device->sleep(fps_timings->sleep_time);
+       if (busy_time < frametime_min) {
+               sleep_time = frametime_min - busy_time;
+               if (sleep_time > 1000)
+                       sleep_ms(sleep_time / 1000);
        } else {
-               fps_timings->sleep_time = 0;
+               sleep_time = 0;
        }
 
-       /* Get the new value of the device timer. Note that device->sleep() may
-        * not sleep for the entire requested time as sleep may be interrupted and
-        * therefore it is arguably more accurate to get the new time from the
-        * device rather than calculating it by adding sleep_time to time.
-        */
+       // Read the timer again to accurately determine how long we actually slept,
+       // rather than calculating it by adding sleep_time to time.
+       time = porting::getTimeUs();
 
-       device->getTimer()->tick(); // Update device timer
-       time = device->getTimer()->getTime();
-
-       if (time > last_time)  // Make sure last_time hasn't overflowed
-               *dtime = (time - last_time) / 1000.0;
+       if (time > last_time) // Make sure last_time hasn't overflowed
+               *dtime = (time - last_time) / 1000000.0f;
        else
                *dtime = 0;
 
-       fps_timings->last_time = time;
+       last_time = time;
 }
 
 void Game::showOverlayMessage(const char *msg, float dtime, int percent, bool draw_clouds)
 {
        const wchar_t *wmsg = wgettext(msg);
-       RenderingEngine::draw_load_screen(wmsg, guienv, texture_src, dtime, percent,
+       m_rendering_engine->draw_load_screen(wmsg, guienv, texture_src, dtime, percent,
                draw_clouds);
        delete[] wmsg;
 }
@@ -4040,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");
@@ -4068,27 +4324,6 @@ void Game::readSettings()
  ****************************************************************************/
 /****************************************************************************/
 
-void Game::extendedResourceCleanup()
-{
-       // Extended resource accounting
-       infostream << "Irrlicht resources after cleanup:" << std::endl;
-       infostream << "\tRemaining meshes   : "
-                  << RenderingEngine::get_mesh_cache()->getMeshCount() << std::endl;
-       infostream << "\tRemaining textures : "
-                  << driver->getTextureCount() << std::endl;
-
-       for (unsigned int i = 0; i < driver->getTextureCount(); i++) {
-               irr::video::ITexture *texture = driver->getTextureByIndex(i);
-               infostream << "\t\t" << i << ":" << texture->getName().getPath().c_str()
-                          << std::endl;
-       }
-
-       clearTextureNameCache();
-       infostream << "\tRemaining materials: "
-               << driver-> getMaterialRendererCount()
-                      << " (note: irrlicht doesn't support removing renderers)" << std::endl;
-}
-
 void Game::showDeathFormspec()
 {
        static std::string formspec_str =
@@ -4106,15 +4341,15 @@ void Game::showDeathFormspec()
        LocalFormspecHandler *txt_dst = new LocalFormspecHandler("MT_DEATH_SCREEN", client);
 
        auto *&formspec = m_game_ui->getFormspecGUI();
-       GUIFormSpecMenu::create(formspec, client, &input->joystick,
-               fs_src, txt_dst, client->getFormspecPrepend(), sound);
+       GUIFormSpecMenu::create(formspec, client, m_rendering_engine->get_gui_env(),
+               &input->joystick, fs_src, txt_dst, client->getFormspecPrepend(), sound);
        formspec->setFocus("btn_respawn");
 }
 
 #define GET_KEY_NAME(KEY) gettext(getKeySetting(#KEY).name())
 void Game::showPauseMenu()
 {
-#ifdef __ANDROID__
+#ifdef HAVE_TOUCHSCREENGUI
        static const std::string control_text = strgettext("Default Controls:\n"
                "No menu visible:\n"
                "- single tap: button activate\n"
@@ -4214,16 +4449,18 @@ void Game::showPauseMenu()
        if (simple_singleplayer_mode || address.empty()) {
                static const std::string on = strgettext("On");
                static const std::string off = strgettext("Off");
-               const std::string &damage = g_settings->getBool("enable_damage") ? on : off;
-               const std::string &creative = g_settings->getBool("creative_mode") ? on : off;
+               // Note: Status of enable_damage and creative_mode settings is intentionally
+               // NOT shown here because the game might roll its own damage system and/or do
+               // a per-player Creative Mode, in which case writing it here would mislead.
+               bool damage = g_settings->getBool("enable_damage");
                const std::string &announced = g_settings->getBool("server_announce") ? on : off;
-               os << strgettext("- Damage: ") << damage << "\n"
-                               << strgettext("- Creative Mode: ") << creative << "\n";
                if (!simple_singleplayer_mode) {
-                       const std::string &pvp = g_settings->getBool("enable_pvp") ? on : off;
-                       //~ PvP = Player versus Player
-                       os << strgettext("- PvP: ") << pvp << "\n"
-                                       << strgettext("- Public: ") << announced << "\n";
+                       if (damage) {
+                               const std::string &pvp = g_settings->getBool("enable_pvp") ? on : off;
+                               //~ PvP = Player versus Player
+                               os << strgettext("- PvP: ") << pvp << "\n";
+                       }
+                       os << strgettext("- Public: ") << announced << "\n";
                        std::string server_name = g_settings->get("server_name");
                        str_formspec_escape(server_name);
                        if (announced == on && !server_name.empty())
@@ -4240,13 +4477,11 @@ void Game::showPauseMenu()
        LocalFormspecHandler *txt_dst = new LocalFormspecHandler("MT_PAUSE_MENU");
 
        auto *&formspec = m_game_ui->getFormspecGUI();
-       GUIFormSpecMenu::create(formspec, client, &input->joystick,
-                       fs_src, txt_dst, client->getFormspecPrepend(), sound);
+       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();
 }
 
 /****************************************************************************/
@@ -4257,6 +4492,7 @@ void Game::showPauseMenu()
 
 void the_game(bool *kill,
                InputHandler *input,
+               RenderingEngine *rendering_engine,
                const GameStartData &start_data,
                std::string &error_message,
                ChatBackend &chat_backend,
@@ -4271,20 +4507,21 @@ void the_game(bool *kill,
 
        try {
 
-               if (game.startup(kill, input, start_data, error_message,
-                               reconnect_requested, &chat_backend)) {
+               if (game.startup(kill, input, rendering_engine, start_data,
+                               error_message, reconnect_requested, &chat_backend)) {
                        game.run();
                }
 
        } catch (SerializationError &e) {
-               error_message = std::string("A serialization error occurred:\n")
-                               + e.what() + "\n\nThe server is probably "
-                               " running a different version of " PROJECT_NAME_C ".";
+               const std::string ver_err = fmtgettext("The server is probably running a different version of %s.", PROJECT_NAME_C);
+               error_message = strgettext("A serialization error occurred:") +"\n"
+                               + e.what() + "\n\n" + ver_err;
                errorstream << error_message << std::endl;
        } catch (ServerError &e) {
                error_message = e.what();
                errorstream << "ServerError: " << error_message << std::endl;
        } catch (ModError &e) {
+               // DO NOT TRANSLATE the `ModError`, it's used by ui.lua
                error_message = std::string("ModError: ") + e.what() +
                                strgettext("\nCheck debug.txt for details.");
                errorstream << error_message << std::endl;