]> git.lizzy.rs Git - dragonfireclient.git/blobdiff - src/client/game.cpp
Localize error messages in mainmenu (#11495)
[dragonfireclient.git] / src / client / game.cpp
index 042b61a78cb48d520748c5e267100f4c6ebfc7ed..fb993d92ff7e8169d6093b69a4b402c9c64088bf 100644 (file)
@@ -34,11 +34,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "clouds.h"
 #include "config.h"
 #include "content_cao.h"
+#include "content/subgames.h"
 #include "client/event_manager.h"
 #include "fontengine.h"
 #include "itemdef.h"
 #include "log.h"
 #include "filesys.h"
+#include "gameparams.h"
 #include "gettext.h"
 #include "gui/guiChatConsole.h"
 #include "gui/guiConfirmRegistration.h"
@@ -55,7 +57,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "particles.h"
 #include "porting.h"
 #include "profiler.h"
-#include "quicktune_shortcutter.h"
 #include "raycast.h"
 #include "server.h"
 #include "settings.h"
@@ -65,7 +66,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "util/basic_macros.h"
 #include "util/directiontables.h"
 #include "util/pointedthing.h"
+#include "util/quicktune_shortcutter.h"
 #include "irrlicht_changes/static_text.h"
+#include "irr_ptr.h"
 #include "version.h"
 #include "script/scripting_client.h"
 #include "hud.h"
@@ -169,13 +172,7 @@ struct LocalFormspecHandler : public TextDest
                                return;
                        }
 
-                       if (fields.find("quit") != fields.end()) {
-                               return;
-                       }
-
-                       if (fields.find("btn_continue") != fields.end()) {
-                               return;
-                       }
+                       return;
                }
 
                if (m_formname == "MT_DEATH_SCREEN") {
@@ -184,7 +181,7 @@ struct LocalFormspecHandler : public TextDest
                        return;
                }
 
-               if (m_client && m_client->modsLoaded())
+               if (m_client->modsLoaded())
                        m_client->getScript()->on_formspec_input(m_formname, fields);
        }
 
@@ -266,6 +263,7 @@ class SoundMaker
 public:
        bool makes_footstep_sound;
        float m_player_step_timer;
+       float m_player_jump_timer;
 
        SimpleSoundSpec m_player_step_sound;
        SimpleSoundSpec m_player_leftpunch_sound;
@@ -275,7 +273,8 @@ class SoundMaker
                m_sound(sound),
                m_ndef(ndef),
                makes_footstep_sound(true),
-               m_player_step_timer(0)
+               m_player_step_timer(0.0f),
+               m_player_jump_timer(0.0f)
        {
        }
 
@@ -288,6 +287,14 @@ class SoundMaker
                }
        }
 
+       void playPlayerJump()
+       {
+               if (m_player_jump_timer <= 0.0f) {
+                       m_player_jump_timer = 0.2f;
+                       m_sound->playSound(SimpleSoundSpec("player_jump", 0.5f), false);
+               }
+       }
+
        static void viewBobbingStep(MtEvent *e, void *data)
        {
                SoundMaker *sm = (SoundMaker *)data;
@@ -302,7 +309,8 @@ class SoundMaker
 
        static void playerJump(MtEvent *e, void *data)
        {
-               //SoundMaker *sm = (SoundMaker*)data;
+               SoundMaker *sm = (SoundMaker *)data;
+               sm->playPlayerJump();
        }
 
        static void cameraPunchLeft(MtEvent *e, void *data)
@@ -351,6 +359,7 @@ class SoundMaker
        void step(float dtime)
        {
                m_player_step_timer -= dtime;
+               m_player_jump_timer -= dtime;
        }
 };
 
@@ -391,12 +400,7 @@ 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
@@ -410,12 +414,14 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter
        CachedVertexShaderSetting<float> m_animation_timer_vertex;
        CachedPixelShaderSetting<float> m_animation_timer_pixel;
        CachedPixelShaderSetting<float, 3> m_day_light;
+       CachedPixelShaderSetting<float, 4> m_star_color;
        CachedPixelShaderSetting<float, 3> m_eye_position_pixel;
        CachedVertexShaderSetting<float, 3> m_eye_position_vertex;
        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;
-       CachedPixelShaderSetting<SamplerLayer_t> m_texture_flags;
        Client *m_client;
 
 public:
@@ -442,12 +448,14 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter
                m_animation_timer_vertex("animationTimer"),
                m_animation_timer_pixel("animationTimer"),
                m_day_light("dayLight"),
+               m_star_color("starColor"),
                m_eye_position_pixel("eyePosition"),
                m_eye_position_vertex("eyePosition"),
                m_minimap_yaw("yawVec"),
+               m_camera_offset_pixel("cameraOffset"),
+               m_camera_offset_vertex("cameraOffset"),
                m_base_texture("baseTexture"),
                m_normal_texture("normalTexture"),
-               m_texture_flags("textureFlags"),
                m_client(client)
        {
                g_settings->registerChangedCallback("enable_fog", settingsCallback, this);
@@ -459,12 +467,8 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter
                g_settings->deregisterChangedCallback("enable_fog", settingsCallback, this);
        }
 
-       virtual void onSetConstants(video::IMaterialRendererServices *services,
-                       bool is_highlevel)
+       void onSetConstants(video::IMaterialRendererServices *services) override
        {
-               if (!is_highlevel)
-                       return;
-
                // Background color
                video::SColor bgcolor = m_sky->getBgColor();
                video::SColorf bgcolorf(bgcolor);
@@ -493,42 +497,37 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter
                        sunlight.b };
                m_day_light.set(dnc, services);
 
-               u32 animation_timer = porting::getTimeMs() % 100000;
+               video::SColorf star_color = m_sky->getCurrentStarColor();
+               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;
                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 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);
                }
 
-               SamplerLayer_t base_tex = 0,
-                               normal_tex = 1,
-                               flags_tex = 2;
+               float camera_offset_array[3];
+               v3f offset = intToFloat(m_client->getCamera()->getOffset(), BS);
+               offset.getAs3Values(camera_offset_array);
+               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);
-               m_texture_flags.set(&flags_tex, services);
        }
 };
 
@@ -559,7 +558,7 @@ class GameGlobalShaderConstantSetterFactory : public IShaderConstantSetterFactor
 
        virtual IShaderConstantSetter* create()
        {
-               GameGlobalShaderConstantSetter *scs = new GameGlobalShaderConstantSetter(
+               auto *scs = new GameGlobalShaderConstantSetter(
                                m_sky, m_force_fog_off, m_fog_range, m_client);
                if (!m_sky)
                        created_nosky.push_back(scs);
@@ -567,14 +566,13 @@ 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
 #endif
 
 /****************************************************************************
-
  ****************************************************************************/
 
 const float object_hit_delay = 0.2;
@@ -595,15 +593,15 @@ struct GameRunData {
        u16 new_playeritem;
        PointedThing pointed_old;
        bool digging;
-       bool ldown_for_dig;
+       bool punching;
+       bool btn_down_for_dig;
        bool dig_instantly;
        bool digging_blocked;
-       bool left_punch;
        bool reset_jump_timer;
        float nodig_delay_timer;
        float dig_time;
        float dig_time_complete;
-       float repeat_rightclick_timer;
+       float repeat_place_timer;
        float object_hit_delay_timer;
        float time_from_last_punch;
        ClientActiveObject *selected_object;
@@ -630,6 +628,8 @@ struct ClientEventHandler
  THE GAME
  ****************************************************************************/
 
+using PausedNodesList = std::vector<std::pair<irr_ptr<scene::IAnimatedMeshSceneNode>, float>>;
+
 /* This is not intended to be a public class. If a public class becomes
  * desirable then it may be better to create another 'wrapper' class that
  * hides most of the stuff in this class (nothing in this class is required
@@ -641,43 +641,31 @@ class Game {
        ~Game();
 
        bool startup(bool *kill,
-                       bool random_input,
                        InputHandler *input,
-                       const std::string &map_dir,
-                       const std::string &playername,
-                       const std::string &password,
-                       // If address is "", local server is used and address is updated
-                       std::string *address,
-                       u16 port,
+                       RenderingEngine *rendering_engine,
+                       const GameStartData &game_params,
                        std::string &error_message,
                        bool *reconnect,
-                       ChatBackend *chat_backend,
-                       const SubgameSpec &gamespec,    // Used for local game
-                       bool simple_singleplayer_mode);
+                       ChatBackend *chat_backend);
 
        void run();
        void shutdown();
 
 protected:
 
-       void extendedResourceCleanup();
-
        // Basic initialisation
-       bool init(const std::string &map_dir, std::string *address,
-                       u16 port,
-                       const SubgameSpec &gamespec);
+       bool init(const std::string &map_dir, const std::string &address,
+                       u16 port, const SubgameSpec &gamespec);
        bool initSound();
        bool createSingleplayerServer(const std::string &map_dir,
-                       const SubgameSpec &gamespec, u16 port, std::string *address);
+                       const SubgameSpec &gamespec, u16 port);
 
        // Client creation
-       bool createClient(const std::string &playername,
-                       const std::string &password, std::string *address, u16 port);
+       bool createClient(const GameStartData &start_data);
        bool initGui();
 
        // Client connection
-       bool connectToServer(const std::string &playername,
-                       const std::string &password, std::string *address, u16 port,
+       bool connectToServer(const GameStartData &start_data,
                        bool *connect_ok, bool *aborted);
        bool getServerContent(bool *aborted);
 
@@ -688,6 +676,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);
 
@@ -705,6 +694,7 @@ class Game {
        void toggleFast();
        void toggleNoClip();
        void toggleCinematic();
+       void toggleBlockBounds();
        void toggleAutoforward();
 
        void toggleMinimap(bool shift_pressed);
@@ -750,6 +740,7 @@ 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);
@@ -768,6 +759,14 @@ class Game {
        {
                return input->wasKeyDown(k);
        }
+       inline bool wasKeyPressed(GameKeyType k)
+       {
+               return input->wasKeyPressed(k);
+       }
+       inline bool wasKeyReleased(GameKeyType k)
+       {
+               return input->wasKeyReleased(k);
+       }
 
 #ifdef __ANDROID__
        void handleAndroidChatInput();
@@ -782,6 +781,9 @@ class Game {
        void showDeathFormspec();
        void showPauseMenu();
 
+       void pauseAnimation();
+       void resumeAnimation();
+
        // ClientEvent handlers
        void handleClientEvent_None(ClientEvent *event, CameraOrientation *cam);
        void handleClientEvent_PlayerDamage(ClientEvent *event, CameraOrientation *cam);
@@ -795,16 +797,22 @@ class Game {
        void handleClientEvent_HudRemove(ClientEvent *event, CameraOrientation *cam);
        void handleClientEvent_HudChange(ClientEvent *event, CameraOrientation *cam);
        void handleClientEvent_SetSky(ClientEvent *event, CameraOrientation *cam);
+       void handleClientEvent_SetSun(ClientEvent *event, CameraOrientation *cam);
+       void handleClientEvent_SetMoon(ClientEvent *event, CameraOrientation *cam);
+       void handleClientEvent_SetStars(ClientEvent *event, CameraOrientation *cam);
        void handleClientEvent_OverrideDayNigthRatio(ClientEvent *event,
                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 &neighbourpos, const PointedThing &pointed,
+               const NodeMetadata *meta);
        static const ClientEventHandler clientEventHandler[CLIENTEVENT_MAX];
 
+       f32 getSensitivityScaleFactor() const;
+
        InputHandler *input = nullptr;
 
        Client *client = nullptr;
@@ -823,6 +831,7 @@ class Game {
        SoundMaker *soundmaker = nullptr;
 
        ChatBackend *chat_backend = nullptr;
+       LogOutputBuffer m_chat_log_buf;
 
        EventManager *eventmgr = nullptr;
        QuicktuneShortcutter *quicktune = nullptr;
@@ -837,6 +846,9 @@ class Game {
        Hud *hud = nullptr;
        Minimap *mapper = nullptr;
 
+       // Map server hud ids to client hud ids
+       std::unordered_map<u32, u32> m_hud_server_to_client;
+
        GameRunData runData;
        Flags m_flags;
 
@@ -845,14 +857,15 @@ class Game {
           these items (e.g. device)
        */
        IrrlichtDevice *device;
+       RenderingEngine *m_rendering_engine;
        video::IVideoDriver *driver;
        scene::ISceneManager *smgr;
        bool *kill;
        std::string *error_message;
        bool *reconnect_requested;
        scene::ISceneNode *skybox;
+       PausedNodesList paused_animated_nodes;
 
-       bool random_input;
        bool simple_singleplayer_mode;
        /* End 'cache' */
 
@@ -877,7 +890,7 @@ class Game {
        bool m_cache_enable_free_move;
        f32  m_cache_mouse_sensitivity;
        f32  m_cache_joystick_frustum_sensitivity;
-       f32  m_repeat_right_click_time;
+       f32  m_repeat_place_time;
        f32  m_cache_cam_smoothing;
        f32  m_cache_fog_start;
 
@@ -887,13 +900,17 @@ class Game {
 
        bool m_does_lost_focus_pause_game = false;
 
-#ifdef __ANDROID__
+       int m_reset_HW_buffer_counter = 0;
+#ifdef HAVE_TOUCHSCREENGUI
        bool m_cache_hold_aux1;
+#endif
+#ifdef __ANDROID__
        bool m_android_chat_open;
 #endif
 };
 
 Game::Game() :
+       m_chat_log_buf(g_logger),
        m_game_ui(new GameUI())
 {
        g_settings->registerChangedCallback("doubletap_jump",
@@ -910,7 +927,7 @@ Game::Game() :
                &settingChangedCallback, this);
        g_settings->registerChangedCallback("joystick_frustum_sensitivity",
                &settingChangedCallback, this);
-       g_settings->registerChangedCallback("repeat_rightclick_time",
+       g_settings->registerChangedCallback("repeat_place_time",
                &settingChangedCallback, this);
        g_settings->registerChangedCallback("noclip",
                &settingChangedCallback, this);
@@ -925,7 +942,7 @@ Game::Game() :
 
        readSettings();
 
-#ifdef __ANDROID__
+#ifdef HAVE_TOUCHSCREENGUI
        m_cache_hold_aux1 = false;      // This is initialised properly later
 #endif
 
@@ -956,7 +973,7 @@ Game::~Game()
        delete itemdef_manager;
        delete draw_control;
 
-       extendedResourceCleanup();
+       clearTextureNameCache();
 
        g_settings->deregisterChangedCallback("doubletap_jump",
                &settingChangedCallback, this);
@@ -968,7 +985,7 @@ Game::~Game()
                &settingChangedCallback, this);
        g_settings->deregisterChangedCallback("mouse_sensitivity",
                &settingChangedCallback, this);
-       g_settings->deregisterChangedCallback("repeat_rightclick_time",
+       g_settings->deregisterChangedCallback("repeat_place_time",
                &settingChangedCallback, this);
        g_settings->deregisterChangedCallback("noclip",
                &settingChangedCallback, this);
@@ -983,36 +1000,30 @@ Game::~Game()
 }
 
 bool Game::startup(bool *kill,
-               bool random_input,
                InputHandler *input,
-               const std::string &map_dir,
-               const std::string &playername,
-               const std::string &password,
-               std::string *address,     // can change if simple_singleplayer_mode
-               u16 port,
+               RenderingEngine *rendering_engine,
+               const GameStartData &start_data,
                std::string &error_message,
                bool *reconnect,
-               ChatBackend *chat_backend,
-               const SubgameSpec &gamespec,
-               bool simple_singleplayer_mode)
+               ChatBackend *chat_backend)
 {
+
        // "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;
-       this->random_input        = random_input;
+       reconnect_requested       = reconnect;
        this->input               = input;
        this->chat_backend        = chat_backend;
-       this->simple_singleplayer_mode = simple_singleplayer_mode;
+       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();
@@ -1023,15 +1034,17 @@ bool Game::startup(bool *kill,
        m_invert_mouse = g_settings->getBool("invert_mouse");
        m_first_loop_after_window_activation = true;
 
-       g_translations->clear();
+       g_client_translations->clear();
 
-       if (!init(map_dir, address, port, gamespec))
+       // address can change if simple_singleplayer_mode
+       if (!init(start_data.world_spec.path, start_data.address,
+                       start_data.socket_port, start_data.game_spec))
                return false;
 
-       if (!createClient(playername, password, address, port))
+       if (!createClient(start_data))
                return false;
 
-       RenderingEngine::initialize(client, hud);
+       m_rendering_engine->initialize(client, hud);
 
        return true;
 }
@@ -1050,11 +1063,11 @@ void Game::run()
        Profiler::GraphValues dummyvalues;
        g_profiler->graphGet(dummyvalues);
 
-       draw_times.last_time = RenderingEngine::get_timer_time();
+       draw_times.last_time = m_rendering_engine->get_timer_time();
 
        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
@@ -1062,12 +1075,12 @@ void Game::run()
        irr::core::dimension2d<u32> 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();
+                       m_rendering_engine->get_video_driver()->getScreenSize();
                // 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
@@ -1080,7 +1093,7 @@ void Game::run()
                }
 
                // Calculate dtime =
-               //    RenderingEngine::run() from this iteration
+               //    m_rendering_engine->run() from this iteration
                //  + Sleep time until the wanted FPS are reached
                limitFps(&draw_times, &dtime);
 
@@ -1099,6 +1112,7 @@ void Game::run()
                m_game_ui->clearInfoText();
                hud->resizeHotbar();
 
+
                updateProfilers(stats, draw_times, dtime);
                processUserInput(dtime);
                // Update camera before player movement to avoid camera lag of one frame
@@ -1110,10 +1124,11 @@ void Game::run()
                updatePlayerControl(cam_view);
                step(&dtime);
                processClientEvents(&cam_view_target);
+               updateDebugState();
                updateCamera(draw_times.busy_time, dtime);
                updateSound(dtime);
                processPlayerInteraction(dtime, m_game_ui->m_flags.show_hud,
-                       m_game_ui->m_flags.show_debug);
+                       m_game_ui->m_flags.show_basic_debug);
                updateFrame(&graph, &stats, dtime, cam_view);
                updateProfilerGraphs(&graph);
 
@@ -1129,7 +1144,7 @@ void Game::run()
 
 void Game::shutdown()
 {
-       RenderingEngine::finalize();
+       m_rendering_engine->finalize();
 #if IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR <= 8
        if (g_settings->get("3d_mode") == "pageflip") {
                driver->setRenderTarget(irr::video::ERT_STEREO_BOTH_BUFFERS);
@@ -1139,6 +1154,10 @@ void Game::shutdown()
        if (formspec)
                formspec->quitMenu();
 
+#ifdef HAVE_TOUCHSCREENGUI
+       g_touchscreengui->hide();
+#endif
+
        showOverlayMessage(N_("Shutting down..."), 0, 0, false);
 
        if (clouds)
@@ -1160,6 +1179,7 @@ void Game::shutdown()
 
        chat_backend->addMessage(L"", L"# Disconnected.");
        chat_backend->addMessage(L"", L"");
+       m_chat_log_buf.clear();
 
        if (client) {
                client->Stop();
@@ -1182,7 +1202,7 @@ void Game::shutdown()
 
 bool Game::init(
                const std::string &map_dir,
-               std::string *address,
+               const std::string &address,
                u16 port,
                const SubgameSpec &gamespec)
 {
@@ -1206,8 +1226,8 @@ bool Game::init(
                return false;
 
        // Create a server if not connecting to an existing one
-       if (address->empty()) {
-               if (!createSingleplayerServer(map_dir, gamespec, port, address))
+       if (address.empty()) {
+               if (!createSingleplayerServer(map_dir, gamespec, port))
                        return false;
        }
 
@@ -1217,7 +1237,7 @@ bool Game::init(
 bool Game::initSound()
 {
 #if USE_SOUND
-       if (g_settings->getBool("enable_sound")) {
+       if (g_settings->getBool("enable_sound") && g_sound_manager_singleton.get()) {
                infostream << "Attempting to use OpenAL audio" << std::endl;
                sound = createOpenALSoundManager(g_sound_manager_singleton.get(), &soundfetcher);
                if (!sound)
@@ -1242,7 +1262,7 @@ bool Game::initSound()
 }
 
 bool Game::createSingleplayerServer(const std::string &map_dir,
-               const SubgameSpec &gamespec, u16 port, std::string *address)
+               const SubgameSpec &gamespec, u16 port)
 {
        showOverlayMessage(N_("Creating server..."), 0, 5);
 
@@ -1262,26 +1282,24 @@ 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;
        }
 
-       server = new Server(map_dir, gamespec, simple_singleplayer_mode, bind_addr, false);
-       server->init();
+       server = new Server(map_dir, gamespec, simple_singleplayer_mode, bind_addr,
+                       false, nullptr, error_message);
        server->start();
 
        return true;
 }
 
-bool Game::createClient(const std::string &playername,
-               const std::string &password, std::string *address, u16 port)
+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;
 
@@ -1292,14 +1310,13 @@ bool Game::createClient(const std::string &playername,
                g_touchscreengui->hide();
        }
 #endif
-       if (!connectToServer(playername, password, address, port,
-                       &could_connect, &connect_aborted))
+       if (!connectToServer(start_data, &could_connect, &connect_aborted))
                return false;
 
        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;
@@ -1308,13 +1325,13 @@ bool Game::createClient(const std::string &playername,
        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;
        }
 
-       GameGlobalShaderConstantSetterFactory *scsf = new GameGlobalShaderConstantSetterFactory(
+       auto *scsf = new GameGlobalShaderConstantSetterFactory(
                        &m_flags.force_fog_off, &runData.fog_range, client);
        shader_src->addShaderConstantSetterFactory(scsf);
 
@@ -1323,34 +1340,22 @@ bool Game::createClient(const std::string &playername,
 
        /* Camera
         */
-       camera = new Camera(*draw_control, client);
-       if (!camera || !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);
 
        /* Clouds
         */
-       if (m_cache_enable_clouds) {
+       if (m_cache_enable_clouds)
                clouds = new Clouds(smgr, -1, time(0));
-               if (!clouds) {
-                       *error_message = "Memory allocation error (clouds)";
-                       errorstream << *error_message << std::endl;
-                       return false;
-               }
-       }
 
        /* Skybox
         */
-       sky = new Sky(-1, texture_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
 
-       if (!sky) {
-               *error_message = "Memory allocation error sky";
-               errorstream << *error_message << std::endl;
-               return false;
-       }
-
        /* Pre-calculated values
         */
        video::ITexture *t = texture_src->getTexture("crack_anylength.png");
@@ -1369,26 +1374,33 @@ bool Game::createClient(const std::string &playername,
        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);
-
-       if (!hud) {
-               *error_message = "Memory error: could not create HUD";
-               errorstream << *error_message << std::endl;
-               return false;
-       }
+       hud = new Hud(client, player, &player->inventory);
 
        mapper = client->getMinimap();
-       if (mapper)
-               mapper->setMinimapMode(MINIMAP_MODE_OFF);
+
+       if (mapper && client->modsLoaded())
+               client->getScript()->on_minimap_ready(mapper);
 
        return true;
 }
@@ -1406,11 +1418,6 @@ bool Game::initGui()
        // Chat backend and console
        gui_chat_console = new GUIChatConsole(guienv, guienv->getRootGUIElement(),
                        -1, chat_backend, client, &g_menumgr);
-       if (!gui_chat_console) {
-               *error_message = "Could not allocate memory for chat console";
-               errorstream << *error_message << std::endl;
-               return false;
-       }
 
 #ifdef HAVE_TOUCHSCREENGUI
 
@@ -1422,8 +1429,7 @@ bool Game::initGui()
        return true;
 }
 
-bool Game::connectToServer(const std::string &playername,
-               const std::string &password, std::string *address, u16 port,
+bool Game::connectToServer(const GameStartData &start_data,
                bool *connect_ok, bool *connection_aborted)
 {
        *connect_ok = false;    // Let's not be overly optimistic
@@ -1432,10 +1438,10 @@ bool Game::connectToServer(const std::string &playername,
 
        showOverlayMessage(N_("Resolving address..."), 0, 15);
 
-       Address connect_address(0, 0, 0, 0, port);
+       Address connect_address(0, 0, 0, 0, start_data.socket_port);
 
        try {
-               connect_address.Resolve(address->c_str());
+               connect_address.Resolve(start_data.address.c_str());
 
                if (connect_address.isZero()) { // i.e. INADDR_ANY, IN6ADDR_ANY
                        //connect_address.Resolve("localhost");
@@ -1449,26 +1455,23 @@ bool Game::connectToServer(const std::string &playername,
                        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(playername.c_str(), password, *address,
+       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());
-
-       if (!client)
-               return false;
+                       m_rendering_engine, connect_address.isIPv6(), m_game_ui.get());
 
        client->m_simple_singleplayer_mode = simple_singleplayer_mode;
 
@@ -1490,9 +1493,9 @@ bool Game::connectToServer(const std::string &playername,
                f32 dtime;
                f32 wait_time = 0; // in seconds
 
-               fps_control.last_time = RenderingEngine::get_timer_time();
+               fps_control.last_time = m_rendering_engine->get_timer_time();
 
-               while (RenderingEngine::run()) {
+               while (m_rendering_engine->run()) {
 
                        limitFps(&fps_control, &dtime);
 
@@ -1513,8 +1516,7 @@ bool Game::connectToServer(const std::string &playername,
                                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;
@@ -1529,17 +1531,18 @@ bool Game::connectToServer(const std::string &playername,
                        if (client->m_is_registration_confirmation_state) {
                                if (registration_confirmation_shown) {
                                        // Keep drawing the GUI
-                                       RenderingEngine::draw_menu_scene(guienv, dtime, true);
+                                       m_rendering_engine->draw_menu_scene(guienv, dtime, true);
                                } else {
                                        registration_confirmation_shown = true;
                                        (new GUIConfirmRegistration(guienv, guienv->getRootGUIElement(), -1,
-                                                  &g_menumgr, client, playername, password, connection_aborted))->drop();
+                                                  &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 (!address->empty() && wait_time > 10) {
-                                       *error_message = "Connection timed out.";
+                               if (!start_data.address.empty() && wait_time > 10) {
+                                       *error_message = gettext("Connection timed out.");
                                        errorstream << *error_message << std::endl;
                                        break;
                                }
@@ -1564,9 +1567,9 @@ bool Game::getServerContent(bool *aborted)
        FpsControl fps_control = { 0 };
        f32 dtime; // in seconds
 
-       fps_control.last_time = RenderingEngine::get_timer_time();
+       fps_control.last_time = m_rendering_engine->get_timer_time();
 
-       while (RenderingEngine::run()) {
+       while (m_rendering_engine->run()) {
 
                limitFps(&fps_control, &dtime);
 
@@ -1587,7 +1590,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;
                }
@@ -1604,20 +1607,23 @@ 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);
-                       message << gettext("Media...") << " " << (client->mediaReceiveProgress()*100) << "%";
+                       float receive = client->mediaReceiveProgress() * 100;
+                       message << gettext("Media...");
+                       if (receive > 0)
+                               message << " " << receive << "%";
                        message.precision(2);
 
                        if ((USE_CURL == 0) ||
@@ -1634,7 +1640,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);
                }
        }
@@ -1666,8 +1672,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;
@@ -1688,19 +1693,19 @@ inline bool Game::handleCallbacks()
 
        if (g_gamecallback->changepassword_requested) {
                (new GUIPasswordChange(guienv, guiroot, -1,
-                                      &g_menumgr, client))->drop();
+                                      &g_menumgr, client, texture_src))->drop();
                g_gamecallback->changepassword_requested = false;
        }
 
        if (g_gamecallback->changevolume_requested) {
                (new GUIVolumeChange(guienv, guiroot, -1,
-                                    &g_menumgr))->drop();
+                                    &g_menumgr, texture_src))->drop();
                g_gamecallback->changevolume_requested = false;
        }
 
        if (g_gamecallback->keyconfig_requested) {
                (new GUIKeyChangeMenu(guienv, guiroot, -1,
-                                     &g_menumgr))->drop();
+                                     &g_menumgr, texture_src))->drop();
                g_gamecallback->keyconfig_requested = false;
        }
 
@@ -1720,6 +1725,25 @@ void Game::processQueues()
        shader_src->processQueue();
 }
 
+void Game::updateDebugState()
+{
+       bool has_basic_debug = client->checkPrivilege("basic_debug");
+       bool has_debug = client->checkPrivilege("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;
+}
 
 void Game::updateProfilers(const RunStats &stats, const FpsControl &draw_times,
                f32 dtime)
@@ -1819,6 +1843,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
@@ -1886,33 +1911,47 @@ void Game::processKeyInput()
                toggleFast();
        } else if (wasKeyDown(KeyType::NOCLIP)) {
                toggleNoClip();
+#if USE_SOUND
        } else if (wasKeyDown(KeyType::MUTE)) {
-               bool new_mute_sound = !g_settings->getBool("mute_sound");
-               g_settings->setBool("mute_sound", new_mute_sound);
-               if (new_mute_sound)
-                       m_game_ui->showTranslatedStatusText("Sound muted");
-               else
-                       m_game_ui->showTranslatedStatusText("Sound unmuted");
+               if (g_settings->getBool("enable_sound")) {
+                       bool new_mute_sound = !g_settings->getBool("mute_sound");
+                       g_settings->setBool("mute_sound", new_mute_sound);
+                       if (new_mute_sound)
+                               m_game_ui->showTranslatedStatusText("Sound muted");
+                       else
+                               m_game_ui->showTranslatedStatusText("Sound unmuted");
+               } else {
+                       m_game_ui->showTranslatedStatusText("Sound system is disabled");
+               }
        } else if (wasKeyDown(KeyType::INC_VOLUME)) {
-               float new_volume = rangelim(g_settings->getFloat("sound_volume") + 0.1f, 0.0f, 1.0f);
-               wchar_t buf[100];
-               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);
+               if (g_settings->getBool("enable_sound")) {
+                       float new_volume = rangelim(g_settings->getFloat("sound_volume") + 0.1f, 0.0f, 1.0f);
+                       g_settings->setFloat("sound_volume", new_volume);
+                       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)) {
-               float new_volume = rangelim(g_settings->getFloat("sound_volume") - 0.1f, 0.0f, 1.0f);
-               wchar_t buf[100];
-               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);
+               if (g_settings->getBool("enable_sound")) {
+                       float new_volume = rangelim(g_settings->getFloat("sound_volume") - 0.1f, 0.0f, 1.0f);
+                       g_settings->setFloat("sound_volume", new_volume);
+                       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
+       } else if (wasKeyDown(KeyType::MUTE) || wasKeyDown(KeyType::INC_VOLUME)
+                       || wasKeyDown(KeyType::DEC_VOLUME)) {
+               m_game_ui->showTranslatedStatusText("Sound system is not supported on this build");
+#endif
        } else if (wasKeyDown(KeyType::CINEMATIC)) {
                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)) {
@@ -1969,15 +2008,11 @@ void Game::processItemSelection(u16 *new_playeritem)
 
        s32 dir = wheel;
 
-       if (input->joystick.wasKeyDown(KeyType::SCROLL_DOWN) ||
-                       wasKeyDown(KeyType::HOTBAR_NEXT)) {
+       if (wasKeyDown(KeyType::HOTBAR_NEXT))
                dir = -1;
-       }
 
-       if (input->joystick.wasKeyDown(KeyType::SCROLL_UP) ||
-                       wasKeyDown(KeyType::HOTBAR_PREV)) {
+       if (wasKeyDown(KeyType::HOTBAR_PREV))
                dir = 1;
-       }
 
        if (dir < 0)
                *new_playeritem = *new_playeritem < max_item ? *new_playeritem + 1 : 0;
@@ -1990,7 +2025,6 @@ void Game::processItemSelection(u16 *new_playeritem)
        for (u16 i = 0; i <= max_item; i++) {
                if (wasKeyDown((GameKeyType) (KeyType::SLOT_1 + i))) {
                        *new_playeritem = i;
-                       infostream << "Selected item: " << new_playeritem << std::endl;
                        break;
                }
        }
@@ -2019,7 +2053,7 @@ void Game::openInventory()
        if (!player || !player->getCAO())
                return;
 
-       infostream << "the_game: " << "Launching inventory" << std::endl;
+       infostream << "Game: Launching inventory" << std::endl;
 
        PlayerInventoryFormSource *fs_src = new PlayerInventoryFormSource(client);
 
@@ -2030,8 +2064,8 @@ void Game::openInventory()
                        || !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());
+               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);
        }
@@ -2122,7 +2156,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
 }
@@ -2155,6 +2189,32 @@ void Game::toggleCinematic()
                m_game_ui->showTranslatedStatusText("Cinematic mode disabled");
 }
 
+void Game::toggleBlockBounds()
+{
+       if (client->checkPrivilege("basic_debug")) {
+               enum Hud::BlockBoundsMode newmode = hud->toggleBlockBounds();
+               switch (newmode) {
+                       case Hud::BLOCK_BOUNDS_OFF:
+                               m_game_ui->showTranslatedStatusText("Block bounds hidden");
+                               break;
+                       case Hud::BLOCK_BOUNDS_CURRENT:
+                               m_game_ui->showTranslatedStatusText("Block bounds shown for current block");
+                               break;
+                       case Hud::BLOCK_BOUNDS_NEAR:
+                               m_game_ui->showTranslatedStatusText("Block bounds shown for nearby blocks");
+                               break;
+                       case Hud::BLOCK_BOUNDS_MAX:
+                               m_game_ui->showTranslatedStatusText("Block bounds shown for all blocks");
+                               break;
+                       default:
+                               break;
+               }
+
+       } else {
+               m_game_ui->showTranslatedStatusText("Can't show block bounds (need 'basic_debug' privilege)");
+       }
+}
+
 // Autoforward by toggling continuous forward.
 void Game::toggleAutoforward()
 {
@@ -2172,52 +2232,37 @@ void Game::toggleMinimap(bool shift_pressed)
        if (!mapper || !m_game_ui->m_flags.show_hud || !g_settings->getBool("enable_minimap"))
                return;
 
-       if (shift_pressed) {
+       if (shift_pressed)
                mapper->toggleMinimapShape();
-               return;
-       }
+       else
+               mapper->nextMode();
 
+       // TODO: When legacy minimap is deprecated, keep only HUD minimap stuff here
+
+       // Not so satisying code to keep compatibility with old fixed mode system
+       // -->
        u32 hud_flags = client->getEnv().getLocalPlayer()->hud_flags;
 
-       MinimapMode mode = MINIMAP_MODE_OFF;
-       if (hud_flags & HUD_FLAG_MINIMAP_VISIBLE) {
-               mode = mapper->getMinimapMode();
-               mode = (MinimapMode)((int)mode + 1);
-               // If radar is disabled and in, or switching to, radar mode
-               if (!(hud_flags & HUD_FLAG_MINIMAP_RADAR_VISIBLE) && mode > 3)
-                       mode = MINIMAP_MODE_OFF;
-       }
+       if (!(hud_flags & HUD_FLAG_MINIMAP_VISIBLE)) {
+               m_game_ui->m_flags.show_minimap = false;
+       } else {
 
-       m_game_ui->m_flags.show_minimap = true;
-       switch (mode) {
-               case MINIMAP_MODE_SURFACEx1:
-                       m_game_ui->showTranslatedStatusText("Minimap in surface mode, Zoom x1");
-                       break;
-               case MINIMAP_MODE_SURFACEx2:
-                       m_game_ui->showTranslatedStatusText("Minimap in surface mode, Zoom x2");
-                       break;
-               case MINIMAP_MODE_SURFACEx4:
-                       m_game_ui->showTranslatedStatusText("Minimap in surface mode, Zoom x4");
-                       break;
-               case MINIMAP_MODE_RADARx1:
-                       m_game_ui->showTranslatedStatusText("Minimap in radar mode, Zoom x1");
-                       break;
-               case MINIMAP_MODE_RADARx2:
-                       m_game_ui->showTranslatedStatusText("Minimap in radar mode, Zoom x2");
-                       break;
-               case MINIMAP_MODE_RADARx4:
-                       m_game_ui->showTranslatedStatusText("Minimap in radar mode, Zoom x4");
-                       break;
-               default:
-                       mode = MINIMAP_MODE_OFF;
-                       m_game_ui->m_flags.show_minimap = false;
-                       if (hud_flags & HUD_FLAG_MINIMAP_VISIBLE)
-                               m_game_ui->showTranslatedStatusText("Minimap hidden");
-                       else
-                               m_game_ui->showTranslatedStatusText("Minimap currently disabled by game or mod");
-       }
+       // If radar is disabled, try to find a non radar mode or fall back to 0
+               if (!(hud_flags & HUD_FLAG_MINIMAP_RADAR_VISIBLE))
+                       while (mapper->getModeIndex() &&
+                                       mapper->getModeDef().type == MINIMAP_TYPE_RADAR)
+                               mapper->nextMode();
 
-       mapper->setMinimapMode(mode);
+               m_game_ui->m_flags.show_minimap = mapper->getModeDef().type !=
+                               MINIMAP_TYPE_OFF;
+       }
+       // <--
+       // End of 'not so satifying code'
+       if ((hud_flags & HUD_FLAG_MINIMAP_VISIBLE) ||
+                       (hud && hud->hasElementOfType(HUD_ELEM_MINIMAP)))
+               m_game_ui->showStatusText(utf8_to_wide(mapper->getModeDef().label));
+       else
+               m_game_ui->showTranslatedStatusText("Minimap currently disabled by game or mod");
 }
 
 void Game::toggleFog()
@@ -2233,24 +2278,41 @@ void Game::toggleFog()
 
 void Game::toggleDebug()
 {
-       // Initial / 4x toggle: Chat only
-       // 1x toggle: Debug text with chat
+       // 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 "basic_debug" priv,
+       // 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 (client->checkPrivilege("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 (client->checkPrivilege("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 (client->checkPrivilege("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")) {
@@ -2277,20 +2339,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));
 }
@@ -2301,19 +2356,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));
 }
@@ -2336,14 +2385,13 @@ void Game::checkZoomEnabled()
                m_game_ui->showTranslatedStatusText("Zoom currently disabled by game or mod");
 }
 
-
 void Game::updateCameraDirection(CameraOrientation *cam, float dtime)
 {
        if ((device->isWindowActive() && device->isWindowFocused()
-                       && !isMenuActive()) || random_input) {
+                       && !isMenuActive()) || input->isRandom()) {
 
 #ifndef __ANDROID__
-               if (!random_input) {
+               if (!input->isRandom()) {
                        // Mac OSX gets upset if this is set every frame
                        if (device->getCursorControl()->isVisible())
                                device->getCursorControl()->setVisible(false);
@@ -2372,6 +2420,18 @@ void Game::updateCameraDirection(CameraOrientation *cam, float dtime)
        }
 }
 
+// Get the factor to multiply with sensitivity to get the same mouse/joystick
+// responsiveness independently of FOV.
+f32 Game::getSensitivityScaleFactor() const
+{
+       f32 fov_y = client->getCamera()->getFovY();
+
+       // Multiply by a constant such that it becomes 1.0 at 72 degree FOV and
+       // 16:9 aspect ratio to minimize disruption of existing sensitivity
+       // settings.
+       return tan(fov_y / 2.0f) * 1.3763818698f;
+}
+
 void Game::updateCameraOrientation(CameraOrientation *cam, float dtime)
 {
 #ifdef HAVE_TOUCHSCREENGUI
@@ -2387,8 +2447,9 @@ void Game::updateCameraOrientation(CameraOrientation *cam, float dtime)
                        dist.Y = -dist.Y;
                }
 
-               cam->camera_yaw   -= dist.X * m_cache_mouse_sensitivity;
-               cam->camera_pitch += dist.Y * m_cache_mouse_sensitivity;
+               f32 sens_scale = getSensitivityScaleFactor();
+               cam->camera_yaw   -= dist.X * m_cache_mouse_sensitivity * sens_scale;
+               cam->camera_pitch += dist.Y * m_cache_mouse_sensitivity * sens_scale;
 
                if (dist.X != 0 || dist.Y != 0)
                        input->setMousePos(center.X, center.Y);
@@ -2397,7 +2458,8 @@ void Game::updateCameraOrientation(CameraOrientation *cam, float dtime)
 #endif
 
        if (m_cache_enable_joysticks) {
-               f32 c = m_cache_joystick_frustum_sensitivity * (1.f / 32767.f) * dtime;
+               f32 sens_scale = getSensitivityScaleFactor();
+               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;
        }
@@ -2408,65 +2470,70 @@ 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::SPECIAL1),
+               isKeyDown(KeyType::JUMP) || player->getAutojump(),
+               isKeyDown(KeyType::AUX1),
                isKeyDown(KeyType::SNEAK),
                isKeyDown(KeyType::ZOOM),
-               input->getLeftState(),
-               input->getRightState(),
+               isKeyDown(KeyType::DIG),
+               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::SPECIAL1)                      & 0x1) << 5) |
-                       ( (u32)(isKeyDown(KeyType::SNEAK)                         & 0x1) << 6) |
-                       ( (u32)(input->getLeftState()                             & 0x1) << 7) |
-                       ( (u32)(input->getRightState()                            & 0x1) << 8
-               );
+       // autoforward if set: move towards pointed position at maximum speed
+       if (player->getPlayerSettings().continuous_forward &&
+                       client->activeObjectsReceived() && !player->isDead()) {
+               control.movement_speed = 1.0f;
+               control.movement_direction = 0.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();
+       u32 keypress_bits = (
+                       ( (u32)(control.jump  & 0x1) << 4) |
+                       ( (u32)(control.aux1  & 0x1) << 5) |
+                       ( (u32)(control.sneak & 0x1) << 6) |
+                       ( (u32)(control.dig   & 0x1) << 7) |
+                       ( (u32)(control.place & 0x1) << 8) |
+                       ( (u32)(control.zoom  & 0x1) << 9)
+               );
 
-       // autojump if set: simulate "jump" key
-       if (player->getAutojump()) {
-               control.jump = true;
-               keypress_bits |= 1U << 4;
-       }
+       // Set direction keys to ensure mod compatibility
+       if (control.movement_speed > 0.001f) {
+               float absolute_direction;
 
-       // autoforward if set: simulate "up" key
-       if (player->getPlayerSettings().continuous_forward && !player->isDead()) {
-               control.up = true;
-               keypress_bits |= 1U << 0;
+               // Check in original orientation (absolute value indicates forward / backward)
+               absolute_direction = abs(control.movement_direction);
+               if (absolute_direction < (3.0f / 8.0f * M_PI))
+                       keypress_bits |= (u32)(0x1 << 0); // Forward
+               if (absolute_direction > (5.0f / 8.0f * M_PI))
+                       keypress_bits |= (u32)(0x1 << 1); // Backward
+
+               // Rotate entire coordinate system by 90 degrees (absolute value indicates left / right)
+               absolute_direction = control.movement_direction + M_PI_2;
+               if (absolute_direction >= M_PI)
+                       absolute_direction -= 2 * M_PI;
+               absolute_direction = abs(absolute_direction);
+               if (absolute_direction < (3.0f / 8.0f * M_PI))
+                       keypress_bits |= (u32)(0x1 << 2); // Left
+               if (absolute_direction > (5.0f / 8.0f * M_PI))
+                       keypress_bits |= (u32)(0x1 << 3); // Right
        }
 
        client->setPlayerControl(control);
@@ -2484,6 +2551,9 @@ inline void Game::step(f32 *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);
 
@@ -2491,6 +2561,33 @@ inline void Game::step(f32 *dtime)
        }
 }
 
+static void pauseNodeAnimation(PausedNodesList &paused, scene::ISceneNode *node) {
+       if (!node)
+               return;
+       for (auto &&child: node->getChildren())
+               pauseNodeAnimation(paused, child);
+       if (node->getType() != scene::ESNT_ANIMATED_MESH)
+               return;
+       auto animated_node = static_cast<scene::IAnimatedMeshSceneNode *>(node);
+       float speed = animated_node->getAnimationSpeed();
+       if (!speed)
+               return;
+       paused.push_back({grab(animated_node), speed});
+       animated_node->setAnimationSpeed(0.0f);
+}
+
+void Game::pauseAnimation()
+{
+       pauseNodeAnimation(paused_animated_nodes, smgr->getRootSceneNode());
+}
+
+void Game::resumeAnimation()
+{
+       for (auto &&pair: paused_animated_nodes)
+               pair.first->setAnimationSpeed(pair.second);
+       paused_animated_nodes.clear();
+}
+
 const ClientEventHandler Game::clientEventHandler[CLIENTEVENT_MAX] = {
        {&Game::handleClientEvent_None},
        {&Game::handleClientEvent_PlayerDamage},
@@ -2505,6 +2602,9 @@ const ClientEventHandler Game::clientEventHandler[CLIENTEVENT_MAX] = {
        {&Game::handleClientEvent_HudRemove},
        {&Game::handleClientEvent_HudChange},
        {&Game::handleClientEvent_SetSky},
+       {&Game::handleClientEvent_SetSun},
+       {&Game::handleClientEvent_SetMoon},
+       {&Game::handleClientEvent_SetStars},
        {&Game::handleClientEvent_OverrideDayNigthRatio},
        {&Game::handleClientEvent_CloudParams},
 };
@@ -2521,14 +2621,18 @@ void Game::handleClientEvent_PlayerDamage(ClientEvent *event, CameraOrientation
 
        // 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
@@ -2572,8 +2676,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());
+               GUIFormSpecMenu::create(formspec, client, m_rendering_engine->get_gui_env(),
+                       &input->joystick, fs_src, txt_dst, client->getFormspecPrepend(), sound);
        }
 
        delete event->show_formspec.formspec;
@@ -2585,8 +2689,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());
+       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;
@@ -2602,160 +2706,188 @@ void Game::handleClientEvent_HandleParticleEvent(ClientEvent *event,
 void Game::handleClientEvent_HudAdd(ClientEvent *event, CameraOrientation *cam)
 {
        LocalPlayer *player = client->getEnv().getLocalPlayer();
-       auto &hud_server_to_client = client->getHUDTranslationMap();
 
-       u32 server_id = event->hudadd.server_id;
+       u32 server_id = event->hudadd->server_id;
        // ignore if we already have a HUD with that ID
-       auto i = hud_server_to_client.find(server_id);
-       if (i != hud_server_to_client.end()) {
-               delete event->hudadd.pos;
-               delete event->hudadd.name;
-               delete event->hudadd.scale;
-               delete event->hudadd.text;
-               delete event->hudadd.align;
-               delete event->hudadd.offset;
-               delete event->hudadd.world_pos;
-               delete event->hudadd.size;
+       auto i = m_hud_server_to_client.find(server_id);
+       if (i != m_hud_server_to_client.end()) {
+               delete event->hudadd;
                return;
        }
 
        HudElement *e = new HudElement;
-       e->type   = (HudElementType)event->hudadd.type;
-       e->pos    = *event->hudadd.pos;
-       e->name   = *event->hudadd.name;
-       e->scale  = *event->hudadd.scale;
-       e->text   = *event->hudadd.text;
-       e->number = event->hudadd.number;
-       e->item   = event->hudadd.item;
-       e->dir    = event->hudadd.dir;
-       e->align  = *event->hudadd.align;
-       e->offset = *event->hudadd.offset;
-       e->world_pos = *event->hudadd.world_pos;
-       e->size = *event->hudadd.size;
-       hud_server_to_client[server_id] = player->addHud(e);
-
-       delete event->hudadd.pos;
-       delete event->hudadd.name;
-       delete event->hudadd.scale;
-       delete event->hudadd.text;
-       delete event->hudadd.align;
-       delete event->hudadd.offset;
-       delete event->hudadd.world_pos;
-       delete event->hudadd.size;
+       e->type   = static_cast<HudElementType>(event->hudadd->type);
+       e->pos    = event->hudadd->pos;
+       e->name   = event->hudadd->name;
+       e->scale  = event->hudadd->scale;
+       e->text   = event->hudadd->text;
+       e->number = event->hudadd->number;
+       e->item   = event->hudadd->item;
+       e->dir    = event->hudadd->dir;
+       e->align  = event->hudadd->align;
+       e->offset = event->hudadd->offset;
+       e->world_pos = event->hudadd->world_pos;
+       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;
 }
 
 void Game::handleClientEvent_HudRemove(ClientEvent *event, CameraOrientation *cam)
 {
        LocalPlayer *player = client->getEnv().getLocalPlayer();
-       HudElement *e = player->removeHud(event->hudrm.id);
-       delete e;
+
+       auto i = m_hud_server_to_client.find(event->hudrm.id);
+       if (i != m_hud_server_to_client.end()) {
+               HudElement *e = player->removeHud(i->second);
+               delete e;
+               m_hud_server_to_client.erase(i);
+       }
+
 }
 
 void Game::handleClientEvent_HudChange(ClientEvent *event, CameraOrientation *cam)
 {
        LocalPlayer *player = client->getEnv().getLocalPlayer();
 
-       u32 id = event->hudchange.id;
-       HudElement *e = player->getHud(id);
+       HudElement *e = nullptr;
 
-       if (e == NULL) {
-               delete event->hudchange.v3fdata;
-               delete event->hudchange.v2fdata;
-               delete event->hudchange.sdata;
-               delete event->hudchange.v2s32data;
+       auto i = m_hud_server_to_client.find(event->hudchange->id);
+       if (i != m_hud_server_to_client.end()) {
+               e = player->getHud(i->second);
+       }
+
+       if (e == nullptr) {
+               delete event->hudchange;
                return;
        }
 
-       switch (event->hudchange.stat) {
-               case HUD_STAT_POS:
-                       e->pos = *event->hudchange.v2fdata;
-                       break;
+#define CASE_SET(statval, prop, dataprop) \
+       case statval: \
+               e->prop = event->hudchange->dataprop; \
+               break
 
-               case HUD_STAT_NAME:
-                       e->name = *event->hudchange.sdata;
-                       break;
+       switch (event->hudchange->stat) {
+               CASE_SET(HUD_STAT_POS, pos, v2fdata);
 
-               case HUD_STAT_SCALE:
-                       e->scale = *event->hudchange.v2fdata;
-                       break;
+               CASE_SET(HUD_STAT_NAME, name, sdata);
 
-               case HUD_STAT_TEXT:
-                       e->text = *event->hudchange.sdata;
-                       break;
+               CASE_SET(HUD_STAT_SCALE, scale, v2fdata);
 
-               case HUD_STAT_NUMBER:
-                       e->number = event->hudchange.data;
-                       break;
+               CASE_SET(HUD_STAT_TEXT, text, sdata);
 
-               case HUD_STAT_ITEM:
-                       e->item = event->hudchange.data;
-                       break;
+               CASE_SET(HUD_STAT_NUMBER, number, data);
 
-               case HUD_STAT_DIR:
-                       e->dir = event->hudchange.data;
-                       break;
+               CASE_SET(HUD_STAT_ITEM, item, data);
 
-               case HUD_STAT_ALIGN:
-                       e->align = *event->hudchange.v2fdata;
-                       break;
+               CASE_SET(HUD_STAT_DIR, dir, data);
 
-               case HUD_STAT_OFFSET:
-                       e->offset = *event->hudchange.v2fdata;
-                       break;
+               CASE_SET(HUD_STAT_ALIGN, align, v2fdata);
 
-               case HUD_STAT_WORLD_POS:
-                       e->world_pos = *event->hudchange.v3fdata;
-                       break;
+               CASE_SET(HUD_STAT_OFFSET, offset, v2fdata);
 
-               case HUD_STAT_SIZE:
-                       e->size = *event->hudchange.v2s32data;
-                       break;
+               CASE_SET(HUD_STAT_WORLD_POS, world_pos, v3fdata);
+
+               CASE_SET(HUD_STAT_SIZE, size, v2s32data);
+
+               CASE_SET(HUD_STAT_Z_INDEX, z_index, data);
+
+               CASE_SET(HUD_STAT_TEXT2, text2, sdata);
+
+               CASE_SET(HUD_STAT_STYLE, style, data);
        }
 
-       delete event->hudchange.v3fdata;
-       delete event->hudchange.v2fdata;
-       delete event->hudchange.sdata;
-       delete event->hudchange.v2s32data;
+#undef CASE_SET
+
+       delete event->hudchange;
 }
 
 void Game::handleClientEvent_SetSky(ClientEvent *event, CameraOrientation *cam)
 {
        sky->setVisible(false);
-       // Whether clouds are visible in front of a custom skybox
-       sky->setCloudsEnabled(event->set_sky.clouds);
+       // Whether clouds are visible in front of a custom skybox.
+       sky->setCloudsEnabled(event->set_sky->clouds);
 
        if (skybox) {
                skybox->remove();
                skybox = NULL;
        }
-
+       // Clear the old textures out in case we switch rendering type.
+       sky->clearSkyboxTextures();
        // Handle according to type
-       if (*event->set_sky.type == "regular") {
+       if (event->set_sky->type == "regular") {
+               // Shows the mesh skybox
                sky->setVisible(true);
-               sky->setCloudsEnabled(true);
-       } else if (*event->set_sky.type == "skybox" &&
-               event->set_sky.params->size() == 6) {
-               sky->setFallbackBgColor(*event->set_sky.bgcolor);
-               skybox = RenderingEngine::get_scene_manager()->addSkyBoxSceneNode(
-                       texture_src->getTextureForMesh((*event->set_sky.params)[0]),
-                       texture_src->getTextureForMesh((*event->set_sky.params)[1]),
-                       texture_src->getTextureForMesh((*event->set_sky.params)[2]),
-                       texture_src->getTextureForMesh((*event->set_sky.params)[3]),
-                       texture_src->getTextureForMesh((*event->set_sky.params)[4]),
-                       texture_src->getTextureForMesh((*event->set_sky.params)[5]));
-       }
-               // Handle everything else as plain color
-       else {
-               if (*event->set_sky.type != "plain")
+               // Update mesh based skybox colours if applicable.
+               sky->setSkyColors(event->set_sky->sky_color);
+               sky->setHorizonTint(
+                       event->set_sky->fog_sun_tint,
+                       event->set_sky->fog_moon_tint,
+                       event->set_sky->fog_tint_type
+               );
+       } else if (event->set_sky->type == "skybox" &&
+                       event->set_sky->textures.size() == 6) {
+               // Disable the dyanmic mesh skybox:
+               sky->setVisible(false);
+               // Set fog colors:
+               sky->setFallbackBgColor(event->set_sky->bgcolor);
+               // Set sunrise and sunset fog tinting:
+               sky->setHorizonTint(
+                       event->set_sky->fog_sun_tint,
+                       event->set_sky->fog_moon_tint,
+                       event->set_sky->fog_tint_type
+               );
+               // Add textures to skybox.
+               for (int i = 0; i < 6; i++)
+                       sky->addTextureToSkybox(event->set_sky->textures[i], i, texture_src);
+       } else {
+               // Handle everything else as plain color.
+               if (event->set_sky->type != "plain")
                        infostream << "Unknown sky type: "
-                               << (*event->set_sky.type) << std::endl;
-
-               sky->setFallbackBgColor(*event->set_sky.bgcolor);
+                               << (event->set_sky->type) << std::endl;
+               sky->setVisible(false);
+               sky->setFallbackBgColor(event->set_sky->bgcolor);
+               // Disable directional sun/moon tinting on plain or invalid skyboxes.
+               sky->setHorizonTint(
+                       event->set_sky->bgcolor,
+                       event->set_sky->bgcolor,
+                       "custom"
+               );
        }
 
-       delete event->set_sky.bgcolor;
-       delete event->set_sky.type;
-       delete event->set_sky.params;
+       delete event->set_sky;
+}
+
+void Game::handleClientEvent_SetSun(ClientEvent *event, CameraOrientation *cam)
+{
+       sky->setSunVisible(event->sun_params->visible);
+       sky->setSunTexture(event->sun_params->texture,
+               event->sun_params->tonemap, texture_src);
+       sky->setSunScale(event->sun_params->scale);
+       sky->setSunriseVisible(event->sun_params->sunrise_visible);
+       sky->setSunriseTexture(event->sun_params->sunrise, texture_src);
+       delete event->sun_params;
+}
+
+void Game::handleClientEvent_SetMoon(ClientEvent *event, CameraOrientation *cam)
+{
+       sky->setMoonVisible(event->moon_params->visible);
+       sky->setMoonTexture(event->moon_params->texture,
+               event->moon_params->tonemap, texture_src);
+       sky->setMoonScale(event->moon_params->scale);
+       delete event->moon_params;
+}
+
+void Game::handleClientEvent_SetStars(ClientEvent *event, CameraOrientation *cam)
+{
+       sky->setStarsVisible(event->star_params->visible);
+       sky->setStarCount(event->star_params->count, false);
+       sky->setStarColor(event->star_params->starcolor);
+       sky->setStarScale(event->star_params->scale);
+       delete event->star_params;
 }
 
 void Game::handleClientEvent_OverrideDayNigthRatio(ClientEvent *event,
@@ -2789,20 +2921,11 @@ void Game::processClientEvents(CameraOrientation *cam)
        }
 }
 
-void Game::updateChat(f32 dtime, const v2u32 &screensize)
+void Game::updateChat(f32 dtime)
 {
-       // Add chat log output for errors to be shown in chat
-       static LogOutputBuffer chat_log_error_buf(g_logger, LL_ERROR);
-
        // Get new messages from error log buffer
-       while (!chat_log_error_buf.empty()) {
-               std::wstring error_message = utf8_to_wide(chat_log_error_buf.get());
-               if (!g_settings->getBool("disable_escape_sequences")) {
-                       error_message.insert(0, L"\x1b(c@red)");
-                       error_message.append(L"\x1b(c@white)");
-               }
-               chat_backend->addMessage(L"", error_message);
-       }
+       while (!m_chat_log_buf.empty())
+               chat_backend->addMessage(L"", utf8_to_wide(m_chat_log_buf.get()));
 
        // Get new messages from client
        std::wstring message;
@@ -2814,8 +2937,14 @@ 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)
@@ -2848,7 +2977,8 @@ void Game::updateCamera(u32 busy_time, f32 dtime)
 
                camera->toggleCameraMode();
 
-               playercao->setVisible(camera->getCameraMode() > CAMERA_MODE_FIRST);
+               // Make the player visible depending on camera mode.
+               playercao->updateMeshCulling();
                playercao->setChildrenVisible(camera->getCameraMode() > CAMERA_MODE_FIRST);
        }
 
@@ -2923,16 +3053,8 @@ void Game::processPlayerInteraction(f32 dtime, bool show_hud, bool show_debug)
 {
        LocalPlayer *player = client->getEnv().getLocalPlayer();
 
-       v3f player_position  = player->getPosition();
-       v3f player_eye_position = player->getEyePosition();
-       v3f camera_position  = camera->getPosition();
-       v3f camera_direction = camera->getDirection();
-       v3s16 camera_offset  = camera->getOffset();
-
-       if (camera->getCameraMode() == CAMERA_MODE_FIRST)
-               player_eye_position += player->eye_offset_first;
-       else
-               player_eye_position += player->eye_offset_third;
+       const v3f camera_direction = camera->getDirection();
+       const v3s16 camera_offset  = camera->getOffset();
 
        /*
                Calculate what block is the crosshair pointing to
@@ -2946,13 +3068,22 @@ void Game::processPlayerInteraction(f32 dtime, bool show_hud, bool show_debug)
 
        core::line3d<f32> shootline;
 
-       if (camera->getCameraMode() != CAMERA_MODE_THIRD_FRONT) {
-               shootline = core::line3d<f32>(player_eye_position,
-                       player_eye_position + camera_direction * BS * d);
-       } else {
+       switch (camera->getCameraMode()) {
+       case CAMERA_MODE_FIRST:
+               // Shoot from camera position, with bobbing
+               shootline.start = camera->getPosition();
+               break;
+       case CAMERA_MODE_THIRD:
+               // Shoot from player head, no bobbing
+               shootline.start = camera->getHeadPosition();
+               break;
+       case CAMERA_MODE_THIRD_FRONT:
+               shootline.start = camera->getHeadPosition();
                // prevent player pointing anything in front-view
-               shootline = core::line3d<f32>(camera_position, camera_position);
+               d = 0;
+               break;
        }
+       shootline.end = shootline.start + camera_direction * BS * d;
 
 #ifdef HAVE_TOUCHSCREENGUI
 
@@ -2969,7 +3100,7 @@ void Game::processPlayerInteraction(f32 dtime, bool show_hud, bool show_debug)
 
        PointedThing pointed = updatePointedThing(shootline,
                        selected_def.liquids_pointable,
-                       !runData.ldown_for_dig,
+                       !runData.btn_down_for_dig,
                        camera_offset);
 
        if (pointed != runData.pointed_old) {
@@ -2977,20 +3108,18 @@ void Game::processPlayerInteraction(f32 dtime, bool show_hud, bool show_debug)
                hud->updateSelectionMesh(camera_offset);
        }
 
-       if (runData.digging_blocked && !input->getLeftState()) {
-               // allow digging again if button is not pressed
+       // Allow digging again if button is not pressed
+       if (runData.digging_blocked && !isKeyDown(KeyType::DIG))
                runData.digging_blocked = false;
-       }
 
        /*
                Stop digging when
-               - releasing left mouse button
+               - releasing dig button
                - pointing away from node
        */
        if (runData.digging) {
-               if (input->getLeftReleased()) {
-                       infostream << "Left button released"
-                                       << " (stopped digging)" << std::endl;
+               if (wasKeyReleased(KeyType::DIG)) {
+                       infostream << "Dig button released (stopped digging)" << std::endl;
                        runData.digging = false;
                } else if (pointed != runData.pointed_old) {
                        if (pointed.type == POINTEDTHING_NODE
@@ -3000,8 +3129,7 @@ void Game::processPlayerInteraction(f32 dtime, bool show_hud, bool show_debug)
                                // Still pointing to the same node, but a different face.
                                // Don't reset.
                        } else {
-                               infostream << "Pointing away from node"
-                                               << " (stopped digging)" << std::endl;
+                               infostream << "Pointing away from node (stopped digging)" << std::endl;
                                runData.digging = false;
                                hud->updateSelectionMesh(camera_offset);
                        }
@@ -3012,51 +3140,60 @@ void Game::processPlayerInteraction(f32 dtime, bool show_hud, bool show_debug)
                        client->setCrack(-1, v3s16(0, 0, 0));
                        runData.dig_time = 0.0;
                }
-       } else if (runData.dig_instantly && input->getLeftReleased()) {
-               // Remove e.g. torches faster when clicking instead of holding LMB
+       } else if (runData.dig_instantly && wasKeyReleased(KeyType::DIG)) {
+               // Remove e.g. torches faster when clicking instead of holding dig button
                runData.nodig_delay_timer = 0;
                runData.dig_instantly = false;
        }
 
-       if (!runData.digging && runData.ldown_for_dig && !input->getLeftState()) {
-               runData.ldown_for_dig = false;
-       }
+       if (!runData.digging && runData.btn_down_for_dig && !isKeyDown(KeyType::DIG))
+               runData.btn_down_for_dig = false;
 
-       runData.left_punch = false;
+       runData.punching = false;
 
        soundmaker->m_player_leftpunch_sound.name = "";
 
        // Prepare for repeating, unless we're not supposed to
-       if (input->getRightState() && !g_settings->getBool("safe_dig_and_place"))
-               runData.repeat_rightclick_timer += dtime;
+       if (isKeyDown(KeyType::PLACE) && !g_settings->getBool("safe_dig_and_place"))
+               runData.repeat_place_timer += dtime;
        else
-               runData.repeat_rightclick_timer = 0;
+               runData.repeat_place_timer = 0;
 
-       if (selected_def.usable && input->getLeftState()) {
-               if (input->getLeftClicked() && (!client->modsLoaded()
-                               || !client->getScript()->on_item_use(selected_item, pointed)))
+       if (selected_def.usable && isKeyDown(KeyType::DIG)) {
+               if (wasKeyPressed(KeyType::DIG) && (!client->modsLoaded() ||
+                               !client->getScript()->on_item_use(selected_item, pointed)))
                        client->interact(INTERACT_USE, pointed);
        } else if (pointed.type == POINTEDTHING_NODE) {
                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);
-       } else if (input->getLeftState()) {
+       } else if (isKeyDown(KeyType::DIG)) {
                // When button is held down in air, show continuous animation
-               runData.left_punch = true;
-       } else if (input->getRightClicked()) {
+               runData.punching = true;
+               // Run callback even though item is not usable
+               if (wasKeyPressed(KeyType::DIG) && client->modsLoaded())
+                       client->getScript()->on_item_use(selected_item, pointed);
+       } else if (wasKeyPressed(KeyType::PLACE)) {
                handlePointingAtNothing(selected_item);
        }
 
        runData.pointed_old = pointed;
 
-       if (runData.left_punch || input->getLeftClicked())
-               camera->setDigging(0); // left click animation
+       if (runData.punching || wasKeyPressed(KeyType::DIG))
+               camera->setDigging(0); // dig animation
 
-       input->resetLeftClicked();
-       input->resetRightClicked();
+       input->clearWasKeyPressed();
+       input->clearWasKeyReleased();
+       // Ensure DIG & PLACE are marked as handled
+       wasKeyDown(KeyType::DIG);
+       wasKeyDown(KeyType::PLACE);
 
-       input->resetLeftReleased();
-       input->resetRightReleased();
+       input->joystick.clearWasKeyPressed(KeyType::DIG);
+       input->joystick.clearWasKeyPressed(KeyType::PLACE);
+
+       input->joystick.clearWasKeyReleased(KeyType::DIG);
+       input->joystick.clearWasKeyReleased(KeyType::PLACE);
 }
 
 
@@ -3077,11 +3214,14 @@ PointedThing Game::updatePointedThing(
        const NodeDefManager *nodedef = map.getNodeDefManager();
 
        runData.selected_object = NULL;
+       hud->pointing_at_object = false;
 
        RaycastState s(shootline, look_for_object, liquids_pointable);
        PointedThing result;
        env.continueRaycast(&s, &result);
        if (result.type == POINTEDTHING_OBJECT) {
+               hud->pointing_at_object = true;
+
                runData.selected_object = client->getEnv().getActiveObject(result.object_id);
                aabb3f selection_box;
                if (show_entity_selectionbox && runData.selected_object->doShowSelectionBox() &&
@@ -3153,7 +3293,7 @@ PointedThing Game::updatePointedThing(
 
 void Game::handlePointingAtNothing(const ItemStack &playerItem)
 {
-       infostream << "Right Clicked in Air" << std::endl;
+       infostream << "Attempted to place item while pointing at nothing" << std::endl;
        PointedThing fauxPointed;
        fauxPointed.type = POINTEDTHING_NOTHING;
        client->interact(INTERACT_ACTIVATE, fauxPointed);
@@ -3172,7 +3312,7 @@ void Game::handlePointingAtNode(const PointedThing &pointed,
 
        ClientMap &map = client->getEnv().getClientMap();
 
-       if (runData.nodig_delay_timer <= 0.0 && input->getLeftState()
+       if (runData.nodig_delay_timer <= 0.0 && isKeyDown(KeyType::DIG)
                        && !runData.digging_blocked
                        && client->checkPrivilege("interact")) {
                handleDigging(pointed, nodepos, selected_item, hand_item, dtime);
@@ -3187,64 +3327,41 @@ 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");
                }
        }
 
-       if ((input->getRightClicked() ||
-                       runData.repeat_rightclick_timer >= m_repeat_right_click_time) &&
+       if ((wasKeyPressed(KeyType::PLACE) ||
+                       runData.repeat_place_timer >= m_repeat_place_time) &&
                        client->checkPrivilege("interact")) {
-               runData.repeat_rightclick_timer = 0;
-               infostream << "Ground right-clicked" << std::endl;
-
-               if (meta && !meta->getString("formspec").empty() && !random_input
-                               && !isKeyDown(KeyType::SNEAK)) {
-                       // Report right click to server
-                       if (nodedef_manager->get(map.getNode(nodepos)).rightclickable) {
-                               client->interact(INTERACT_PLACE, pointed);
-                       }
-
-                       infostream << "Launching custom inventory view" << std::endl;
-
-                       InventoryLocation inventoryloc;
-                       inventoryloc.setNodeMeta(nodepos);
-
-                       NodeMetadataFormSource *fs_src = new NodeMetadataFormSource(
-                               &client->getEnv().getClientMap(), nodepos);
-                       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());
+               runData.repeat_place_timer = 0;
+               infostream << "Place button pressed while looking at ground" << std::endl;
 
-                       formspec->setFormSpec(meta->getString("formspec"), inventoryloc);
-               } else {
-                       // Report right click to server
-
-                       camera->setDigging(1);  // right click animation (always shown for feedback)
+               // Placing animation (always shown for feedback)
+               camera->setDigging(1);
 
-                       soundmaker->m_player_rightpunch_sound = SimpleSoundSpec();
+               soundmaker->m_player_rightpunch_sound = SimpleSoundSpec();
 
-                       // If the wielded item has node placement prediction,
-                       // make that happen
-                       // And also set the sound and send the interact
-                       auto &def = selected_item.getDefinition(itemdef_manager);
-                       bool placed = nodePlacement(def, selected_item, nodepos, neighbourpos,
-                               pointed);
+               // If the wielded item has node placement prediction,
+               // make that happen
+               // 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,
+                       pointed, meta);
 
-                       if (placed && client->modsLoaded())
-                               client->getScript()->on_placenode(pointed, def);
-               }
+               if (placed && client->modsLoaded())
+                       client->getScript()->on_placenode(pointed, def);
        }
 }
 
 bool Game::nodePlacement(const ItemDefinition &selected_def,
        const ItemStack &selected_item, const v3s16 &nodepos, const v3s16 &neighbourpos,
-       const PointedThing &pointed)
+       const PointedThing &pointed, const NodeMetadata *meta)
 {
-       std::string prediction = selected_def.node_placement_prediction;
+       const auto &prediction = selected_def.node_placement_prediction;
+
        const NodeDefManager *nodedef = client->ndef();
        ClientMap &map = client->getEnv().getClientMap();
        MapNode node;
@@ -3256,6 +3373,31 @@ bool Game::nodePlacement(const ItemDefinition &selected_def,
                return false;
        }
 
+       // formspec in meta
+       if (meta && !meta->getString("formspec").empty() && !input->isRandom()
+                       && !isKeyDown(KeyType::SNEAK)) {
+               // on_rightclick callbacks are called anyway
+               if (nodedef_manager->get(map.getNode(nodepos)).rightclickable)
+                       client->interact(INTERACT_PLACE, pointed);
+
+               infostream << "Launching custom inventory view" << std::endl;
+
+               InventoryLocation inventoryloc;
+               inventoryloc.setNodeMeta(nodepos);
+
+               NodeMetadataFormSource *fs_src = new NodeMetadataFormSource(
+                       &client->getEnv().getClientMap(), nodepos);
+               TextDest *txt_dst = new TextDestNodeMetadata(nodepos, 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(meta->getString("formspec"), inventoryloc);
+               return false;
+       }
+
+       // on_rightclick callback
        if (prediction.empty() || (nodedef->get(node).rightclickable &&
                        !isKeyDown(KeyType::SNEAK))) {
                // Report to server
@@ -3264,8 +3406,7 @@ bool Game::nodePlacement(const ItemDefinition &selected_def,
        }
 
        verbosestream << "Node placement prediction for "
-               << selected_def.name << " is "
-               << prediction << std::endl;
+               << selected_def.name << " is " << prediction << std::endl;
        v3s16 p = neighbourpos;
 
        // Place inside node itself if buildable_to
@@ -3276,6 +3417,7 @@ bool Game::nodePlacement(const ItemDefinition &selected_def,
                } else {
                        node = map.getNode(p, &is_valid_position);
                        if (is_valid_position && !nodedef->get(node).buildable_to) {
+                               soundmaker->m_player_rightpunch_sound = selected_def.sound_place_failed;
                                // Report to server
                                client->interact(INTERACT_PLACE, pointed);
                                return false;
@@ -3289,8 +3431,7 @@ bool Game::nodePlacement(const ItemDefinition &selected_def,
 
        if (!found) {
                errorstream << "Node placement prediction failed for "
-                       << selected_def.name << " (places "
-                       << prediction
+                       << selected_def.name << " (places " << prediction
                        << ") - Name not known" << std::endl;
                // Handle this as if prediction was empty
                // Report to server
@@ -3301,9 +3442,14 @@ 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;
 
-       if (predicted_f.param_type_2 == CPT2_WALLMOUNTED ||
+       const u8 place_param2 = selected_def.place_param2;
+
+       if (place_param2) {
+               param2 = place_param2;
+       } else if (predicted_f.param_type_2 == CPT2_WALLMOUNTED ||
                        predicted_f.param_type_2 == CPT2_COLORED_WALLMOUNTED) {
                v3s16 dir = nodepos - neighbourpos;
 
@@ -3314,9 +3460,7 @@ bool Game::nodePlacement(const ItemDefinition &selected_def,
                } else {
                        param2 = dir.Z < 0 ? 5 : 4;
                }
-       }
-
-       if (predicted_f.param_type_2 == CPT2_FACEDIR ||
+       } else if (predicted_f.param_type_2 == CPT2_FACEDIR ||
                        predicted_f.param_type_2 == CPT2_COLORED_FACEDIR) {
                v3s16 dir = nodepos - floatToInt(client->getEnv().getLocalPlayer()->getPosition(), BS);
 
@@ -3327,11 +3471,9 @@ bool Game::nodePlacement(const ItemDefinition &selected_def,
                }
        }
 
-       assert(param2 <= 5);
-
-       //Check attachment if node is in group attached_node
-       if (((ItemGroupList) predicted_f.groups)["attached_node"] != 0) {
-               static v3s16 wallmounted_dirs[8] = {
+       // 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),
@@ -3348,6 +3490,7 @@ bool Game::nodePlacement(const ItemDefinition &selected_def,
                        pp = p + v3s16(0, -1, 0);
 
                if (!nodedef->get(map.getNode(pp)).walkable) {
+                       soundmaker->m_player_rightpunch_sound = selected_def.sound_place_failed;
                        // Report to server
                        client->interact(INTERACT_PLACE, pointed);
                        return false;
@@ -3355,11 +3498,11 @@ bool Game::nodePlacement(const ItemDefinition &selected_def,
        }
 
        // Apply color
-       if ((predicted_f.param_type_2 == CPT2_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_WALLMOUNTED)) {
-               const std::string &indexstr = selected_item.metadata.getString(
-                       "palette_index", 0);
+               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) {
@@ -3399,11 +3542,10 @@ bool Game::nodePlacement(const ItemDefinition &selected_def,
                        soundmaker->m_player_rightpunch_sound = selected_def.sound_place_failed;
                        return false;
                }
-       } catch (InvalidPositionException &e) {
+       } catch (const InvalidPositionException &e) {
                errorstream << "Node placement prediction failed for "
                        << selected_def.name << " (places "
-                       << prediction
-                       << ") - Position not loaded" << std::endl;
+                       << prediction << ") - Position not loaded" << std::endl;
                soundmaker->m_player_rightpunch_sound = selected_def.sound_place_failed;
                return false;
        }
@@ -3424,7 +3566,7 @@ void Game::handlePointingAtObject(const PointedThing &pointed,
 
        m_game_ui->setInfoText(infotext);
 
-       if (input->getLeftState()) {
+       if (isKeyDown(KeyType::DIG)) {
                bool do_punch = false;
                bool do_punch_damage = false;
 
@@ -3434,12 +3576,12 @@ void Game::handlePointingAtObject(const PointedThing &pointed,
                        runData.object_hit_delay_timer = object_hit_delay;
                }
 
-               if (input->getLeftClicked())
+               if (wasKeyPressed(KeyType::DIG))
                        do_punch = true;
 
                if (do_punch) {
-                       infostream << "Left-clicked object" << std::endl;
-                       runData.left_punch = true;
+                       infostream << "Punched object" << std::endl;
+                       runData.punching = true;
                }
 
                if (do_punch_damage) {
@@ -3454,8 +3596,8 @@ void Game::handlePointingAtObject(const PointedThing &pointed,
                        if (!disable_send)
                                client->interact(INTERACT_START_DIGGING, pointed);
                }
-       } else if (input->getRightClicked()) {
-               infostream << "Right-clicked object" << std::endl;
+       } else if (wasKeyDown(KeyType::PLACE)) {
+               infostream << "Pressed place button while pointing at object" << std::endl;
                client->interact(INTERACT_PLACE, pointed);  // place
        }
 }
@@ -3473,7 +3615,8 @@ void Game::handleDigging(const PointedThing &pointed, const v3s16 &nodepos,
        // cheat detection.
        // Get digging parameters
        DigParams params = getDigParams(nodedef_manager->get(n).groups,
-                       &selected_item.getToolCapabilities(itemdef_manager));
+                       &selected_item.getToolCapabilities(itemdef_manager),
+                       selected_item.wear);
 
        // If can't dig, try hand
        if (!params.diggable) {
@@ -3501,7 +3644,7 @@ void Game::handleDigging(const PointedThing &pointed, const v3s16 &nodepos,
                        return;
                client->interact(INTERACT_START_DIGGING, pointed);
                runData.digging = true;
-               runData.ldown_for_dig = true;
+               runData.btn_down_for_dig = true;
        }
 
        if (!runData.dig_instantly) {
@@ -3595,10 +3738,9 @@ void Game::handleDigging(const PointedThing &pointed, const v3s16 &nodepos,
                client->setCrack(-1, nodepos);
        }
 
-       camera->setDigging(0);  // left click animation
+       camera->setDigging(0);  // Dig animation
 }
 
-
 void Game::updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime,
                const CameraOrientation &cam)
 {
@@ -3679,7 +3821,7 @@ void Game::updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime,
                                video::SColor clouds_dark = clouds->getColor()
                                                .getInterpolated(video::SColor(255, 0, 0, 0), 0.9);
                                sky->overrideColors(clouds_dark, clouds->getColor());
-                               sky->setBodiesVisible(false);
+                               sky->setInClouds(true);
                                runData.fog_range = std::fmin(runData.fog_range * 0.5f, 32.0f * BS);
                                // do not draw clouds after all
                                clouds->setVisible(false);
@@ -3724,9 +3866,7 @@ void Game::updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime,
                Get chat messages from client
        */
 
-       v2u32 screensize = driver->getScreenSize();
-
-       updateChat(dtime, screensize);
+       updateChat(dtime);
 
        /*
                Inventory
@@ -3748,15 +3888,22 @@ void Game::updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime,
        */
        runData.update_draw_list_timer += dtime;
 
+       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;
        }
 
+       if (RenderingEngine::get_shadow_renderer()) {
+               updateShadows();
+       }
+
        m_game_ui->update(*stats, client, draw_control, cam, runData.pointed_old, gui_chat_console, dtime);
 
        /*
@@ -3807,12 +3954,14 @@ void Game::updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime,
        } catch (SettingNotFoundException) {
        }
 #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());
 
@@ -3841,7 +3990,7 @@ void Game::updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime,
        /*
                Update minimap pos and rotation
        */
-       if (mapper && m_game_ui->m_flags.show_minimap && m_game_ui->m_flags.show_hud) {
+       if (mapper && m_game_ui->m_flags.show_hud) {
                mapper->setPos(floatToInt(player->getPosition(), BS));
                mapper->setAngle(player->getYaw());
        }
@@ -3849,6 +3998,27 @@ void Game::updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime,
        /*
                End scene
        */
+       if (++m_reset_HW_buffer_counter > 500) {
+               /*
+                 Periodically remove all mesh HW buffers.
+
+                 Work around for a quirk in Irrlicht where a HW buffer is only
+                 released after 20000 iterations (triggered from endScene()).
+
+                 Without this, all loaded but unused meshes will retain their HW
+                 buffers for at least 5 minutes, at which point looking up the HW buffers
+                 becomes a bottleneck and the framerate drops (as much as 30%).
+
+                 Tests showed that numbers between 50 and 1000 are good, so picked 500.
+                 There are no other public Irrlicht APIs that allow interacting with the
+                 HW buffers without tracking the status of every individual mesh.
+
+                 The HW buffers for _visible_ meshes will be reinitialized in the next frame.
+               */
+               infostream << "Game::updateFrame(): Removing all HW buffers." << std::endl;
+               driver->removeAllHardwareBuffers();
+               m_reset_HW_buffer_counter = 0;
+       }
        driver->endScene();
 
        stats->drawtime = tt_draw.stop(true);
@@ -3864,7 +4034,34 @@ 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 = fmod(getWickedTimeOfDay(in_timeofday) + 0.75f, 0.5f) + 0.25f;
+       const float offset_constant = 10000.0f;
+
+       v3f light(0.0f, 0.0f, -1.0f);
+       light.rotateXZBy(90);
+       light.rotateXYBy(timeoftheday * 360 - 90);
+       light.rotateYZBy(sky->getSkyBodyOrbitTilt());
+
+       v3f 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
@@ -3884,9 +4081,10 @@ inline void Game::limitFps(FpsControl *fps_timings, f32 *dtime)
        else
                fps_timings->busy_time = 0;
 
-       u32 frametime_min = 1000 / (g_menumgr.pausesGame()
-                       ? g_settings->getFloat("pause_fps_max")
-                       : g_settings->getFloat("fps_max"));
+       u32 frametime_min = 1000 / (
+               device->isWindowFocused() && !g_menumgr.pausesGame()
+                       ? g_settings->getFloat("fps_max")
+                       : g_settings->getFloat("fps_max_unfocused"));
 
        if (fps_timings->busy_time < frametime_min) {
                fps_timings->sleep_time = frametime_min - fps_timings->busy_time;
@@ -3915,7 +4113,7 @@ inline void Game::limitFps(FpsControl *fps_timings, f32 *dtime)
 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;
 }
@@ -3934,7 +4132,7 @@ void Game::readSettings()
        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_right_click_time            = g_settings->getFloat("repeat_rightclick_time");
+       m_repeat_place_time                  = g_settings->getFloat("repeat_place_time");
 
        m_cache_enable_noclip                = g_settings->getBool("noclip");
        m_cache_enable_free_move             = g_settings->getBool("free_move");
@@ -3960,27 +4158,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 =
@@ -3998,15 +4175,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());
+       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"
@@ -4026,30 +4203,32 @@ void Game::showPauseMenu()
                "- %s: move backwards\n"
                "- %s: move left\n"
                "- %s: move right\n"
-               "- %s: jump/climb\n"
-               "- %s: sneak/go down\n"
+               "- %s: jump/climb up\n"
+               "- %s: dig/punch\n"
+               "- %s: place/use\n"
+               "- %s: sneak/climb down\n"
                "- %s: drop item\n"
                "- %s: inventory\n"
                "- Mouse: turn/look\n"
-               "- Mouse left: dig/punch\n"
-               "- Mouse right: place/use\n"
                "- Mouse wheel: select item\n"
                "- %s: chat\n"
        );
 
-        char control_text_buf[600];
-
-        porting::mt_snprintf(control_text_buf, sizeof(control_text_buf), control_text_template.c_str(),
-                       GET_KEY_NAME(keymap_forward),
-                       GET_KEY_NAME(keymap_backward),
-                       GET_KEY_NAME(keymap_left),
-                       GET_KEY_NAME(keymap_right),
-                       GET_KEY_NAME(keymap_jump),
-                       GET_KEY_NAME(keymap_sneak),
-                       GET_KEY_NAME(keymap_drop),
-                       GET_KEY_NAME(keymap_inventory),
-                       GET_KEY_NAME(keymap_chat)
-                       );
+       char control_text_buf[600];
+
+       porting::mt_snprintf(control_text_buf, sizeof(control_text_buf), control_text_template.c_str(),
+               GET_KEY_NAME(keymap_forward),
+               GET_KEY_NAME(keymap_backward),
+               GET_KEY_NAME(keymap_left),
+               GET_KEY_NAME(keymap_right),
+               GET_KEY_NAME(keymap_jump),
+               GET_KEY_NAME(keymap_dig),
+               GET_KEY_NAME(keymap_place),
+               GET_KEY_NAME(keymap_sneak),
+               GET_KEY_NAME(keymap_drop),
+               GET_KEY_NAME(keymap_inventory),
+               GET_KEY_NAME(keymap_chat)
+       );
 
        std::string control_text = std::string(control_text_buf);
        str_formspec_escape(control_text);
@@ -4070,8 +4249,12 @@ void Game::showPauseMenu()
        }
 
 #ifndef __ANDROID__
-       os              << "button_exit[4," << (ypos++) << ";3,0.5;btn_sound;"
-               << strgettext("Sound Volume") << "]";
+#if USE_SOUND
+       if (g_settings->getBool("enable_sound")) {
+               os << "button_exit[4," << (ypos++) << ";3,0.5;btn_sound;"
+                       << strgettext("Sound Volume") << "]";
+       }
+#endif
        os              << "button_exit[4," << (ypos++) << ";3,0.5;btn_key_config;"
                << strgettext("Change Keys")  << "]";
 #endif
@@ -4107,6 +4290,7 @@ void Game::showPauseMenu()
                                << 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";
                        std::string server_name = g_settings->get("server_name");
@@ -4125,10 +4309,13 @@ 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());
+       GUIFormSpecMenu::create(formspec, client, m_rendering_engine->get_gui_env(),
+                       &input->joystick, fs_src, txt_dst, client->getFormspecPrepend(), sound);
        formspec->setFocus("btn_continue");
        formspec->doPause = true;
+
+       if (simple_singleplayer_mode)
+               pauseAnimation();
 }
 
 /****************************************************************************/
@@ -4138,19 +4325,12 @@ void Game::showPauseMenu()
 /****************************************************************************/
 
 void the_game(bool *kill,
-               bool random_input,
                InputHandler *input,
-               const std::string &map_dir,
-               const std::string &playername,
-               const std::string &password,
-               const std::string &address,         // If empty local server is created
-               u16 port,
-
+               RenderingEngine *rendering_engine,
+               const GameStartData &start_data,
                std::string &error_message,
                ChatBackend &chat_backend,
-               bool *reconnect_requested,
-               const SubgameSpec &gamespec,        // Used for local game
-               bool simple_singleplayer_mode)
+               bool *reconnect_requested) // Used for local game
 {
        Game game;
 
@@ -4158,29 +4338,27 @@ void the_game(bool *kill,
         * is created then this is updated and we don't want to change the value
         * passed to us by the calling function
         */
-       std::string server_address = address;
 
        try {
 
-               if (game.startup(kill, random_input, input, map_dir,
-                               playername, password, &server_address, port, error_message,
-                               reconnect_requested, &chat_backend, gamespec,
-                               simple_singleplayer_mode)) {
+               if (game.startup(kill, input, rendering_engine, start_data,
+                               error_message, reconnect_requested, &chat_backend)) {
                        game.run();
-                       game.shutdown();
                }
 
        } 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;
        }
+       game.shutdown();
 }