]> git.lizzy.rs Git - dragonfireclient.git/commitdiff
Merge branch 'master' of https://github.com/minetest/minetest
authorElias Fleckenstein <eliasfleckenstein@web.de>
Tue, 17 May 2022 20:12:00 +0000 (22:12 +0200)
committerElias Fleckenstein <eliasfleckenstein@web.de>
Tue, 17 May 2022 20:12:00 +0000 (22:12 +0200)
71 files changed:
1  2 
.gitignore
CMakeLists.txt
README.md
builtin/async/game.lua
builtin/client/register.lua
builtin/common/chatcommands.lua
builtin/common/misc_helpers.lua
builtin/game/init.lua
builtin/init.lua
builtin/mainmenu/dlg_contentstore.lua
builtin/mainmenu/dlg_settings_advanced.lua
builtin/mainmenu/init.lua
builtin/mainmenu/pkgmgr.lua
builtin/mainmenu/tab_about.lua
builtin/mainmenu/tab_content.lua
builtin/settingtypes.txt
doc/client_lua_api.txt
doc/lua_api.txt
src/client/camera.cpp
src/client/camera.h
src/client/client.cpp
src/client/client.h
src/client/clientenvironment.cpp
src/client/clientmap.cpp
src/client/content_cao.cpp
src/client/content_cao.h
src/client/game.cpp
src/client/game.h
src/client/gameui.cpp
src/client/gameui.h
src/client/hud.cpp
src/client/inputhandler.h
src/client/localplayer.cpp
src/client/localplayer.h
src/client/mapblock_mesh.cpp
src/client/mapblock_mesh.h
src/client/mesh_generator_thread.h
src/client/minimap.cpp
src/client/render/core.cpp
src/client/renderingengine.cpp
src/collision.cpp
src/defaultsettings.cpp
src/environment.cpp
src/gamedef.h
src/gui/CMakeLists.txt
src/gui/cheatMenu.cpp
src/map.cpp
src/map.h
src/network/clientpackethandler.cpp
src/nodedef.cpp
src/player.cpp
src/player.h
src/script/common/c_content.cpp
src/script/common/c_content.h
src/script/cpp_api/s_base.cpp
src/script/cpp_api/s_base.h
src/script/cpp_api/s_client.cpp
src/script/cpp_api/s_security.cpp
src/script/lua_api/l_client.cpp
src/script/lua_api/l_env.cpp
src/script/lua_api/l_env.h
src/script/lua_api/l_item.cpp
src/script/lua_api/l_localplayer.cpp
src/script/lua_api/l_localplayer.h
src/script/lua_api/l_mainmenu.cpp
src/script/lua_api/l_server.cpp
src/script/lua_api/l_util.cpp
src/serverenvironment.cpp
src/unittest/test.cpp
util/buildbot/buildwin32.sh
util/buildbot/buildwin64.sh

diff --combined .gitignore
index 2e410b48a65b6096781916f0bef8db9bb3b73033,bb5e0a0cdb150d712ddc777a68dca9674d9a93cf..8a963d3c9b09fe7278db06ffe695bde676a60b86
@@@ -52,9 -52,9 +52,9 @@@ build/.cmake
  !/mods/minetest/mods_here.txt
  /worlds
  /world/
 -/clientmods/*
 -!/clientmods/preview/
  /client/mod_storage/
 +/clientmods/*
 +!/clientmods/mods_here.txt
  
  ## Configuration/log files
  minetest.conf
@@@ -107,6 -107,13 +107,13 @@@ CMakeDoxy
  compile_commands.json
  *.apk
  *.zip
+ # Visual Studio
+ *.vcxproj*
+ *.sln
+ .vs/
  
  # Optional user provided library folder
  lib/irrlichtmt
+ # Generated mod storage database
+ client/mod_storage.sqlite
diff --combined CMakeLists.txt
index deb327c5bc17df45e6c2f4b7d1ce58a80a4e34b9,d8dd85af6e02c18ac9ef92965eea114c3bcd6828..018e233da2b19b23b5ab740a1bae4e46bfa8f0aa
@@@ -9,24 -9,25 +9,25 @@@ endif(
  
  # This can be read from ${PROJECT_NAME} after project() is called
  project(minetest)
 -set(PROJECT_NAME_CAPITALIZED "Minetest")
 +set(PROJECT_NAME_CAPITALIZED "Dragonfire")
  
- set(CMAKE_CXX_STANDARD 11)
- set(GCC_MINIMUM_VERSION "4.8")
- set(CLANG_MINIMUM_VERSION "3.4")
+ set(CMAKE_CXX_STANDARD 14)
+ set(CMAKE_CXX_STANDARD_REQUIRED TRUE)
+ set(GCC_MINIMUM_VERSION "5.1")
+ set(CLANG_MINIMUM_VERSION "3.5")
  
  # Also remember to set PROTOCOL_VERSION in network/networkprotocol.h when releasing
  set(VERSION_MAJOR 5)
- set(VERSION_MINOR 5)
+ set(VERSION_MINOR 6)
  set(VERSION_PATCH 0)
 -set(VERSION_EXTRA "" CACHE STRING "Stuff to append to version string")
 +set(VERSION_EXTRA "dragonfire" CACHE STRING "Stuff to append to version string")
  
  # Change to false for releases
 -set(DEVELOPMENT_BUILD TRUE)
 +set(DEVELOPMENT_BUILD FALSE)
  
  set(VERSION_STRING "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}")
  if(VERSION_EXTRA)
-       set(VERSION_STRING ${VERSION_STRING}-${VERSION_EXTRA})
+       set(VERSION_STRING "${VERSION_STRING}-${VERSION_EXTRA}")
  elseif(DEVELOPMENT_BUILD)
        set(VERSION_STRING "${VERSION_STRING}-dev")
  endif()
@@@ -51,7 -52,7 +52,7 @@@ set(RUN_IN_PLACE ${DEFAULT_RUN_IN_PLACE
  set(BUILD_CLIENT TRUE CACHE BOOL "Build client")
  set(BUILD_SERVER FALSE CACHE BOOL "Build server")
  set(BUILD_UNITTESTS TRUE CACHE BOOL "Build unittests")
+ set(BUILD_BENCHMARKS FALSE CACHE BOOL "Build benchmarks")
  
  set(WARN_ALL TRUE CACHE BOOL "Enable -Wall for Release build")
  
@@@ -64,8 -65,21 +65,21 @@@ endif(
  set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/")
  
  
+ set(IRRLICHTMT_BUILD_DIR "" CACHE PATH "Path to IrrlichtMt build directory.")
+ if(NOT "${IRRLICHTMT_BUILD_DIR}" STREQUAL "")
+       find_package(IrrlichtMt QUIET
+               PATHS "${IRRLICHTMT_BUILD_DIR}"
+               NO_DEFAULT_PATH
+       )
+       if(NOT TARGET IrrlichtMt::IrrlichtMt)
+               # find_package() searches certain subdirectories. ${PATH}/cmake is not
+               # the only one, but it is the one where IrrlichtMt is supposed to export
+               # IrrlichtMtConfig.cmake
+               message(FATAL_ERROR "Could not find IrrlichtMtConfig.cmake in ${IRRLICHTMT_BUILD_DIR}/cmake.")
+       endif()
  # This is done here so that relative search paths are more reasonable
- if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/lib/irrlichtmt")
elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/lib/irrlichtmt")
        message(STATUS "Using user-provided IrrlichtMt at subdirectory 'lib/irrlichtmt'")
        if(BUILD_CLIENT)
                # tell IrrlichtMt to create a static library
@@@ -101,11 -115,13 +115,13 @@@ else(
                # Note that we can't use target_include_directories() since that doesn't work for IMPORTED targets before CMake 3.11
                set_target_properties(IrrlichtMt::IrrlichtMt PROPERTIES
                        INTERFACE_INCLUDE_DIRECTORIES "${IRRLICHT_INCLUDE_DIR}")
-       else()
-               message(STATUS "Found IrrlichtMt ${IrrlichtMt_VERSION}")
        endif()
  endif()
  
+ if(TARGET IrrlichtMt::IrrlichtMt)
+       message(STATUS "Found IrrlichtMt ${IrrlichtMt_VERSION}")
+ endif()
  
  # Installation
  
@@@ -135,15 -151,16 +151,16 @@@ elseif(UNIX) # Linux, BSD et
                set(ICONDIR "unix/icons")
                set(LOCALEDIR "locale")
        else()
-               set(SHAREDIR "${CMAKE_INSTALL_PREFIX}/share/${PROJECT_NAME}")
-               set(BINDIR "${CMAKE_INSTALL_PREFIX}/bin")
-               set(DOCDIR "${CMAKE_INSTALL_PREFIX}/share/doc/${PROJECT_NAME}")
-               set(MANDIR "${CMAKE_INSTALL_PREFIX}/share/man")
+               include(GNUInstallDirs)
+               set(SHAREDIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}")
+               set(BINDIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}")
+               set(DOCDIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DOCDIR}")
+               set(MANDIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_MANDIR}")
                set(EXAMPLE_CONF_DIR ${DOCDIR})
-               set(XDG_APPS_DIR "${CMAKE_INSTALL_PREFIX}/share/applications")
-               set(APPDATADIR "${CMAKE_INSTALL_PREFIX}/share/metainfo")
-               set(ICONDIR "${CMAKE_INSTALL_PREFIX}/share/icons")
-               set(LOCALEDIR "${CMAKE_INSTALL_PREFIX}/share/locale")
+               set(XDG_APPS_DIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/applications")
+               set(APPDATADIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/metainfo")
+               set(ICONDIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/icons")
+               set(LOCALEDIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LOCALEDIR}")
        endif()
  endif()
  
@@@ -231,14 -248,14 +248,14 @@@ if(UNIX AND NOT APPLE
        install(FILES "doc/minetest.6" "doc/minetestserver.6" DESTINATION "${MANDIR}/man6")
        install(FILES "misc/net.minetest.minetest.desktop" DESTINATION "${XDG_APPS_DIR}")
        install(FILES "misc/net.minetest.minetest.appdata.xml" DESTINATION "${APPDATADIR}")
 -      install(FILES "misc/minetest.svg" DESTINATION "${ICONDIR}/hicolor/scalable/apps")
 -      install(FILES "misc/minetest-xorg-icon-128.png"
 +      install(FILES "misc/dragonfire.svg" DESTINATION "${ICONDIR}/hicolor/scalable/apps")
 +      install(FILES "misc/dragonfire-xorg-icon-128.png"
                DESTINATION "${ICONDIR}/hicolor/128x128/apps"
 -              RENAME "minetest.png")
 +              RENAME "dragonfire.png")
  endif()
  
  if(APPLE)
 -      install(FILES "misc/minetest-icon.icns" DESTINATION "${SHAREDIR}")
 +      install(FILES "misc/dragonfire-icon.icns" DESTINATION "${SHAREDIR}")
        install(FILES "misc/Info.plist" DESTINATION "${BUNDLE_PATH}/Contents")
  endif()
  
  find_package(GMP REQUIRED)
  find_package(Json REQUIRED)
  find_package(Lua REQUIRED)
- # JsonCpp doesn't compile well on GCC 4.8
if(NOT USE_SYSTEM_JSONCPP)
-       set(GCC_MINIMUM_VERSION "4.9")
+ if(NOT USE_LUAJIT)
+       set(LUA_BIT_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/lib/bitop)
      set(LUA_BIT_LIBRARY bitop)
+       add_subdirectory(lib/bitop)
  endif()
  
  if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
@@@ -264,9 -281,12 +281,12 @@@ elseif(CMAKE_CXX_COMPILER_ID MATCHES "(
        endif()
  endif()
  
+ if(BUILD_BENCHMARKS)
+       add_subdirectory(lib/catch2)
+ endif()
  # Subdirectories
  # Be sure to add all relevant definitions above this
  add_subdirectory(src)
  
  
@@@ -324,7 -344,7 +344,7 @@@ if(WIN32
                set(CPACK_CREATE_DESKTOP_LINKS ${PROJECT_NAME})
                set(CPACK_PACKAGING_INSTALL_PREFIX "/${PROJECT_NAME_CAPITALIZED}")
  
 -              set(CPACK_WIX_PRODUCT_ICON "${CMAKE_CURRENT_SOURCE_DIR}/misc/minetest-icon.ico")
 +              set(CPACK_WIX_PRODUCT_ICON "${CMAKE_CURRENT_SOURCE_DIR}/misc/dragonfire-icon.ico")
                # Supported languages can be found at
                # http://wixtoolset.org/documentation/manual/v3/wixui/wixui_localization.html
                #set(CPACK_WIX_CULTURES "ar-SA,bg-BG,ca-ES,hr-HR,cs-CZ,da-DK,nl-NL,en-US,et-EE,fi-FI,fr-FR,de-DE")
diff --combined README.md
index 5f4dded5900b65677079e66c06e73409a6323757,b6b545a22ca85536afd141c209171cc481a7e440..b28a0ff06c63ee8dbc93c7cd761bb9290509c9e2
+++ b/README.md
@@@ -7,12 -7,12 +7,12 @@@ Minetes
  
  Minetest is a free open-source voxel game engine with easy modding and game creation.
  
- Copyright (C) 2010-2020 Perttu Ahola <celeron55@gmail.com>
+ Copyright (C) 2010-2022 Perttu Ahola <celeron55@gmail.com>
  and contributors (see source file comments and the version control log)
  
  In case you downloaded the source code
  --------------------------------------
 -If you downloaded the Minetest Engine source code in which this file is
 +If you downloaded the Dragonfire Client source code in which this file is
  contained, you probably want to download the [Minetest Game](https://github.com/minetest/minetest_game/)
  project too. See its README.txt for more information.
  
@@@ -132,10 -132,11 +132,11 @@@ Compilin
  
  | Dependency | Version | Commentary |
  |------------|---------|------------|
- | GCC        | 4.9+    | Can be replaced with Clang 3.4+ |
+ | GCC        | 5.1+    | or Clang 3.5+ |
  | CMake      | 3.5+    |            |
  | IrrlichtMt | -       | Custom version of Irrlicht, see https://github.com/minetest/irrlicht |
- | SQLite3    | 3.0+    |            |
+ | Freetype   | 2.0+    |            |
+ | SQLite3    | 3+      |            |
  | Zstd       | 1.0+    |            |
  | LuaJIT     | 2.0+    | Bundled Lua 5.1 is used if not present |
  | GMP        | 5.0.0+  | Bundled mini-GMP is used if not present |
  
  For Debian/Ubuntu users:
  
-     sudo apt install g++ make libc6-dev cmake libpng-dev libjpeg-dev libxxf86vm-dev libgl1-mesa-dev libsqlite3-dev libogg-dev libvorbis-dev libopenal-dev libcurl4-gnutls-dev libfreetype6-dev zlib1g-dev libgmp-dev libjsoncpp-dev libzstd-dev
+     sudo apt install g++ make libc6-dev cmake libpng-dev libjpeg-dev libxxf86vm-dev libgl1-mesa-dev libsqlite3-dev libogg-dev libvorbis-dev libopenal-dev libcurl4-gnutls-dev libfreetype6-dev zlib1g-dev libgmp-dev libjsoncpp-dev libzstd-dev libluajit-5.1-dev
  
  For Fedora users:
  
      sudo dnf install make automake gcc gcc-c++ kernel-devel cmake libcurl-devel openal-soft-devel libvorbis-devel libXxf86vm-devel libogg-devel freetype-devel mesa-libGL-devel zlib-devel jsoncpp-devel gmp-devel sqlite-devel luajit-devel leveldb-devel ncurses-devel spatialindex-devel libzstd-devel
-     
  For Arch users:
  
      sudo pacman -S base-devel libcurl-gnutls cmake libxxf86vm libpng sqlite libogg libvorbis openal freetype2 jsoncpp gmp luajit leveldb ncurses zstd
@@@ -171,7 -172,7 +172,7 @@@ For Fedora users
  
  Download source (this is the URL to the latest of source repository, which might not work at all times) using Git:
  
 -    git clone --depth 1 https://github.com/minetest/minetest.git
 +    git clone --depth 1 https://github.com/EliasFleckenstein03/dragonfireclient
      cd minetest
  
  Download minetest_game (otherwise only the "Development Test" game is available) using Git:
@@@ -224,10 -225,13 +225,13 @@@ Run it
    - Debug build is slower, but gives much more useful output in a debugger.
  - If you build a bare server you don't need to have the Irrlicht or IrrlichtMt library installed.
    - In that case use `-DIRRLICHT_INCLUDE_DIR=/some/where/irrlicht/include`.
- - IrrlichtMt can also be installed somewhere that is not a standard install path.
-   - In that case use `-DCMAKE_PREFIX_PATH=/path/to/install_prefix`
-   - The path must be set so that `$(CMAKE_PREFIX_PATH)/lib/cmake/IrrlichtMt` exists
-     or that `$(CMAKE_PREFIX_PATH)` is the path of an IrrlichtMt build folder.
+ - Minetest will use the IrrlichtMt package that is found first, given by the following order:
+   1. Specified `IRRLICHTMT_BUILD_DIR` CMake variable
+   2. `${PROJECT_SOURCE_DIR}/lib/irrlichtmt` (if existent)
+   3. Installation of IrrlichtMt in the system-specific library paths
+   4. For server builds with disabled `BUILD_CLIENT` variable, the headers from `IRRLICHT_INCLUDE_DIR` will be used.
+   - NOTE: Changing the IrrlichtMt build directory (includes system installs) requires regenerating the CMake cache (`rm CMakeCache.txt`)
  
  ### CMake options
  
@@@ -236,6 -240,7 +240,7 @@@ General options and their default value
      BUILD_CLIENT=TRUE          - Build Minetest client
      BUILD_SERVER=FALSE         - Build Minetest server
      BUILD_UNITTESTS=TRUE       - Build unittest sources
+     BUILD_BENCHMARKS=FALSE     - Build benchmark sources
      CMAKE_BUILD_TYPE=Release   - Type of build (Release vs. Debug)
          Release                - Release build
          Debug                  - Debug build
          MinSizeRel             - Release build with -Os passed to compiler to make executable as small as possible
      ENABLE_CURL=ON             - Build with cURL; Enables use of online mod repo, public serverlist and remote media fetching via http
      ENABLE_CURSES=ON           - Build with (n)curses; Enables a server side terminal (command line option: --terminal)
-     ENABLE_FREETYPE=ON         - Build with FreeType2; Allows using TTF fonts
      ENABLE_GETTEXT=ON          - Build with Gettext; Allows using translations
-     ENABLE_GLES=OFF            - Build for OpenGL ES instead of OpenGL (requires support by IrrlichtMt)
+     ENABLE_GLES=OFF            - Enable extra support code for OpenGL ES (requires support by IrrlichtMt)
      ENABLE_LEVELDB=ON          - Build with LevelDB; Enables use of LevelDB map backend
      ENABLE_POSTGRESQL=ON       - Build with libpq; Enables use of PostgreSQL map backend (PostgreSQL 9.5 or greater recommended)
      ENABLE_REDIS=ON            - Build with libhiredis; Enables use of Redis map backend
      ENABLE_PROMETHEUS=OFF      - Build with Prometheus metrics exporter (listens on tcp/30000 by default)
      ENABLE_SYSTEM_GMP=ON       - Use GMP from system (much faster than bundled mini-gmp)
      ENABLE_SYSTEM_JSONCPP=ON   - Use JsonCPP from system
-     OPENGL_GL_PREFERENCE=LEGACY - Linux client build only; See CMake Policy CMP0072 for reference
      RUN_IN_PLACE=FALSE         - Create a portable install (worlds, settings etc. in current directory)
      USE_GPROF=FALSE            - Enable profiling using GProf
      VERSION_EXTRA=             - Text to append to version (e.g. VERSION_EXTRA=foobar -> Minetest 0.4.9-foobar)
+     ENABLE_TOUCH=FALSE         - Enable Touchscreen support (requires support by IrrlichtMt)
  
  Library specific options:
  
      CURL_LIBRARY                    - Only if building with cURL; path to libcurl.a/libcurl.so/libcurl.lib
      EGL_INCLUDE_DIR                 - Only if building with GLES; directory that contains egl.h
      EGL_LIBRARY                     - Only if building with GLES; path to libEGL.a/libEGL.so
-     FREETYPE_INCLUDE_DIR_freetype2  - Only if building with FreeType 2; directory that contains an freetype directory with files such as ftimage.h in it
-     FREETYPE_INCLUDE_DIR_ft2build   - Only if building with FreeType 2; directory that contains ft2build.h
-     FREETYPE_LIBRARY                - Only if building with FreeType 2; path to libfreetype.a/libfreetype.so/freetype.lib
-     FREETYPE_DLL                    - Only if building with FreeType 2 on Windows; path to libfreetype.dll
+     EXTRA_DLL                       - Only on Windows; optional paths to additional DLLs that should be packaged
+     FREETYPE_INCLUDE_DIR_freetype2  - Directory that contains files such as ftimage.h
+     FREETYPE_INCLUDE_DIR_ft2build   - Directory that contains ft2build.h
+     FREETYPE_LIBRARY                - Path to libfreetype.a/libfreetype.so/freetype.lib
+     FREETYPE_DLL                    - Only on Windows; path to libfreetype-6.dll
      GETTEXT_DLL                     - Only when building with gettext on Windows; paths to libintl + libiconv DLLs
      GETTEXT_INCLUDE_DIR             - Only when building with gettext; directory that contains iconv.h
      GETTEXT_LIBRARY                 - Only when building with gettext on Windows; path to libintl.dll.a
      OPENAL_DLL                      - Only if building with sound on Windows; path to OpenAL32.dll
      OPENAL_INCLUDE_DIR              - Only if building with sound; directory where al.h is located
      OPENAL_LIBRARY                  - Only if building with sound; path to libopenal.a/libopenal.so/OpenAL32.lib
-     OPENGLES2_INCLUDE_DIR           - Only if building with GLES; directory that contains gl2.h
-     OPENGLES2_LIBRARY               - Only if building with GLES; path to libGLESv2.a/libGLESv2.so
      SQLITE3_INCLUDE_DIR             - Directory that contains sqlite3.h
      SQLITE3_LIBRARY                 - Path to libsqlite3.a/libsqlite3.so/sqlite3.lib
      VORBISFILE_LIBRARY              - Only if building with sound; path to libvorbisfile.a/libvorbisfile.so/libvorbisfile.dll.a
      VORBIS_DLL                      - Only if building with sound on Windows; paths to vorbis DLLs
      VORBIS_INCLUDE_DIR              - Only if building with sound; directory that contains a directory vorbis with vorbisenc.h inside
      VORBIS_LIBRARY                  - Only if building with sound; path to libvorbis.a/libvorbis.so/libvorbis.dll.a
-     XXF86VM_LIBRARY                 - Only on Linux; path to libXXf86vm.a/libXXf86vm.so
      ZLIB_DLL                        - Only on Windows; path to zlib1.dll
      ZLIB_INCLUDE_DIR                - Directory that contains zlib.h
      ZLIB_LIBRARY                    - Path to libz.a/libz.so/zlib.lib
@@@ -326,13 -328,12 +328,12 @@@ It is highly recommended to use vcpkg a
  
  After you successfully built vcpkg you can easily install the required libraries:
  ```powershell
- vcpkg install zlib zstd curl[winssl] openal-soft libvorbis libogg sqlite3 freetype luajit gmp jsoncpp --triplet x64-windows
+ vcpkg install zlib zstd curl[winssl] openal-soft libvorbis libogg libjpeg-turbo sqlite3 freetype luajit gmp jsoncpp opengl-registry --triplet x64-windows
  ```
  
  - **Don't forget about IrrlichtMt.** The easiest way is to clone it to `lib/irrlichtmt` as described in the Linux section.
  - `curl` is optional, but required to read the serverlist, `curl[winssl]` is required to use the content store.
  - `openal-soft`, `libvorbis` and `libogg` are optional, but required to use sound.
- - `freetype` is optional, it allows true-type font rendering.
  - `luajit` is optional, it replaces the integrated Lua interpreter with a faster just-in-time interpreter.
  - `gmp` and `jsoncpp` are optional, otherwise the bundled versions will be compiled
  
@@@ -381,6 -382,60 +382,60 @@@ Build the binaries as described above, 
  Open the generated project file with Visual Studio. Right-click **Package** and choose **Generate**.
  It may take some minutes to generate the installer.
  
+ ### Compiling on MacOS
+ #### Requirements
+ - [Homebrew](https://brew.sh/)
+ - [Git](https://git-scm.com/downloads)
+ Install dependencies with homebrew:
+ ```
+ brew install cmake freetype gettext gmp hiredis jpeg jsoncpp leveldb libogg libpng libvorbis luajit zstd
+ ```
+ #### Download
+ Download source (this is the URL to the latest of source repository, which might not work at all times) using Git:
+ ```bash
+ git clone --depth 1 https://github.com/minetest/minetest.git
+ cd minetest
+ ```
+ Download minetest_game (otherwise only the "Development Test" game is available) using Git:
+ ```
+ git clone --depth 1 https://github.com/minetest/minetest_game.git games/minetest_game
+ ```
+ Download Minetest's fork of Irrlicht:
+ ```
+ git clone --depth 1 https://github.com/minetest/irrlicht.git lib/irrlichtmt
+ ```
+ #### Build
+ ```bash
+ mkdir build
+ cd build
+ cmake .. \
+     -DCMAKE_OSX_DEPLOYMENT_TARGET=10.14 \
+     -DCMAKE_FIND_FRAMEWORK=LAST \
+     -DCMAKE_INSTALL_PREFIX=../build/macos/ \
+     -DRUN_IN_PLACE=FALSE -DENABLE_GETTEXT=TRUE
+ make -j$(sysctl -n hw.logicalcpu)
+ make install
+ ```
+ #### Run
+ ```
+ open ./build/macos/minetest.app
+ ```
  
  Docker
  ------
diff --combined builtin/async/game.lua
index 0000000000000000000000000000000000000000,8cb9720b66d113aaac82cd4fd96e5ee520665fb0..e3119d8cb0cdbe725579fb173dcb3843d39f0fef
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,45 +1,46 @@@
 -dofile(gamepath .. "voxelarea.lua")
+ core.log("info", "Initializing asynchronous environment (game)")
+ local function pack2(...)
+       return {n=select('#', ...), ...}
+ end
+ -- Entrypoint to run async jobs, called by C++
+ function core.job_processor(func, params)
+       local retval = pack2(func(unpack(params, 1, params.n)))
+       return retval
+ end
+ -- Import a bunch of individual files from builtin/game/
+ local gamepath = core.get_builtin_path() .. "game" .. DIR_DELIM
++local commonpath = core.get_builtin_path() .. "common" .. DIR_DELIM
+ dofile(gamepath .. "constants.lua")
+ dofile(gamepath .. "item_s.lua")
+ dofile(gamepath .. "misc_s.lua")
+ dofile(gamepath .. "features.lua")
++dofile(commonpath .. "voxelarea.lua")
+ -- Transfer of globals
+ do
+       local all = assert(core.transferred_globals)
+       core.transferred_globals = nil
+       -- reassemble other tables
+       all.registered_nodes = {}
+       all.registered_craftitems = {}
+       all.registered_tools = {}
+       for k, v in pairs(all.registered_items) do
+               if v.type == "node" then
+                       all.registered_nodes[k] = v
+               elseif v.type == "craftitem" then
+                       all.registered_craftitems[k] = v
+               elseif v.type == "tool" then
+                       all.registered_tools[k] = v
+               end
+       end
+       for k, v in pairs(all) do
+               core[k] = v
+       end
+ end
index f17188b842b6c1de422a3aacba5a573838fa24ba,61db4a30b7cd564d19cd98f3f19bcd8ebca001d7..637a22556cd0a293484bbeb37c03fdec6a221c30
@@@ -1,4 -1,3 +1,3 @@@
  core.callback_origins = {}
  
  local getinfo = debug.getinfo
@@@ -47,26 -46,6 +46,26 @@@ function core.run_callbacks(callbacks, 
        return ret
  end
  
 +function core.override_item(name, redefinition)
 +      if redefinition.name ~= nil then
 +              error("Attempt to redefine name of "..name.." to "..dump(redefinition.name), 2)
 +      end
 +      if redefinition.type ~= nil then
 +              error("Attempt to redefine type of "..name.." to "..dump(redefinition.type), 2)
 +      end
 +      local itemdef = core.get_item_def(name)
 +      if not itemdef then
 +              error("Attempt to override non-existent item "..name, 2)
 +      end
 +      local nodedef = core.get_node_def(name)
 +      table.combine(itemdef, nodedef)
 +
 +      for k, v in pairs(redefinition) do
 +              rawset(itemdef, k, v)
 +      end
 +      core.register_item_raw(itemdef)
 +end
 +
  --
  -- Callback registration
  --
@@@ -102,13 -81,3 +101,13 @@@ core.registered_on_item_use, core.regis
  core.registered_on_modchannel_message, core.register_on_modchannel_message = make_registration()
  core.registered_on_modchannel_signal, core.register_on_modchannel_signal = make_registration()
  core.registered_on_inventory_open, core.register_on_inventory_open = make_registration()
 +core.registered_on_recieve_physics_override, core.register_on_recieve_physics_override = make_registration()
 +core.registered_on_play_sound, core.register_on_play_sound = make_registration()
 +core.registered_on_spawn_particle, core.register_on_spawn_particle = make_registration()
 +core.registered_on_object_properties_change, core.register_on_object_properties_change = make_registration()
 +core.registered_on_object_hp_change, core.register_on_object_hp_change = make_registration()
 +core.registered_on_object_add, core.register_on_object_add = make_registration()
 +
 +core.registered_nodes = {}
 +core.registered_items = {}
 +core.object_refs = {}
index 62f9eacd06c93525fbdeeb377a1357ec1219d54c,7c3da0601c44546f5b82d84379506920183af7d5..817f1f526186d2657ba9084731d12adf844ec8d5
@@@ -6,6 -6,42 +6,42 @@@ local S = core.get_translator("__builti
  
  core.registered_chatcommands = {}
  
+ -- Interpret the parameters of a command, separating options and arguments.
+ -- Input: command, param
+ --   command: name of command
+ --   param: parameters of command
+ -- Returns: opts, args
+ --   opts is a string of option letters, or false on error
+ --   args is an array with the non-option arguments in order, or an error message
+ -- Example: for this command line:
+ --      /command a b -cd e f -g
+ -- the function would receive:
+ --      a b -cd e f -g
+ -- and it would return:
+ --    "cdg", {"a", "b", "e", "f"}
+ -- Negative numbers are taken as arguments. Long options (--option) are
+ -- currently rejected as reserved.
+ local function getopts(command, param)
+       local opts = ""
+       local args = {}
+       for match in param:gmatch("%S+") do
+               if match:byte(1) == 45 then -- 45 = '-'
+                       local second = match:byte(2)
+                       if second == 45 then
+                               return false, S("Invalid parameters (see /help @1).", command)
+                       elseif second and (second < 48 or second > 57) then -- 48 = '0', 57 = '9'
+                               opts = opts .. match:sub(2)
+                       else
+                               -- numeric, add it to args
+                               args[#args + 1] = match
+                       end
+               else
+                       args[#args + 1] = match
+               end
+       end
+       return opts, args
+ end
  function core.register_chatcommand(cmd, def)
        def = def or {}
        def.params = def.params or ""
@@@ -33,67 -69,30 +69,75 @@@ function core.override_chatcommand(name
        core.registered_chatcommands[name] = chatcommand
  end
  
 +if INIT == "client" then
 +      function core.register_list_command(command, desc, setting)
 +              local def = {}
 +              def.description = desc
 +              def.params = "del <item> | add <item> | list"
 +              function def.func(param)
 +                      local list = (minetest.settings:get(setting) or ""):split(",")
 +                      if param == "list" then
 +                              return true, table.concat(list, ", ")
 +                      else
 +                              local sparam = param:split(" ")
 +                              local cmd = sparam[1]
 +                              local item = sparam[2]
 +                              if cmd == "del" then
 +                                      if not item then
 +                                              return false, "Missing item."
 +                                      end
 +                                      local i = table.indexof(list, item)
 +                                      if i == -1 then
 +                                              return false, item .. " is not on the list."
 +                                      else
 +                                              table.remove(list, i)
 +                                              core.settings:set(setting, table.concat(list, ","))
 +                                              return true, "Removed " .. item .. " from the list."
 +                                      end
 +                              elseif cmd == "add" then
 +                                      if not item then
 +                                              return false, "Missing item."
 +                                      end
 +                                      local i = table.indexof(list, item)
 +                                      if i ~= -1 then
 +                                              return false, item .. " is already on the list."
 +                                      else
 +                                              table.insert(list, item)
 +                                              core.settings:set(setting, table.concat(list, ","))
 +                                              return true, "Added " .. item .. " to the list."
 +                                      end
 +                              end
 +                      end
 +                      return false, "Invalid usage. (See .help " .. command .. ")"
 +              end
 +              core.register_chatcommand(command, def)
 +      end
 +end
 +
+ local function format_help_line(cmd, def)
+       local cmd_marker = INIT == "client" and "." or "/"
+       local msg = core.colorize("#00ffff", cmd_marker .. cmd)
+       if def.params and def.params ~= "" then
+               msg = msg .. " " .. def.params
+       end
+       if def.description and def.description ~= "" then
+               msg = msg .. ": " .. def.description
+       end
+       return msg
+ end
  local function do_help_cmd(name, param)
-       local function format_help_line(cmd, def)
-               local cmd_marker = "/"
-               if INIT == "client" then
-                       cmd_marker = "."
-               end
-               local msg = core.colorize("#00ffff", cmd_marker .. cmd)
-               if def.params and def.params ~= "" then
-                       msg = msg .. " " .. def.params
-               end
-               if def.description and def.description ~= "" then
-                       msg = msg .. ": " .. def.description
-               end
-               return msg
+       local opts, args = getopts("help", param)
+       if not opts then
+               return false, args
+       end
+       if #args > 1 then
+               return false, S("Too many arguments, try using just /help <command>")
        end
-       if param == "" then
+       local use_gui = INIT ~= "client" and core.get_player_by_name(name)
+       use_gui = use_gui and not opts:find("t")
+       if #args == 0 and not use_gui then
                local cmds = {}
                for cmd, def in pairs(core.registered_chatcommands) do
                        if INIT == "client" or core.check_player_privs(name, def.privs) then
                                .. "everything.")
                end
                return true, msg
-       elseif param == "all" then
+       elseif #args == 0 or (args[1] == "all" and use_gui) then
+               core.show_general_help_formspec(name)
+               return true
+       elseif args[1] == "all" then
                local cmds = {}
                for cmd, def in pairs(core.registered_chatcommands) do
                        if INIT == "client" or core.check_player_privs(name, def.privs) then
                        msg = core.gettext("Available commands:")
                end
                return true, msg.."\n"..table.concat(cmds, "\n")
-       elseif INIT == "game" and param == "privs" then
+       elseif INIT == "game" and args[1] == "privs" then
+               if use_gui then
+                       core.show_privs_help_formspec(name)
+                       return true
+               end
                local privs = {}
                for priv, def in pairs(core.registered_privileges) do
                        privs[#privs + 1] = priv .. ": " .. def.description
                table.sort(privs)
                return true, S("Available privileges:").."\n"..table.concat(privs, "\n")
        else
-               local cmd = param
+               local cmd = args[1]
                local def = core.registered_chatcommands[cmd]
                if not def then
                        local msg
@@@ -165,8 -171,8 +216,8 @@@ if INIT == "client" the
        })
  else
        core.register_chatcommand("help", {
-               params = S("[all | privs | <cmd>]"),
-               description = S("Get help for commands or list privileges"),
+               params = S("[all | privs | <cmd>] [-t]"),
+               description = S("Get help for commands or list privileges (-t: output in chat)"),
                func = do_help_cmd,
        })
  end
index e4bc056d9ff424f9b83d9a439bbc5438200cf5dd,f5f89acd7942acdfb6963064fb42437e745d8bba..542b2040d6579df334fe444489b3c0ebb41a566b
@@@ -517,17 -517,6 +517,17 @@@ function table.shuffle(t, from, to, ran
  end
  
  
 +function table.combine(t, other)
 +      other = other or {}
 +      for k, v in pairs(other) do
 +              if type(v) == "table" and type(t[k]) == "table" then
 +                      table.combine(t[k], v)
 +              else
 +                      t[k] = v
 +              end
 +      end
 +end
 +
  --------------------------------------------------------------------------------
  -- mainmenu only functions
  --------------------------------------------------------------------------------
@@@ -543,7 -532,7 +543,7 @@@ if INIT == "mainmenu" the
        end
  end
  
- if INIT == "client" or INIT == "mainmenu" then
+ if core.gettext then -- for client and mainmenu
        function fgettext_ne(text, ...)
                text = core.gettext(text)
                local arg = {n=select('#', ...), ...}
@@@ -595,66 -584,6 +595,66 @@@ function core.colorize(color, message
        return table.concat(lines, "\n") .. core.get_color_escape_sequence("#ffffff")
  end
  
 +local function rgb_to_hex(rgb)
 +      local hexadecimal = "#"
 +
 +      for key, value in pairs(rgb) do
 +              local hex = ""
 +
 +              while(value > 0)do
 +                      local index = math.fmod(value, 16) + 1
 +                      value = math.floor(value / 16)
 +                      hex = string.sub("0123456789ABCDEF", index, index) .. hex
 +              end
 +
 +              if(string.len(hex) == 0)then
 +                      hex = "00"
 +              elseif(string.len(hex) == 1)then
 +                      hex = "0" .. hex
 +              end
 +
 +              hexadecimal = hexadecimal .. hex
 +      end
 +
 +      return hexadecimal
 +end
 +
 +local function color_from_hue(hue)
 +      local h = hue / 60
 +      local c = 255
 +      local x = (1 - math.abs(h % 2 - 1)) * 255
 +
 +      local i = math.floor(h)
 +      if i == 0 then
 +              return rgb_to_hex({c, x, 0})
 +      elseif i == 1 then
 +              return rgb_to_hex({x, c, 0})
 +      elseif i == 2 then
 +              return rgb_to_hex({0, c, x})
 +      elseif i == 3 then
 +              return rgb_to_hex({0, x, c})
 +      elseif i == 4 then
 +              return rgb_to_hex({x, 0, c})
 +      else
 +              return rgb_to_hex({c, 0, x})
 +      end
 +end
 +
 +function core.rainbow(input)
 +      local step = 360 / input:len()
 +      local hue = 0
 +      local output = ""
 +      for i = 1, input:len() do
 +              local char = input:sub(i, i)
 +              if char:match("%s") then
 +                      output = output .. char
 +              else
 +                      output = output  .. core.get_color_escape_sequence(color_from_hue(hue)) .. char
 +              end
 +              hue = hue + step
 +      end
 +      return output
 +end
  
  function core.strip_foreground_colors(str)
        return (str:gsub(ESCAPE_CHAR .. "%(c@[^)]+%)", ""))
@@@ -708,19 -637,6 +708,19 @@@ function core.get_translator(textdomain
        return function(str, ...) return core.translate(textdomain or "", str, ...) end
  end
  
 +function core.get_pointed_thing_position(pointed_thing, above)
 +      if pointed_thing.type == "node" then
 +              if above then
 +                      -- The position where a node would be placed
 +                      return pointed_thing.above
 +              end
 +              -- The position where a node would be dug
 +              return pointed_thing.under
 +      elseif pointed_thing.type == "object" then
 +              return pointed_thing.ref and pointed_thing.ref:get_pos()
 +      end
 +end
 +
  --------------------------------------------------------------------------------
  -- Returns the exact coordinate of a pointed surface
  --------------------------------------------------------------------------------
@@@ -785,12 -701,3 +785,12 @@@ en
  function core.is_nan(number)
        return number ~= number
  end
 +
 +function core.inventorycube(img1, img2, img3)
 +      img2 = img2 or img1
 +      img3 = img3 or img1
 +      return "[inventorycube"
 +                      .. "{" .. img1:gsub("%^", "&")
 +                      .. "{" .. img2:gsub("%^", "&")
 +                      .. "{" .. img3:gsub("%^", "&")
 +end
diff --combined builtin/game/init.lua
index 9a9966d7e57ab8f2f999c2f6f74c0b54e9f41fe4,68d6a10f8d164585fa65e7ed6942f7645f57c332..b9ab97b58251078a3f97523866db2204f8727baa
@@@ -8,6 -8,7 +8,7 @@@ local gamepath   = scriptpath .. "game"
  local builtin_shared = {}
  
  dofile(gamepath .. "constants.lua")
+ dofile(gamepath .. "item_s.lua")
  assert(loadfile(gamepath .. "item.lua"))(builtin_shared)
  dofile(gamepath .. "register.lua")
  
@@@ -16,9 -17,9 +17,10 @@@ if core.settings:get_bool("profiler.loa
  end
  
  dofile(commonpath .. "after.lua")
 +dofile(commonpath .. "voxelarea.lua")
  dofile(gamepath .. "item_entity.lua")
  dofile(gamepath .. "deprecated.lua")
+ dofile(gamepath .. "misc_s.lua")
  dofile(gamepath .. "misc.lua")
  dofile(gamepath .. "privileges.lua")
  dofile(gamepath .. "auth.lua")
@@@ -29,8 -30,10 +31,9 @@@ dofile(gamepath .. "static_spawn.lua"
  dofile(gamepath .. "detached_inventory.lua")
  assert(loadfile(gamepath .. "falling.lua"))(builtin_shared)
  dofile(gamepath .. "features.lua")
 -dofile(gamepath .. "voxelarea.lua")
  dofile(gamepath .. "forceloading.lua")
  dofile(gamepath .. "statbars.lua")
  dofile(gamepath .. "knockback.lua")
+ dofile(gamepath .. "async.lua")
  
  profiler = nil
diff --combined builtin/init.lua
index 8bb69a33d26c5b9368c194cca9b2f6bca3b4b3ae,8691360169379bc82ede6e9892a41442fea6a3b8..fd176e942dcb3b3e77f849b6c5c68ca16491738e
@@@ -37,6 -37,7 +37,6 @@@ dofile(commonpath .. "misc_helpers.lua"
  
  if INIT == "game" then
        dofile(gamepath .. "init.lua")
 -      assert(not core.get_http_api)
  elseif INIT == "mainmenu" then
        local mm_script = core.settings:get("main_menu_script")
        local custom_loaded = false
        if not custom_loaded then
                dofile(core.get_mainmenu_path() .. DIR_DELIM .. "init.lua")
        end
- elseif INIT == "async" then
-       dofile(asyncpath .. "init.lua")
+ elseif INIT == "async"  then
+       dofile(asyncpath .. "mainmenu.lua")
+ elseif INIT == "async_game" then
+       dofile(asyncpath .. "game.lua")
  elseif INIT == "client" then
        dofile(clientpath .. "init.lua")
  else
index 790da03bafc3fdcc01c9a4ae6378b6030b585147,276a7b0962aa07f9692760dec9f25136509ad80f..ff32c8c9f7d0d689071ffa375eb32b9f14f37fe7
@@@ -25,7 -25,7 +25,7 @@@ en
  
  -- Unordered preserves the original order of the ContentDB API,
  -- before the package list is ordered based on installed state.
- local store = { packages = {}, packages_full = {}, packages_full_unordered = {} }
+ local store = { packages = {}, packages_full = {}, packages_full_unordered = {}, aliases = {} }
  
  local http = core.get_http_api()
  
@@@ -42,8 -42,8 +42,8 @@@ local num_per_page = 
  local filter_type = 1
  local filter_types_titles = {
        fgettext("All packages"),
 -      fgettext("Games"),
 -      fgettext("Mods"),
 +--    fgettext("Games"),
 +      fgettext("Clientmods"),
        fgettext("Texture packs"),
  }
  
@@@ -52,7 -52,7 +52,7 @@@ local download_queue = {
  
  local filter_types_type = {
        nil,
 -      "game",
 +--    "game",
        "mod",
        "txp",
  }
@@@ -62,9 -62,19 +62,19 @@@ local REASON_UPDATE = "update
  local REASON_DEPENDENCY = "dependency"
  
  
+ -- encodes for use as URL parameter or path component
+ local function urlencode(str)
+       return str:gsub("[^%a%d()._~-]", function(char)
+               return string.format("%%%02X", string.byte(char))
+       end)
+ end
+ assert(urlencode("sample text?") == "sample%20text%3F")
  local function get_download_url(package, reason)
        local base_url = core.settings:get("contentdb_url")
-       local ret = base_url .. ("/packages/%s/%s/releases/%d/download/"):format(package.author, package.name, package.release)
+       local ret = base_url .. ("/packages/%s/releases/%d/download/"):format(
+               package.url_part, package.release)
        if reason then
                ret = ret .. "?reason=" .. reason
        end
  end
  
  
- local function download_package(param)
-       if core.download_file(param.url, param.filename) then
+ local function download_and_extract(param)
+       local package = param.package
+       local filename = core.get_temp_path(true)
+       if filename == "" or not core.download_file(param.url, filename) then
+               core.log("error", "Downloading " .. dump(param.url) .. " failed")
                return {
-                       filename = param.filename,
-                       successful = true,
+                       msg = fgettext("Failed to download $1", package.name)
                }
+       end
+       local tempfolder = core.get_temp_path()
+       if tempfolder ~= "" then
+               tempfolder = tempfolder .. DIR_DELIM .. "MT_" .. math.random(1, 1024000)
+               if not core.extract_zip(filename, tempfolder) then
+                       tempfolder = nil
+               end
        else
-               core.log("error", "downloading " .. dump(param.url) .. " failed")
+               tempfolder = nil
+       end
+       os.remove(filename)
+       if not tempfolder then
                return {
-                       successful = false,
+                       msg = fgettext("Install: Unsupported file type or broken archive"),
                }
        end
+       return {
+               path = tempfolder
+       }
  end
  
  local function start_install(package, reason)
        local params = {
                package = package,
                url = get_download_url(package, reason),
-               filename = os.tempfolder() .. "_MODNAME_" .. package.name .. ".zip",
        }
  
        number_downloading = number_downloading + 1
  
        local function callback(result)
-               if result.successful then
-                       local path, msg = pkgmgr.install(package.type,
-                                       result.filename, package.name,
-                                       package.path)
+               if result.msg then
+                       gamedata.errormessage = result.msg
+               else
+                       local path, msg = pkgmgr.install_dir(package.type, result.path, package.name, package.path)
+                       core.delete_dir(result.path)
                        if not path then
                                gamedata.errormessage = msg
                        else
                                        conf:write()
                                end
                        end
-                       os.remove(result.filename)
-               else
-                       gamedata.errormessage = fgettext("Failed to download $1", package.name)
                end
  
                package.downloading = false
        package.queued = false
        package.downloading = true
  
-       if not core.handle_async(download_package, params, callback) then
+       if not core.handle_async(download_and_extract, params, callback) then
                core.log("error", "ERROR: async event failed")
                gamedata.errormessage = fgettext("Failed to download $1", package.name)
                return
@@@ -184,7 -209,7 +209,7 @@@ local function get_raw_dependencies(pac
        local url_fmt = "/api/packages/%s/dependencies/?only_hard=1&protocol_version=%s&engine_version=%s"
        local version = core.get_version()
        local base_url = core.settings:get("contentdb_url")
-       local url = base_url .. url_fmt:format(package.id, core.get_max_supp_proto(), version.string)
+       local url = base_url .. url_fmt:format(package.url_part, core.get_max_supp_proto(), urlencode(version.string))
  
        local response = http.fetch_sync({ url = url })
        if not response.succeeded then
@@@ -559,17 -584,16 +584,16 @@@ function store.load(
        local base_url = core.settings:get("contentdb_url")
        local url = base_url ..
                "/api/packages/?type=mod&type=game&type=txp&protocol_version=" ..
-               core.get_max_supp_proto() .. "&engine_version=" .. version.string
+               core.get_max_supp_proto() .. "&engine_version=" .. urlencode(version.string)
  
        for _, item in pairs(core.settings:get("contentdb_flag_blacklist"):split(",")) do
                item = item:trim()
                if item ~= "" then
-                       url = url .. "&hide=" .. item
+                       url = url .. "&hide=" .. urlencode(item)
                end
        end
  
-       local timeout = tonumber(core.settings:get("curl_file_download_timeout"))
-       local response = http.fetch_sync({ url = url, timeout = timeout })
+       local response = http.fetch_sync({ url = url })
        if not response.succeeded then
                return
        end
  
        for _, package in pairs(store.packages_full) do
                local name_len = #package.name
+               -- This must match what store.update_paths() does!
+               package.id = package.author:lower() .. "/"
                if package.type == "game" and name_len > 5 and package.name:sub(name_len - 4) == "_game" then
-                       package.id = package.author:lower() .. "/" .. package.name:sub(1, name_len - 5)
+                       package.id = package.id .. package.name:sub(1, name_len - 5)
                else
-                       package.id = package.author:lower() .. "/" .. package.name
+                       package.id = package.id .. package.name
                end
  
+               package.url_part = urlencode(package.author) .. "/" .. urlencode(package.name)
                if package.aliases then
                        for _, alias in ipairs(package.aliases) do
                                -- We currently don't support name changing
@@@ -604,7 -632,7 +632,7 @@@ en
  function store.update_paths()
        local mod_hash = {}
        pkgmgr.refresh_globals()
 -      for _, mod in pairs(pkgmgr.global_mods:get_list()) do
 +      for _, mod in pairs(pkgmgr.clientmods:get_list()) do
                if mod.author and mod.release > 0 then
                        local id = mod.author:lower() .. "/" .. mod.name
                        mod_hash[store.aliases[id] or id] = mod
@@@ -834,8 -862,7 +862,7 @@@ function store.get_formspec(dlgdata
                        formspec[#formspec + 1] = "cdb_downloading.png;3;400;]"
                elseif package.queued then
                        formspec[#formspec + 1] = left_base
-                       formspec[#formspec + 1] = core.formspec_escape(defaulttexturedir)
-                       formspec[#formspec + 1] = "cdb_queued.png;queued]"
+                       formspec[#formspec + 1] = "cdb_queued.png;queued;]"
                elseif not package.path then
                        local elem_name = "install_" .. i .. ";"
                        formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#71aa34]"
@@@ -998,9 -1025,9 +1025,9 @@@ function store.handle_submit(this, fiel
                end
  
                if fields["view_" .. i] then
-                       local url = ("%s/packages/%s/%s?protocol_version=%d"):format(
-                                       core.settings:get("contentdb_url"),
-                                       package.author, package.name, core.get_max_supp_proto())
+                       local url = ("%s/packages/%s?protocol_version=%d"):format(
+                                       core.settings:get("contentdb_url"), package.url_part,
+                                       core.get_max_supp_proto())
                        core.open_url(url)
                        return true
                end
index 38a658969955bcbcae9c50e37bda19db83ebcfe4,320db7e40cb28eae5a5fe8416b3903343cc80a7f..d6f485cf73d29c2176b1c16a51bf8b2e94e9b2d4
@@@ -378,7 -378,7 +378,7 @@@ local function parse_config_file(read_a
                -- Parse mods
                local mods_category_initialized = false
                local mods = {}
-               get_mods(core.get_modpath(), mods)
+               get_mods(core.get_modpath(), "mods", mods)
                for _, mod in ipairs(mods) do
                        local path = mod.path .. DIR_DELIM .. FILENAME
                        local file = io.open(path, "r")
  
                                table.insert(settings, {
                                        name = mod.name,
+                                       readable_name = mod.title,
                                        level = 1,
                                        type = "category",
                                })
                                file:close()
                        end
                end
-               get_mods(core.get_clientmodpath(), clientmods)
 +
 +              -- Parse clientmods
 +              local clientmods_category_initialized = false
 +              local clientmods = {}
++              get_mods(core.get_clientmodpath(), "clientmods", clientmods)
 +              for _, clientmod in ipairs(clientmods) do
 +                      local path = clientmod.path .. DIR_DELIM .. FILENAME
 +                      local file = io.open(path, "r")
 +                      if file then
 +                              if not clientmods_category_initialized then
 +                                      fgettext_ne("Clientmods") -- not used, but needed for xgettext
 +                                      table.insert(settings, {
 +                                              name = "Clientmods",
 +                                              level = 0,
 +                                              type = "category",
 +                                      })
 +                                      clientmods_category_initialized = true
 +                              end
 +
 +                              table.insert(settings, {
 +                                      name = clientmod.name,
 +                                      level = 1,
 +                                      type = "category",
 +                              })
 +
 +                              parse_single_file(file, path, read_all, settings, 2, false)
 +
 +                              file:close()
 +                      end
 +              end
        end
  
        return settings
@@@ -527,44 -498,40 +528,40 @@@ en
  
  local function get_current_np_group(setting)
        local value = core.settings:get_np_group(setting.name)
-       local t = {}
        if value == nil then
-               t = setting.values
-       else
-               table.insert(t, value.offset)
-               table.insert(t, value.scale)
-               table.insert(t, value.spread.x)
-               table.insert(t, value.spread.y)
-               table.insert(t, value.spread.z)
-               table.insert(t, value.seed)
-               table.insert(t, value.octaves)
-               table.insert(t, value.persistence)
-               table.insert(t, value.lacunarity)
-               table.insert(t, value.flags)
+               return setting.values
        end
-       return t
+       local p = "%g"
+       return {
+               p:format(value.offset),
+               p:format(value.scale),
+               p:format(value.spread.x),
+               p:format(value.spread.y),
+               p:format(value.spread.z),
+               p:format(value.seed),
+               p:format(value.octaves),
+               p:format(value.persistence),
+               p:format(value.lacunarity),
+               value.flags
+       }
  end
  
  local function get_current_np_group_as_string(setting)
        local value = core.settings:get_np_group(setting.name)
-       local t
        if value == nil then
-               t = setting.default
-       else
-               t = value.offset .. ", " ..
-                       value.scale .. ", (" ..
-                       value.spread.x .. ", " ..
-                       value.spread.y .. ", " ..
-                       value.spread.z .. "), " ..
-                       value.seed .. ", " ..
-                       value.octaves .. ", " ..
-                       value.persistence .. ", " ..
-                       value.lacunarity
-               if value.flags ~= "" then
-                       t = t .. ", " .. value.flags
-               end
+               return setting.default
        end
-       return t
+       return ("%g, %g, (%g, %g, %g), %g, %g, %g, %g"):format(
+               value.offset,
+               value.scale,
+               value.spread.x,
+               value.spread.y,
+               value.spread.z,
+               value.seed,
+               value.octaves,
+               value.persistence,
+               value.lacunarity
+       ) .. (value.flags ~= "" and (", " .. value.flags) or "")
  end
  
  local checkboxes = {} -- handle checkboxes events
@@@ -697,7 -664,7 +694,7 @@@ local function create_change_setting_fo
        elseif setting.type == "v3f" then
                local val = get_current_value(setting)
                local v3f = {}
-               for line in val:gmatch("[+-]?[%d.-e]+") do -- All numeric characters
+               for line in val:gmatch("[+-]?[%d.+-eE]+") do -- All numeric characters
                        table.insert(v3f, line)
                end
  
@@@ -990,7 -957,7 +987,7 @@@ local function create_settings_formspec
        local current_level = 0
        for _, entry in ipairs(settings) do
                local name
-               if not core.settings:get_bool("main_menu_technical_settings") and entry.readable_name then
+               if not core.settings:get_bool("show_technical_names") and entry.readable_name then
                        name = fgettext_ne(entry.readable_name)
                else
                        name = entry.name
                        "button[10,4.9;2,1;btn_edit;" .. fgettext("Edit") .. "]" ..
                        "button[7,4.9;3,1;btn_restore;" .. fgettext("Restore Default") .. "]" ..
                        "checkbox[0,4.3;cb_tech_settings;" .. fgettext("Show technical names") .. ";"
-                                       .. dump(core.settings:get_bool("main_menu_technical_settings")) .. "]"
+                                       .. dump(core.settings:get_bool("show_technical_names")) .. "]"
  
        return formspec
  end
@@@ -1114,7 -1081,7 +1111,7 @@@ local function handle_settings_buttons(
        end
  
        if fields["cb_tech_settings"] then
-               core.settings:set("main_menu_technical_settings", fields["cb_tech_settings"])
+               core.settings:set("show_technical_names", fields["cb_tech_settings"])
                core.settings:write()
                core.update_formspec(this:get_formspec())
                return true
index 2481078e2733ee70aabeadc924befe584f2878ae,8e716c2ebceeb555983b196c2c9752badd64291b..94345467734943b5f10134224ea317d3b81817f0
@@@ -35,7 -35,7 +35,7 @@@ dofile(menupath .. DIR_DELIM .. "async_
  dofile(menupath .. DIR_DELIM .. "common.lua")
  dofile(menupath .. DIR_DELIM .. "pkgmgr.lua")
  dofile(menupath .. DIR_DELIM .. "serverlistmgr.lua")
- dofile(menupath .. DIR_DELIM .. "textures.lua")
+ dofile(menupath .. DIR_DELIM .. "game_theme.lua")
  
  dofile(menupath .. DIR_DELIM .. "dlg_config_world.lua")
  dofile(menupath .. DIR_DELIM .. "dlg_settings_advanced.lua")
@@@ -87,7 -87,7 +87,7 @@@ local function init_globals(
                core.settings:set("menu_last_game", default_game)
        end
  
-       mm_texture.init()
+       mm_game_theme.init()
  
        -- Create main tabview
        local tv_main = tabview_create("maintab", {x = 12, y = 5.4}, {x = 0, y = 0})
        if tv_main.current_tab == "local" then
                local game = pkgmgr.find_by_gameid(core.settings:get("menu_last_game"))
                if game == nil then
-                       mm_texture.reset()
+                       mm_game_theme.reset()
                end
        end
  
        tv_main:show()
  
        ui.update()
-       core.sound_play("main_menu", true)
  end
  
  init_globals()
 +
index 58a4ed8c1e8b68ecf663cdd1e1049391d62fb9f7,334fcf5f828fe70e325abdf0229a849be6bca9be..072c41f0cd17ad27a6744a0d06cd5c42343e51f7
@@@ -78,34 -78,35 +78,35 @@@ local function load_texture_packs(txtpa
  
        for _, item in ipairs(list) do
                if item ~= "base" then
-                       local name = item
                        local path = txtpath .. DIR_DELIM .. item .. DIR_DELIM
-                       if path == current_texture_path then
-                               name = fgettext("$1 (Enabled)", name)
-                       end
                        local conf = Settings(path .. "texture_pack.conf")
+                       local enabled = path == current_texture_path
+                       local title = conf:get("title") or item
  
+                       -- list_* is only used if non-nil, else the regular versions are used.
                        retval[#retval + 1] = {
                                name = item,
+                               title = title,
+                               list_name = enabled and fgettext("$1 (Enabled)", item) or nil,
+                               list_title = enabled and fgettext("$1 (Enabled)", title) or nil,
                                author = conf:get("author"),
                                release = tonumber(conf:get("release")) or 0,
-                               list_name = name,
                                type = "txp",
                                path = path,
-                               enabled = path == current_texture_path,
+                               enabled = enabled,
                        }
                end
        end
  end
  
- function get_mods(path,retval,modpack)
+ function get_mods(path, virtual_path, retval, modpack)
        local mods = core.get_dir_list(path, true)
  
        for _, name in ipairs(mods) do
                if name:sub(1, 1) ~= "." then
-                       local prefix = path .. DIR_DELIM .. name
+                       local mod_path = path .. DIR_DELIM .. name
+                       local mod_virtual_path = virtual_path .. "/" .. name
                        local toadd = {
                                dir_name = name,
                                parent_dir = path,
  
                        -- Get config file
                        local mod_conf
-                       local modpack_conf = io.open(prefix .. DIR_DELIM .. "modpack.conf")
+                       local modpack_conf = io.open(mod_path .. DIR_DELIM .. "modpack.conf")
                        if modpack_conf then
                                toadd.is_modpack = true
                                modpack_conf:close()
  
-                               mod_conf = Settings(prefix .. DIR_DELIM .. "modpack.conf"):to_table()
+                               mod_conf = Settings(mod_path .. DIR_DELIM .. "modpack.conf"):to_table()
                                if mod_conf.name then
                                        name = mod_conf.name
                                        toadd.is_name_explicit = true
                                end
                        else
-                               mod_conf = Settings(prefix .. DIR_DELIM .. "mod.conf"):to_table()
+                               mod_conf = Settings(mod_path .. DIR_DELIM .. "mod.conf"):to_table()
                                if mod_conf.name then
                                        name = mod_conf.name
                                        toadd.is_name_explicit = true
  
                        -- Read from config
                        toadd.name = name
+                       toadd.title = mod_conf.title
                        toadd.author = mod_conf.author
                        toadd.release = tonumber(mod_conf.release) or 0
-                       toadd.path = prefix
+                       toadd.path = mod_path
+                       toadd.virtual_path = mod_virtual_path
                        toadd.type = "mod"
  
                        -- Check modpack.txt
                        -- Note: modpack.conf is already checked above
-                       local modpackfile = io.open(prefix .. DIR_DELIM .. "modpack.txt")
+                       local modpackfile = io.open(mod_path .. DIR_DELIM .. "modpack.txt")
                        if modpackfile then
                                modpackfile:close()
                                toadd.is_modpack = true
                        elseif toadd.is_modpack then
                                toadd.type = "modpack"
                                toadd.is_modpack = true
-                               get_mods(prefix, retval, name)
+                               get_mods(mod_path, mod_virtual_path, retval, name)
                        end
                end
        end
@@@ -181,21 -184,6 +184,6 @@@ function pkgmgr.get_texture_packs(
  end
  
  --------------------------------------------------------------------------------
- function pkgmgr.extract(modfile)
-       if modfile.type == "zip" then
-               local tempfolder = os.tempfolder()
-               if tempfolder ~= nil and
-                       tempfolder ~= "" then
-                       core.create_dir(tempfolder)
-                       if core.extract_zip(modfile.name,tempfolder) then
-                               return tempfolder
-                       end
-               end
-       end
-       return nil
- end
  function pkgmgr.get_folder_type(path)
        local testfile = io.open(path .. DIR_DELIM .. "init.lua","r")
        if testfile ~= nil then
@@@ -349,7 -337,7 +337,7 @@@ function pkgmgr.identify_modname(modpat
        return nil
  end
  --------------------------------------------------------------------------------
- function pkgmgr.render_packagelist(render_list)
+ function pkgmgr.render_packagelist(render_list, use_technical_names)
        if not render_list then
                if not pkgmgr.global_mods then
                        pkgmgr.refresh_globals()
                else
                        retval[#retval + 1] = "0"
                end
-               retval[#retval + 1] = core.formspec_escape(v.list_name or v.name)
+               if use_technical_names then
+                       retval[#retval + 1] = core.formspec_escape(v.list_name or v.name)
+               else
+                       retval[#retval + 1] = core.formspec_escape(v.list_title or v.list_name or v.title or v.name)
+               end
        end
  
        return table.concat(retval, ",")
@@@ -412,6 -405,14 +405,14 @@@ function pkgmgr.is_modpack_entirely_ena
        return true
  end
  
+ local function disable_all_by_name(list, name, except)
+       for i=1, #list do
+               if list[i].name == name and list[i] ~= except then
+                       list[i].enabled = false
+               end
+       end
+ end
  ---------- toggles or en/disables a mod or modpack and its dependencies --------
  local function toggle_mod_or_modpack(list, toggled_mods, enabled_mods, toset, mod)
        if not mod.is_modpack then
                        toset = not mod.enabled
                end
                if mod.enabled ~= toset then
-                       mod.enabled = toset
                        toggled_mods[#toggled_mods+1] = mod.name
                end
                if toset then
                        -- Mark this mod for recursive dependency traversal
                        enabled_mods[mod.name] = true
+                       -- Disable other mods with the same name
+                       disable_all_by_name(list, mod.name, mod)
                end
+               mod.enabled = toset
        else
                -- Toggle or en/disable every mod in the modpack,
                -- interleaved unsupported
@@@ -451,7 -455,7 +455,7 @@@ function pkgmgr.enable_mod(this, toset
        local enabled_mods = {}
        toggle_mod_or_modpack(list, toggled_mods, enabled_mods, toset, mod)
  
-       if not toset then
+       if next(enabled_mods) == nil then
                -- Mod(s) were disabled, so no dependencies need to be enabled
                table.sort(toggled_mods)
                core.log("info", "Following mods were disabled: " ..
  
        -- Enable mods' depends after activation
  
-       -- Make a list of mod ids indexed by their names
+       -- Make a list of mod ids indexed by their names. Among mods with the
+       -- same name, enabled mods take precedence, after which game mods take
+       -- precedence, being last in the mod list.
        local mod_ids = {}
        for id, mod2 in pairs(list) do
                if mod2.type == "mod" and not mod2.is_modpack then
-                       mod_ids[mod2.name] = id
+                       local prev_id = mod_ids[mod2.name]
+                       if not prev_id or not list[prev_id].enabled then
+                               mod_ids[mod2.name] = id
+                       end
                end
        end
  
                        end
                end
        end
        -- If sp is 0, every dependency is already activated
        while sp > 0 do
                local name = to_enable[sp]
                                core.log("warning", "Mod dependency \"" .. name ..
                                        "\" not found!")
                        else
-                               if mod_to_enable.enabled == false then
+                               if not mod_to_enable.enabled then
                                        mod_to_enable.enabled = true
                                        toggled_mods[#toggled_mods+1] = mod_to_enable.name
                                end
                                -- Push the dependencies of the dependency onto the stack
                                local depends = pkgmgr.get_dependencies(mod_to_enable.path)
                                for i = 1, #depends do
-                                       if not enabled_mods[name] then
+                                       if not enabled_mods[depends[i]] then
                                                sp = sp+1
                                                to_enable[sp] = depends[i]
                                        end
@@@ -561,11 -571,10 +571,10 @@@ function pkgmgr.install_dir(type, path
                local from = basefolder and basefolder.path or path
                if targetpath then
                        core.delete_dir(targetpath)
-                       core.create_dir(targetpath)
                else
                        targetpath = core.get_texturepath() .. DIR_DELIM .. basename
                end
-               if not core.copy_dir(from, targetpath) then
+               if not core.copy_dir(from, targetpath, false) then
                        return nil,
                                fgettext("Failed to install $1 to $2", basename, targetpath)
                end
                -- Get destination name for modpack
                if targetpath then
                        core.delete_dir(targetpath)
-                       core.create_dir(targetpath)
                else
                        local clean_path = nil
                        if basename ~= nil then
                                clean_path = get_last_folder(cleanup_path(basefolder.path))
                        end
                        if clean_path then
 -                              targetpath = core.get_modpath() .. DIR_DELIM .. clean_path
 +                              targetpath = core.get_clientmodpath() .. DIR_DELIM .. clean_path
                        else
                                return nil,
                                        fgettext("Install Mod: Unable to find suitable folder name for modpack $1",
  
                if targetpath then
                        core.delete_dir(targetpath)
-                       core.create_dir(targetpath)
                else
                        local targetfolder = basename
                        if targetfolder == nil then
                        end
  
                        if targetfolder ~= nil and pkgmgr.isValidModname(targetfolder) then
 -                              targetpath = core.get_modpath() .. DIR_DELIM .. targetfolder
 +                              targetpath = core.get_clientmodpath() .. DIR_DELIM .. targetfolder
                        else
                                return nil, fgettext("Install Mod: Unable to find real mod name for: $1", path)
                        end
  
                if targetpath then
                        core.delete_dir(targetpath)
-                       core.create_dir(targetpath)
                else
                        targetpath = core.get_gamepath() .. DIR_DELIM .. basename
                end
        end
  
        -- Copy it
-       if not core.copy_dir(basefolder.path, targetpath) then
+       if not core.copy_dir(basefolder.path, targetpath, false) then
                return nil,
                        fgettext("Failed to install $1 to $2", basename, targetpath)
        end
        return targetpath, nil
  end
  
- --------------------------------------------------------------------------------
- function pkgmgr.install(type, modfilename, basename, dest)
-       local archive_info = pkgmgr.identify_filetype(modfilename)
-       local path = pkgmgr.extract(archive_info)
-       if path == nil then
-               return nil,
-                       fgettext("Install: file: \"$1\"", archive_info.name) .. "\n" ..
-                       fgettext("Install: Unsupported file type \"$1\" or broken archive",
-                               archive_info.type)
-       end
-       local targetpath, msg = pkgmgr.install_dir(type, path, basename, dest)
-       core.delete_dir(path)
-       return targetpath, msg
- end
  --------------------------------------------------------------------------------
-       if modpath ~= nil and
-               modpath ~= "" then
-               get_mods(modpath,clientmods)
 +function pkgmgr.prepareclientmodlist(data)
 +      local retval = {}
 +
 +      local clientmods = {}
 +
 +      --read clientmods
 +      local modpath = core.get_clientmodpath()
 +
++      if modpath ~= nil and modpath ~= "" then
++              get_mods(modpath, "clientmods", clientmods)
 +      end
 +
 +      for i=1,#clientmods,1 do
 +              clientmods[i].type = "mod"
 +              clientmods[i].loc = "global"
 +              clientmods[i].is_clientside = true
 +              retval[#retval + 1] = clientmods[i]
 +      end
 +
 +      --read mods configuration
 +      local filename = modpath ..
 +                              DIR_DELIM .. "mods.conf"
 +
 +      local conffile = Settings(filename)
 +
 +      for key,value in pairs(conffile:to_table()) do
 +              if key:sub(1, 9) == "load_mod_" then
 +                      key = key:sub(10)
 +                      local element = nil
 +                      for i=1,#retval,1 do
 +                              if retval[i].name == key and
 +                                      not retval[i].is_modpack then
 +                                      element = retval[i]
 +                                      break
 +                              end
 +                      end
 +                      if element ~= nil then
 +                              element.enabled = value ~= "false" and value ~= "nil" and value
 +                      else
 +                              core.log("info", "Clientmod: " .. key .. " " .. dump(value) .. " but not found")
 +                      end
 +              end
 +      end
 +
 +      return retval
 +end
 +
  function pkgmgr.preparemodlist(data)
        local retval = {}
  
        local game_mods = {}
  
        --read global mods
-       local modpath = core.get_modpath()
-       if modpath ~= nil and
-               modpath ~= "" then
-               get_mods(modpath,global_mods)
+       local modpaths = core.get_modpaths()
+       for key, modpath in pairs(modpaths) do
+               get_mods(modpath, key, global_mods)
        end
  
        for i=1,#global_mods,1 do
                global_mods[i].type = "mod"
                global_mods[i].loc = "global"
+               global_mods[i].enabled = false
                retval[#retval + 1] = global_mods[i]
        end
  
                                DIR_DELIM .. "world.mt"
  
        local worldfile = Settings(filename)
-       for key,value in pairs(worldfile:to_table()) do
+       for key, value in pairs(worldfile:to_table()) do
                if key:sub(1, 9) == "load_mod_" then
                        key = key:sub(10)
-                       local element = nil
-                       for i=1,#retval,1 do
+                       local mod_found = false
+                       local fallback_found = false
+                       local fallback_mod = nil
+                       for i=1, #retval do
                                if retval[i].name == key and
-                                       not retval[i].is_modpack then
-                                       element = retval[i]
-                                       break
+                                               not retval[i].is_modpack then
+                                       if core.is_yes(value) or retval[i].virtual_path == value then
+                                               retval[i].enabled = true
+                                               mod_found = true
+                                               break
+                                       elseif fallback_found then
+                                               -- Only allow fallback if only one mod matches
+                                               fallback_mod = nil
+                                       else
+                                               fallback_found = true
+                                               fallback_mod = retval[i]
+                                       end
                                end
                        end
-                       if element ~= nil then
-                               element.enabled = value ~= "false" and value ~= "nil" and value
-                       else
-                               core.log("info", "Mod: " .. key .. " " .. dump(value) .. " but not found")
+                       if not mod_found then
+                               if fallback_mod and value:find("/") then
+                                       fallback_mod.enabled = true
+                               else
+                                       core.log("info", "Mod: " .. key .. " " .. dump(value) .. " but not found")
+                               end
                        end
                end
        end
@@@ -865,51 -820,8 +867,12 @@@ function pkgmgr.refresh_globals(
                        pkgmgr.comparemod, is_equal, nil, {})
        pkgmgr.global_mods:add_sort_mechanism("alphabetic", sort_mod_list)
        pkgmgr.global_mods:set_sortmode("alphabetic")
 +      pkgmgr.clientmods = filterlist.create(pkgmgr.prepareclientmodlist,
 +                      pkgmgr.comparemod, is_equal, nil, {})
 +      pkgmgr.clientmods:add_sort_mechanism("alphabetic", sort_mod_list)
 +      pkgmgr.clientmods:set_sortmode("alphabetic")
  end
  
- --------------------------------------------------------------------------------
- function pkgmgr.identify_filetype(name)
-       if name:sub(-3):lower() == "zip" then
-               return {
-                               name = name,
-                               type = "zip"
-                               }
-       end
-       if name:sub(-6):lower() == "tar.gz" or
-               name:sub(-3):lower() == "tgz"then
-               return {
-                               name = name,
-                               type = "tgz"
-                               }
-       end
-       if name:sub(-6):lower() == "tar.bz2" then
-               return {
-                               name = name,
-                               type = "tbz"
-                               }
-       end
-       if name:sub(-2):lower() == "7z" then
-               return {
-                               name = name,
-                               type = "7z"
-                               }
-       end
-       return {
-               name = name,
-               type = "ukn"
-       }
- end
  --------------------------------------------------------------------------------
  function pkgmgr.find_by_gameid(gameid)
        for i=1,#pkgmgr.games,1 do
@@@ -925,7 -837,7 +888,7 @@@ function pkgmgr.get_game_mods(gamespec
        if gamespec ~= nil and
                gamespec.gamemods_path ~= nil and
                gamespec.gamemods_path ~= "" then
-               get_mods(gamespec.gamemods_path, retval)
+               get_mods(gamespec.gamemods_path, ("games/%s/mods"):format(gamespec.id), retval)
        end
  end
  
index cb566f773bd94dbd4c0cff6cc480bc6ba6c96d2d,ba258fd2d0486672aa95abd4d933287dc5d9de91..3336786ba4ea9e56d4cafbf29a4eda18d632da28
  --51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  
  --------------------------------------------------------------------------------
 +local dragonfire_team = {
 +      "Elias Fleckenstein [Main Developer]",
 +      "cora [Core Developer]",
 +      "emilia [Core Developer]",
 +      "oneplustwo [Developer]",
 +      "joshia_wi [Developer]",
 +      "Code-Sploit [Developer]",
 +      "DerZombiiie [User Support]",
 +      "Rtx [User Support]",
 +}
  
  local core_developers = {
        "Perttu Ahola (celeron55) <celeron55@gmail.com>",
        "Lars Hofhansl <larsh@apache.org>",
        "Pierre-Yves Rollo <dev@pyrollo.com>",
        "v-rob <robinsonvincent89@gmail.com>",
+       "hecks",
+       "Hugues Ross <hugues.ross@gmail.com>",
+       "Dmitry Kostenko (x2048) <codeforsmile@gmail.com>",
  }
  
  -- For updating active/previous contributors, see the script in ./util/gather_git_credits.py
  
  local active_contributors = {
-       "Wuzzy [devtest game, visual corrections]",
-       "Zughy [Visual improvements, various fixes]",
-       "Maksim (MoNTE48) [Android]",
+       "Wuzzy [I18n for builtin, liquid features, fixes]",
+       "Zughy [Various features and fixes]",
        "numzero [Graphics and rendering]",
-       "appgurueu [Various internal fixes]",
-       "Desour [Formspec and vector API changes]",
-       "HybridDog [Rendering fixes and documentation]",
-       "Hugues Ross [Graphics-related improvements]",
-       "ANAND (ClobberXD) [Mouse buttons rebinding]",
-       "luk3yx [Fixes]",
-       "hecks [Audiovisuals, Lua API]",
-       "LoneWolfHT [Object crosshair, documentation fixes]",
-       "Lejo [Server-related improvements]",
-       "EvidenceB [Compass HUD element]",
-       "Paul Ouellette (pauloue) [Lua API, documentation]",
-       "TheTermos [Collision detection, physics]",
+       "Desour [Internal fixes, Clipboard on X11]",
+       "Lars Müller [Various internal fixes]",
+       "JosiahWI [CMake, cleanups and fixes]",
+       "HybridDog [builtin, documentation]",
+       "Jude Melton-Houghton [Database implementation]",
+       "savilli [Fixes]",
+       "Liso [Shadow Mapping]",
+       "MoNTE48 [Build fix]",
+       "Jean-Patrick Guerrero (kilbith) [Fixes]",
+       "ROllerozxa [Code cleanups]",
+       "Lejo [bitop library integration]",
+       "LoneWolfHT [Build fixes]",
+       "NeroBurner [Joystick]",
+       "Elias Fleckenstein [Internal fixes]",
        "David CARLIER [Unix & Haiku build fixes]",
-       "dcbrwn [Object shading]",
-       "Elias Fleckenstein [API features/fixes]",
-       "Jean-Patrick Guerrero (kilbith) [model element, visual fixes]",
-       "k.h.lai [Memory leak fixes, documentation]",
+       "pecksin [Clickable web links]",
+       "srfqi [Android & rendering fixes]",
+       "EvidenceB [Formspec]",
  }
  
  local previous_core_developers = {
@@@ -80,6 -73,7 +83,7 @@@
        "Zeno",
        "ShadowNinja <shadowninja@minetest.net>",
        "Auke Kok (sofar) <sofar@foo-projects.org>",
+       "Aaron Suen <warr1024@gmail.com>",
  }
  
  local previous_contributors = {
        "MirceaKitsune <mirceakitsune@gmail.com>",
        "Constantin Wenger (SpeedProg)",
        "Ciaran Gultnieks (CiaranG)",
-       "stujones11 [Android UX improvements]",
-       "Rogier <rogier777@gmail.com> [Fixes]",
-       "Gregory Currie (gregorycu) [optimisation]",
-       "srifqi [Fixes]",
+       "Paul Ouellette (pauloue)",
+       "stujones11",
+       "Rogier <rogier777@gmail.com>",
+       "Gregory Currie (gregorycu)",
        "JacobF",
        "Jeija <jeija@mesecons.net> [HTTP, particles]",
  }
@@@ -119,8 -113,6 +123,8 @@@ return 
                        "tablecolumns[color;text]" ..
                        "tableoptions[background=#00000000;highlight=#00000000;border=false]" ..
                        "table[3.5,-0.25;8.5,6.05;list_credits;" ..
 +                      "#FFFF00," .. fgettext("Dragonfire Team") .. ",," ..
 +                      buildCreditList(dragonfire_team) .. ",,," ..
                        "#FFFF00," .. fgettext("Core Developers") .. ",," ..
                        buildCreditList(core_developers) .. ",,," ..
                        "#FFFF00," .. fgettext("Active Contributors") .. ",," ..
index 1bffeeb223234353a51fd65f301255254e1625a4,5e14d1902efcc506bed6ea9d3ad6638012f76102..a366d4ab4a11e3907c928ebab755903408e384cf
  local packages_raw
  local packages
  
 +local function modname_valid(name)
 +      return not name:find("[^a-z0-9_]")
 +end
 +
  --------------------------------------------------------------------------------
  local function get_formspec(tabview, name, tabdata)
        if pkgmgr.global_mods == nil then
@@@ -37,7 -33,6 +37,7 @@@
                table.insert_all(packages_raw, pkgmgr.games)
                table.insert_all(packages_raw, pkgmgr.get_texture_packs())
                table.insert_all(packages_raw, pkgmgr.global_mods:get_list())
 +              table.insert_all(packages_raw, pkgmgr.clientmods:get_list())
  
                local function get_data()
                        return packages_raw
  
                packages = filterlist.create(get_data, pkgmgr.compare_package,
                                is_equal, nil, {})
 +
 +              local filename = core.get_clientmodpath() .. DIR_DELIM .. "mods.conf"
 +
 +              local conffile = Settings(filename)
 +              local mods = conffile:to_table()
 +
 +              for i = 1, #packages_raw do
 +                      local mod = packages_raw[i]
 +                      if mod.is_clientside and not mod.is_modpack then
 +                              if modname_valid(mod.name) then
 +                                      conffile:set("load_mod_" .. mod.name,
 +                                              mod.enabled and "true" or "false")
 +                              elseif mod.enabled then
 +                                      gamedata.errormessage = fgettext_ne("Failed to enable clientmo" ..
 +                                              "d \"$1\" as it contains disallowed characters. " ..
 +                                              "Only characters [a-z0-9_] are allowed.",
 +                                              mod.name)
 +                              end
 +                              mods["load_mod_" .. mod.name] = nil
 +                      end
 +              end
 +
 +              -- Remove mods that are not present anymore
 +              for key in pairs(mods) do
 +                      if key:sub(1, 9) == "load_mod_" then
 +                              conffile:remove(key)
 +                      end
 +              end
 +
 +              if not conffile:write() then
 +                      core.log("error", "Failed to write clientmod config file")
 +              end
        end
  
        if tabdata.selected_pkg == nil then
                tabdata.selected_pkg = 1
        end
  
+       local use_technical_names = core.settings:get_bool("show_technical_names")
  
        local retval =
                "label[0.05,-0.25;".. fgettext("Installed Packages:") .. "]" ..
                "tablecolumns[color;tree;text]" ..
                "table[0,0.25;5.1,4.3;pkglist;" ..
-               pkgmgr.render_packagelist(packages) ..
+               pkgmgr.render_packagelist(packages, use_technical_names) ..
                ";" .. tabdata.selected_pkg .. "]" ..
                "button[0,4.85;5.25,0.5;btn_contentdb;".. fgettext("Browse online content") .. "]"
  
                        desc = info.description
                end
  
+               local title_and_name
+               if selected_pkg.type == "game" then
+                       title_and_name = selected_pkg.name
+               else
+                       title_and_name = (selected_pkg.title or selected_pkg.name) .. "\n" ..
+                               core.colorize("#BFBFBF", selected_pkg.name)
+               end
                retval = retval ..
                                "image[5.5,0;3,2;" .. core.formspec_escape(modscreenshot) .. "]" ..
-                               "label[8.25,0.6;" .. core.formspec_escape(selected_pkg.name) .. "]" ..
+                               "label[8.25,0.6;" .. core.formspec_escape(title_and_name) .. "]" ..
                                "box[5.5,2.2;6.15,2.35;#000]"
  
                if selected_pkg.type == "mod" then
                        if selected_pkg.is_modpack then
 -                              retval = retval ..
 -                                      "button[8.65,4.65;3.25,1;btn_mod_mgr_rename_modpack;" ..
 -                                      fgettext("Rename") .. "]"
 +                              if selected_pkg.is_clientside then
 +                                      if pkgmgr.is_modpack_entirely_enabled({list = packages}, selected_pkg.name) then
 +                                              retval = retval ..
 +                                                      "button[8.65,4.65;3.25,1;btn_mod_mgr_mp_disable;" ..
 +                                                      fgettext("Disable modpack") .. "]"
 +                                      else
 +                                              retval = retval ..
 +                                                      "button[8.65,4.65;3.25,1;btn_mod_mgr_mp_enable;" ..
 +                                                      fgettext("Enable modpack") .. "]"
 +                                      end
 +                              else
 +                                      retval = retval ..
 +                                              "button[8.65,4.65;3.25,1;btn_mod_mgr_rename_modpack;" ..
 +                                              fgettext("Rename") .. "]"
 +                              end
                        else
                                --show dependencies
                                desc = desc .. "\n\n"
                                                        "\n" .. toadd_soft
                                        end
                                end
 +                              if selected_pkg.is_clientside then
 +                                      if selected_pkg.enabled then
 +                                              retval = retval ..
 +                                                      "button[8.65,4.65;3.25,1;btn_mod_mgr_disable_mod;" ..
 +                                                      fgettext("Disable") .. "]"
 +                                      else
 +                                              retval = retval ..
 +                                                      "button[8.65,4.65;3.25,1;btn_mod_mgr_enable_mod;" ..
 +                                                      fgettext("Enable") .. "]"
 +                                      end
 +                              end
                        end
  
                else
  end
  
  --------------------------------------------------------------------------------
 -local function handle_doubleclick(pkg)
 +local function handle_doubleclick(pkg, pkg_name)
        if pkg.type == "txp" then
                if core.settings:get("texture_path") == pkg.path then
                        core.settings:set("texture_path", "")
                        core.settings:set("texture_path", pkg.path)
                end
                packages = nil
+               mm_game_theme.init()
+               mm_game_theme.reset()
 +      elseif pkg.is_clientside then
 +              pkgmgr.enable_mod({data = {list = packages, selected_mod = pkg_name}})
 +              packages = nil
        end
  end
  
@@@ -226,27 -176,11 +239,27 @@@ local function handle_buttons(tabview, 
                local event = core.explode_table_event(fields["pkglist"])
                tabdata.selected_pkg = event.row
                if event.type == "DCL" then
 -                      handle_doubleclick(packages:get_list()[tabdata.selected_pkg])
 +                      handle_doubleclick(packages:get_list()[tabdata.selected_pkg], tabdata.selected_pkg)
                end
                return true
        end
  
 +      if fields.btn_mod_mgr_mp_enable ~= nil or
 +                      fields.btn_mod_mgr_mp_disable ~= nil then
 +              pkgmgr.enable_mod({data = {list = packages, selected_mod = tabdata.selected_pkg}},
 +                      fields.btn_mod_mgr_mp_enable ~= nil)
 +              packages = nil
 +              return true
 +      end
 +
 +      if fields.btn_mod_mgr_enable_mod ~= nil or
 +                      fields.btn_mod_mgr_disable_mod ~= nil then
 +              pkgmgr.enable_mod({data = {list = packages, selected_mod = tabdata.selected_pkg}},
 +                      fields.btn_mod_mgr_enable_mod ~= nil)
 +              packages = nil
 +              return true
 +      end
 +
        if fields["btn_contentdb"] ~= nil then
                local dlg = create_store_dlg()
                dlg:set_parent(tabview)
                return true
        end
  
-       if fields.btn_mod_mgr_use_txp then
-               local txp = packages:get_list()[tabdata.selected_pkg]
-               core.settings:set("texture_path", txp.path)
-               packages = nil
-               return true
-       end
+       if fields.btn_mod_mgr_use_txp or fields.btn_mod_mgr_disable_txp then
+               local txp_path = ""
+               if fields.btn_mod_mgr_use_txp then
+                       txp_path = packages:get_list()[tabdata.selected_pkg].path
+               end
  
-       if fields.btn_mod_mgr_disable_txp then
-               core.settings:set("texture_path", "")
+               core.settings:set("texture_path", txp_path)
                packages = nil
+               mm_game_theme.init()
+               mm_game_theme.reset()
                return true
        end
  
diff --combined builtin/settingtypes.txt
index e023aeab7416d442ee0acb996597104c6646428a,ff69d97412768cbc4d8c4e457edb989a4dd5203a..2e0bb560a7739e406de88063cfdc224bc57d54a1
@@@ -146,17 -146,17 +146,17 @@@ enable_joysticks (Enable joysticks) boo
  joystick_id (Joystick ID) int 0
  
  #    The type of joystick
- joystick_type (Joystick type) enum auto auto,generic,xbox
+ joystick_type (Joystick type) enum auto auto,generic,xbox,dragonrise_gamecube
  
  #    The time in seconds it takes between repeated events
  #    when holding down a joystick button combination.
  repeat_joystick_button_time (Joystick button repetition interval) float 0.17 0.001
  
- #    The deadzone of the joystick
- joystick_deadzone (Joystick deadzone) int 2048
+ #    The dead zone of the joystick
+ joystick_deadzone (Joystick dead zone) int 2048
  
  #    The sensitivity of the joystick axes for moving the
- #    ingame view frustum around.
+ #    in-game view frustum around.
  joystick_frustum_sensitivity (Joystick frustum sensitivity) float 170
  
  #    Key for moving the player forward.
@@@ -197,10 -197,6 +197,10 @@@ keymap_place (Place key) key KEY_RBUTTO
  #    See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3
  keymap_inventory (Inventory key) key KEY_KEY_I
  
 +#    Key for opening the special inventory.
 +#    See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3
 +keymap_special_inventory (Special inventory key) key KEY_KEY_O
 +
  #    Key for moving fast in fast mode.
  #    See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3
  keymap_aux1 (Aux1 key) key KEY_KEY_E
@@@ -281,14 -277,6 +281,14 @@@ keymap_drop (Drop item key) key KEY_KEY
  #    See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3
  keymap_zoom (View zoom key) key KEY_KEY_Z
  
 +#    Key for toggling Killaura.
 +#    See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3
 +keymap_toggle_killaura (Killaura key) key KEY_KEY_X
 +
 +#    Key for toggling Freecam.
 +#    See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3
 +keymap_toggle_freecam (Freecam key) key KEY_KEY_G
 +
  #    Key for selecting the first hotbar slot.
  #    See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3
  keymap_slot1 (Hotbar slot 1 key) key KEY_KEY_1
@@@ -463,9 -451,9 +463,9 @@@ keymap_decrease_viewing_range_min (Vie
  
  [**Basic]
  
- #    Whether nametag backgrounds should be shown by default.
+ #    Whether name tag backgrounds should be shown by default.
  #    Mods may still set a background.
- show_nametag_backgrounds (Show nametag backgrounds by default) bool true
+ show_nametag_backgrounds (Show name tag backgrounds by default) bool true
  
  #    Enable vertex buffer objects.
  #    This should greatly improve graphics performance.
@@@ -487,6 -475,10 +487,10 @@@ connected_glass (Connect glass) bool fa
  #    Disable for speed or for different looks.
  smooth_lighting (Smooth lighting) bool true
  
+ #    Enables tradeoffs that reduce CPU load or increase rendering performance
+ #    at the expense of minor visual glitches that do not impact game playability.
+ performance_tradeoffs (Tradeoffs for performance) bool false
  #    Clouds are a client side effect.
  enable_clouds (Clouds) bool true
  
@@@ -501,7 -493,7 +505,7 @@@ enable_particles (Digging particles) bo
  
  [**Filtering]
  
- #    Use mip mapping to scale textures. May slightly increase performance,
+ #    Use mipmapping to scale textures. May slightly increase performance,
  #    especially when using a high resolution texture pack.
  #    Gamma correct downscaling is not supported.
  mip_map (Mipmapping) bool false
@@@ -600,9 -592,10 +604,10 @@@ enable_waving_plants (Waving plants) bo
  #    Requires shaders to be enabled.
  enable_dynamic_shadows (Dynamic shadows) bool false
  
- #    Set the shadow strength.
+ #    Set the shadow strength gamma.
+ #    Adjusts the intensity of in-game dynamic shadows.
  #    Lower value means lighter shadows, higher value means darker shadows.
- shadow_strength (Shadow strength) float 0.2 0.05 1.0
+ shadow_strength_gamma (Shadow strength gamma) float 1.0 0.1 10.0
  
  #    Maximum distance to render shadows.
  shadow_map_max_distance (Shadow map max distance in nodes to render shadows) float 120.0 10.0 1000.0
@@@ -621,12 -614,12 +626,12 @@@ shadow_map_texture_32bit (Shadow map te
  #    On true uses Poisson disk to make "soft shadows". Otherwise uses PCF filtering.
  shadow_poisson_filter (Poisson filtering) bool true
  
- #   Define shadow filtering quality
+ #   Define shadow filtering quality.
  #   This simulates the soft shadows effect by applying a PCF or Poisson disk
  #   but also uses more resources.
  shadow_filters (Shadow filter quality) enum 1 0,1,2
  
- #    Enable colored shadows. 
+ #    Enable colored shadows.
  #    On true translucent nodes cast colored shadows. This is expensive.
  shadow_map_color (Colored shadows) bool false
  
@@@ -638,10 -631,10 +643,10 @@@ shadow_update_frames (Map shadows updat
  
  #    Set the soft shadow radius size.
  #    Lower values mean sharper shadows, bigger values mean softer shadows.
- #    Minimum value: 1.0; maxiumum value: 10.0
+ #    Minimum value: 1.0; maximum value: 10.0
  shadow_soft_radius (Soft shadow radius) float 1.0 1.0 10.0
  
- #    Set the tilt of Sun/Moon orbit in degrees
+ #    Set the tilt of Sun/Moon orbit in degrees.
  #    Value of 0 means no tilt / vertical orbit.
  #    Minimum value: 0.0; maximum value: 60.0
  shadow_sky_body_orbit_tilt (Sky Body Orbit Tilt) float 0.0 0.0 60.0
@@@ -801,7 -794,7 +806,7 @@@ desynchronize_mapblock_texture_animatio
  #    Useful if there's something to be displayed right or left of hotbar.
  hud_hotbar_max_width (Maximum hotbar width) float 1.0
  
- #    Modifies the size of the hudbar elements.
+ #    Modifies the size of the HUD elements.
  hud_scaling (HUD scale factor) float 1.0
  
  #    Enables caching of facedir rotated meshes.
@@@ -865,6 -858,10 +870,10 @@@ autoscale_mode (Autoscaling mode) enum 
  #    A restart is required after changing this.
  show_entity_selectionbox (Show entity selection boxes) bool false
  
+ #    Distance in nodes at which transparency depth sorting is enabled
+ #    Use this to limit the performance impact of transparency depth sorting
+ transparency_sorting_distance (Transparency Sorting Distance) int 16 0 128
  [*Menus]
  
  #    Use a cloud animation for the main menu background.
@@@ -894,10 -891,6 +903,6 @@@ tooltip_show_delay (Tooltip delay) int 
  #    Append item name to tooltip.
  tooltip_append_itemname (Append item name) bool false
  
- #    Whether FreeType fonts are used, requires FreeType support to be compiled in.
- #    If disabled, bitmap and XML vectors fonts are used instead.
- freetype (FreeType fonts) bool true
  font_bold (Font bold by default) bool false
  
  font_italic (Font italic by default) bool false
@@@ -908,12 -901,16 +913,16 @@@ font_shadow (Font shadow) int 
  #    Opaqueness (alpha) of the shadow behind the default font, between 0 and 255.
  font_shadow_alpha (Font shadow alpha) int 127 0 255
  
- #    Font size of the default font in point (pt).
+ #    Font size of the default font where 1 unit = 1 pixel at 96 DPI
  font_size (Font size) int 16 1
  
- #    Path to the default font.
- #    If “freetype” setting is enabled: Must be a TrueType font.
- #    If “freetype” setting is disabled: Must be a bitmap or XML vectors font.
+ #    For pixel-style fonts that do not scale well, this ensures that font sizes used
+ #    with this font will always be divisible by this value, in pixels. For instance,
+ #    a pixel font 16 pixels tall should have this set to 16, so it will only ever be
+ #    sized 16, 32, 48, etc., so a mod requesting a size of 25 will get 32.
+ font_size_divisible_by (Font size divisible by) int 1 1
+ #    Path to the default font. Must be a TrueType font.
  #    The fallback font will be used if the font cannot be loaded.
  font_path (Regular font path) filepath fonts/Arimo-Regular.ttf
  
@@@ -921,12 -918,16 +930,16 @@@ font_path_bold (Bold font path) filepat
  font_path_italic (Italic font path) filepath fonts/Arimo-Italic.ttf
  font_path_bold_italic (Bold and italic font path) filepath fonts/Arimo-BoldItalic.ttf
  
- #    Font size of the monospace font in point (pt).
- mono_font_size (Monospace font size) int 15 1
+ #    Font size of the monospace font where 1 unit = 1 pixel at 96 DPI
+ mono_font_size (Monospace font size) int 16 1
  
- #    Path to the monospace font.
- #    If “freetype” setting is enabled: Must be a TrueType font.
- #    If “freetype” setting is disabled: Must be a bitmap or XML vectors font.
+ #    For pixel-style fonts that do not scale well, this ensures that font sizes used
+ #    with this font will always be divisible by this value, in pixels. For instance,
+ #    a pixel font 16 pixels tall should have this set to 16, so it will only ever be
+ #    sized 16, 32, 48, etc., so a mod requesting a size of 25 will get 32.
+ mono_font_size_divisible_by (Monospace font size divisible by) int 1 1
+ #    Path to the monospace font. Must be a TrueType font.
  #    This font is used for e.g. the console and profiler screen.
  mono_font_path (Monospace font path) filepath fonts/Cousine-Regular.ttf
  
@@@ -934,9 -935,7 +947,7 @@@ mono_font_path_bold (Bold monospace fon
  mono_font_path_italic (Italic monospace font path) filepath fonts/Cousine-Italic.ttf
  mono_font_path_bold_italic (Bold and italic monospace font path) filepath fonts/Cousine-BoldItalic.ttf
  
- #    Path of the fallback font.
- #    If “freetype” setting is enabled: Must be a TrueType font.
- #    If “freetype” setting is disabled: Must be a bitmap or XML vectors font.
+ #    Path of the fallback font. Must be a TrueType font.
  #    This font will be used for certain languages or if the default font is unavailable.
  fallback_font_path (Fallback font path) filepath fonts/DroidSansFallbackFull.ttf
  
@@@ -949,7 -948,7 +960,7 @@@ chat_font_size (Chat font size) int 
  screenshot_path (Screenshot folder) path screenshots
  
  #    Format of screenshots.
- screenshot_format (Screenshot format) enum png png,jpg,bmp,pcx,ppm,tga
+ screenshot_format (Screenshot format) enum png png,jpg
  
  #    Screenshot quality. Only used for JPEG format.
  #    1 means worst quality; 100 means best quality.
@@@ -961,6 -960,9 +972,9 @@@ screenshot_quality (Screenshot quality
  #    Adjust dpi configuration to your screen (non X11/Android only) e.g. for 4k screens.
  screen_dpi (DPI) int 72 1
  
+ #    Adjust the detected display density, used for scaling UI elements.
+ display_density_factor (Display Density Scaling Factor) float 1
  #    Windows systems only: Start Minetest with the command line window in the background.
  #    Contains the same information as the file debug.txt (default name).
  enable_console (Enable console window) bool false
@@@ -985,8 -987,8 +999,8 @@@ mute_sound (Mute sound) bool fals
  
  [Client]
  
- #    Clickable weblinks (middle-click or ctrl-left-click) enabled in chat console output.
- clickable_chat_weblinks (Chat weblinks) bool false
+ #    Clickable weblinks (middle-click or Ctrl+left-click) enabled in chat console output.
+ clickable_chat_weblinks (Chat weblinks) bool true
  
  #    Optional override for chat weblink color.
  chat_weblink_color (Weblink color) string
@@@ -1044,6 -1046,12 +1058,12 @@@ client_unload_unused_data_timeout (Mapb
  #    Set to -1 for unlimited amount.
  client_mapblock_limit (Mapblock limit) int 7500
  
+ #    Whether to show technical names.
+ #    Affects mods and texture packs in the Content and Select Mods menus, as well as
+ #    setting names in All Settings.
+ #    Controlled by the checkbox in the "All settings" menu.
+ show_technical_names (Show technical names) bool false
  #    Whether to show the client debug info (has the same effect as hitting F5).
  show_debug (Show debug info) bool false
  
@@@ -1114,7 -1122,7 +1134,7 @@@ max_packets_per_iteration (Max. packet
  
  #    Compression level to use when sending mapblocks to the client.
  #    -1 - use default compression level
- #     0 - least compresson, fastest
+ #     0 - least compression, fastest
  #     9 - best compression, slowest
  map_compression_level_net (Map Compression Level for Network Transfer) int -1 -1 9
  
@@@ -1178,7 -1186,7 +1198,7 @@@ enable_mod_channels (Mod channels) boo
  #    If this is set, players will always (re)spawn at the given position.
  static_spawnpoint (Static spawnpoint) string
  
- #    If enabled, new players cannot join with an empty password.
+ #    If enabled, players cannot join without a password or change theirs to an empty password.
  disallow_empty_password (Disallow empty passwords) bool false
  
  #    If enabled, disable cheat prevention in multiplayer.
@@@ -1300,7 -1308,7 +1320,7 @@@ movement_gravity (Gravity) float 9.8
  deprecated_lua_api_handling (Deprecated Lua API handling) enum log none,log,error
  
  #    Number of extra blocks that can be loaded by /clearobjects at once.
- #    This is a trade-off between sqlite transaction overhead and
+ #    This is a trade-off between SQLite transaction overhead and
  #    memory consumption (4096=100MB, as a rule of thumb).
  max_clearobjects_extra_loaded_blocks (Max. clearobjects extra blocks) int 4096
  
  server_unload_unused_data_timeout (Unload unused server data) int 29
  
  #    Maximum number of statically stored objects in a block.
- max_objects_per_block (Maximum objects per block) int 64
+ max_objects_per_block (Maximum objects per block) int 256
  
  #    See https://www.sqlite.org/pragma.html#pragma_synchronous
  sqlite_synchronous (Synchronous SQLite) enum 2 0,1,2
  
  #    Compression level to use when saving mapblocks to disk.
  #    -1 - use default compression level
- #     0 - least compresson, fastest
+ #     0 - least compression, fastest
  #     9 - best compression, slowest
  map_compression_level_disk (Map Compression Level for Disk Storage) int -1 -1 9
  
@@@ -1423,8 -1431,8 +1443,8 @@@ instrument.abm (Active Block Modifiers
  #    Instrument the action function of Loading Block Modifiers on registration.
  instrument.lbm (Loading Block Modifiers) bool true
  
- #    Instrument chatcommands on registration.
- instrument.chatcommand (Chatcommands) bool true
+ #    Instrument chat commands on registration.
+ instrument.chatcommand (Chat commands) bool true
  
  #    Instrument global callback functions on registration.
  #    (anything you pass to a minetest.register_*() function)
@@@ -1460,7 -1468,8 +1480,8 @@@ language (Language) enum   ,be,bg,ca,cs
  #    -    action
  #    -    info
  #    -    verbose
- debug_log_level (Debug log level) enum action ,none,error,warning,action,info,verbose
+ #    -    trace
+ debug_log_level (Debug log level) enum action ,none,error,warning,action,info,verbose,trace
  
  #    If the file size of debug.txt exceeds the number of megabytes specified in
  #    this setting when it is opened, the file is moved to debug.txt.1,
  debug_log_size_max (Debug log file size threshold) int 50
  
  #    Minimal level of logging to be written to chat.
- chat_log_level (Chat log level) enum error ,none,error,warning,action,info,verbose
+ chat_log_level (Chat log level) enum error ,none,error,warning,action,info,verbose,trace
  
  #    Enable IPv6 support (for both client and server).
  #    Required for IPv6 connections to work at all.
@@@ -1514,11 -1523,11 +1535,11 @@@ max_block_generate_distance (Max block 
  #    Limit of map generation, in nodes, in all 6 directions from (0, 0, 0).
  #    Only mapchunks completely within the mapgen limit are generated.
  #    Value is stored per-world.
- mapgen_limit (Map generation limit) int 31000 0 31000
+ mapgen_limit (Map generation limit) int 31007 0 31007
  
  #    Global map generation attributes.
  #    In Mapgen v6 the 'decorations' flag controls all decorations except trees
- #    and junglegrass, in all other mapgens this flag controls all decorations.
+ #    and jungle grass, in all other mapgens this flag controls all decorations.
  mg_flags (Mapgen flags) flags caves,dungeons,light,decorations,biomes,ores caves,dungeons,light,decorations,biomes,ores,nocaves,nodungeons,nolight,nodecorations,nobiomes,noores
  
  [*Biome API temperature and humidity noise parameters]
@@@ -2266,114 -2275,3 +2287,114 @@@ contentdb_flag_blacklist (ContentDB Fla
  #    Maximum number of concurrent downloads. Downloads exceeding this limit will be queued.
  #    This should be lower than curl_parallel_limit.
  contentdb_max_concurrent_downloads (ContentDB Max Concurrent Downloads) int 3
- cheat_menu_font (MenuFont) enum FM_Mono FM_Standard,FM_Mono,FM_Fallback,FM_Simple,FM_SimpleMono,FM_MaxMode,FM_Unspecified
 +
 +[Cheat Menu]
 +
 +#   Font to use for cheat menu
++cheat_menu_font (MenuFont) enum FM_Mono FM_Standard,FM_Mono,FM_Fallback,FM_MaxMode,FM_Unspecified
 +
 +#   (RGB value)
 +cheat_menu_bg_color (Cell background color) v3f 255, 145, 88
 +
 +cheat_menu_bg_color_alpha (Cell background color alpha) int 192
 +
 +#   (RGB value)
 +cheat_menu_active_bg_color (Active cell background color) v3f 255, 87, 53
 +
 +cheat_menu_active_bg_color_alpha (Active cell background color alpha) int 192
 +
 +#   (RGB value)
 +cheat_menu_font_color (Font color) v3f 0, 0, 0
 +
 +cheat_menu_font_color_alpha (Font color alpha) int 255 
 +
 +#   (RGB value)
 +cheat_menu_selected_font_color (Selected font color) v3f 255, 252, 88
 +
 +cheat_menu_selected_font_color_alpha (Selected font color alpha) int 255
 +
 +cheat_menu_head_height (Head height) int 50
 +
 +cheat_menu_entry_height (Entry height) int 40
 +
 +cheat_menu_entry_width (Entry width) int 200
 +
 +[Cheats]
 +
 +fullbright (Fullbright) bool false
 +
 +xray (XRay) bool false
 +
 +xray_nodes (XRay Nodes) string default:stone,mcl_core:stone
 +
 +priv_bypass (PrivBypass) bool true
 +
 +fastdig (FastDig) bool false
 +
 +fastplace (FastPlace) bool false
 +
 +autodig (AutoDig) bool false
 +
 +autoplace (AutoPlace) bool false
 +
 +prevent_natural_damage (NoFallDamage) bool true
 +
 +freecam (Freecam) bool false
 +
 +no_hurt_cam (NoHurtCam) bool false
 +
 +hud_flags_bypass (HUDBypass) bool true
 +
 +antiknockback (AntiKnockback) bool false
 +
 +entity_speed (EntitySpeed) bool false
 +
 +jesus (Jesus) bool false
 +
 +instant_break (InstantBreak) bool false
 +
 +no_night (BrightNight) bool false
 +
 +coords (Coords) bool false
 +
 +point_liquids (PointLiquids) bool false
 +
 +spamclick (FastHit) bool false
 +
 +no_force_rotate (NoForceRotate) bool false
 +
 +no_slow (NoSlow) bool false
 +
 +cheat_hud (CheatHUD) bool true
 +
 +node_esp_nodes (NodeESP Nodes) string
 +
 +jetpack (JetPack) bool false
 +
 +autohit (AutoHit) bool false
 +
 +antislip (AntiSlip) bool false
 +
 +enable_entity_tracers (EntityTracers) bool false
 +
 +enable_entity_esp (EntityESP) bool false
 +
 +enable_player_tracers (PlayerTracers) bool false
 +
 +enable_player_esp (PlayerESP) bool false
 +
 +enable_node_esp (NodeESP) bool false
 +
 +enable_node_tracers (NodeTracers) bool false
 +
 +entity_esp_color (EntityESP Color) v3f 255, 255, 255
 +
 +player_esp_color (PlayerESP Color) v3f 0, 255, 0
 +
 +tool_range (Additional Tool Range) int 2
 +
 +reach (Reach) bool false
 +
 +airjump (AirJump) bool false
 +
 +spider (Spider) bool false
diff --combined doc/client_lua_api.txt
index 40e0327e4f93664244aab796b17749f39cdadbb7,d08cd9b5efeae6f84a52343e950ddc00286a1c6f..75e40945ff18517cf0f818e9de69ebb92eea7487
@@@ -1,4 -1,4 +1,4 @@@
- Minetest Lua Client Modding API Reference 5.5.0
+ Minetest Lua Client Modding API Reference 5.6.0
  ================================================
  * More information at <http://www.minetest.net/>
  * Developer Wiki: <http://dev.minetest.net/>
@@@ -86,16 -86,9 +86,16 @@@ Mod directory structur
      clientmods
      ├── modname
      │   ├── mod.conf
 +    |   ├── settingtypes.txt
      │   ├── init.lua
      └── another
  
 +### `settingtypes.txt`
 +
 +The format is documented in `builtin/settingtypes.txt`.
 +It is parsed by the main menu settings dialogue to list mod-specific
 +settings in the "Clientmods" category.
 +
  ### modname
  
  The location of this directory.
@@@ -219,7 -212,7 +219,7 @@@ For helper functions see "Vector helper
  ### pointed_thing
  * `{type="nothing"}`
  * `{type="node", under=pos, above=pos}`
 -* `{type="object", id=ObjectID}`
 +* `{type="object", ref=ClientObjectRef}`
  
  Flag Specifier Format
  ---------------------
@@@ -587,6 -580,7 +587,7 @@@ Spatial Vector
  * `vector.floor(v)`: returns a vector, each dimension rounded down
  * `vector.round(v)`: returns a vector, each dimension rounded to nearest int
  * `vector.apply(v, func)`: returns a vector
+ * `vector.combine(v, w, func)`: returns a vector
  * `vector.equals(v1, v2)`: returns a boolean
  
  For the following functions `x` can be either a vector or a number:
@@@ -677,14 -671,6 +678,14 @@@ Minetest namespace referenc
  ### Global callback registration functions
  Call these functions only at load time!
  
 +* `minetest.get_send_speed(speed)`
 +      * This function is called every time the player's speed is sent to server
 +      * The `speed` argument is the actual speed of the player
 +      * If you define it, you can return a modified `speed`. This speed will be
 +              sent to server instead.
 +* `minetest.open_enderchest()`
 +    * This function is called if the client uses the Keybind for it (by default "O")
 +    * You can override it
  * `minetest.register_globalstep(function(dtime))`
      * Called every client environment step, usually interval of 0.1s
  * `minetest.register_on_mods_loaded(function())`
      * Adds definition to minetest.registered_chatcommands
  * `minetest.unregister_chatcommand(name)`
      * Unregisters a chatcommands registered with register_chatcommand.
 +* `minetest.register_list_command(command, desc, setting)`
 +    * Registers a chatcommand `command` to manage a list that takes the args `del | add | list <param>`
 +    * The list is stored comma-seperated in `setting`
 +    * `desc` is the description
 +    * `add` adds something to the list
 +    * `del` del removes something from the list
 +    * `list` lists all items on the list
  * `minetest.register_on_chatcommand(function(command, params))`
      * Called always when a chatcommand is triggered, before `minetest.registered_chatcommands`
        is checked to see if that the command exists, but after the input is parsed.
      * Called when the local player open inventory
      * Newest functions are called first
      * If any function returns true, inventory doesn't open
 +* `minetest.register_on_recieve_physics_override(function(override))`
 +    * Called when recieving physics_override from server
 +    * Newest functions are called first
 +    * If any function returns true, the physics override does not change
 +* `minetest.register_on_play_sound(function(SimpleSoundSpec))`
 +    * Called when recieving a play sound command from server
 +    * Newest functions are called first
 +    * If any function returns true, the sound does not play
 +* `minetest.register_on_spawn_partice(function(particle definition))`
 +    * Called when recieving a spawn particle command from server
 +    * Newest functions are called first
 +    * If any function returns true, the particle does not spawn
 +* `minetest.register_on_object_add(function(obj))`
 +      * Called every time an object is added
 +* `minetest.register_on_object_properties_change(function(obj))`
 +    * Called every time the properties of an object are changed server-side
 +    * May modify the object's properties without the fear of infinite recursion
 +* `minetest.register_on_object_hp_change(function(obj))`
 +    * Called every time the hp of an object are changes server-side
 +
 +### Setting-related
 +* `minetest.settings`: Settings object containing all of the settings from the
 +  main config file (`minetest.conf`).
 +* `minetest.setting_get_pos(name)`: Loads a setting from the main settings and
 +  parses it as a position (in the format `(1,2,3)`). Returns a position or nil.
 +
  ### Sounds
  * `minetest.sound_play(spec, parameters)`: returns a handle
      * `spec` is a `SimpleSoundSpec`
      * Returns the time of day: `0` for midnight, `0.5` for midday
  
  ### Map
 +* `minetest.interact(action, pointed_thing)`
 +    * Sends an interaction to the server
 +    * `pointed_thing` is a pointed_thing
 +    * `action` is one of
 +        * "start_digging": Use to punch nodes / objects
 +        * "stop_digging": Use to abort digging a "start_digging" command
 +        * "digging_completed": Use to finish a "start_digging" command or dig a node instantly
 +        * "place": Use to rightclick nodes and objects
 +        * "use": Use to leftclick an item in air (pointed_thing.type is usually "nothing")
 +        * "activate": Same as "use", but rightclick
 +* `minetest.place_node(pos)`
 +    * Places the wielded node/item of the player at pos.
 +* `minetest.dig_node(pos)`
 +    * Instantly digs the node at pos. This may fuck up with anticheat.
  * `minetest.get_node_or_nil(pos)`
      * Returns the node at the given position as table in the format
        `{name="node_name", param1=0, param2=0}`, returns `nil`
      * `nodenames`: e.g. `{"ignore", "group:tree"}` or `"default:dirt"`
      * `search_center` is an optional boolean (default: `false`)
        If true `pos` is also checked for the nodes
 +* `minetest.find_nodes_near(pos, radius, nodenames, [search_center])`: returns a
 +  list of positions.
 +    * `radius`: using a maximum metric
 +    * `nodenames`: e.g. `{"ignore", "group:tree"}` or `"default:dirt"`
 +    * `search_center` is an optional boolean (default: `false`)
 +      If true `pos` is also checked for the nodes
 +* `minetest.find_nodes_near_under_air(pos, radius, nodenames, [search_center])`: returns a
 +  list of positions.
 +    * `radius`: using a maximum metric
 +    * `nodenames`: e.g. `{"ignore", "group:tree"}` or `"default:dirt"`
 +    * `search_center` is an optional boolean (default: `false`)
 +      If true `pos` is also checked for the nodes
 +    * Return value: Table with all node positions with a node air above
 +* `minetest.find_nodes_near_under_air_except(pos, radius, nodenames, [search_center])`: returns a
 +  list of positions.
 +    * `radius`: using a maximum metric
 +    * `nodenames`: e.g. `{"ignore", "group:tree"}` or `"default:dirt"`, specifies the nodes to be ignored
 +    * `search_center` is an optional boolean (default: `false`)
 +      If true `pos` is also checked for the nodes
 +    * Return value: Table with all node positions with a node air above
  * `minetest.find_nodes_in_area(pos1, pos2, nodenames, [grouped])`
      * `pos1` and `pos2` are the min and max positions of the area to search.
      * `nodenames`: e.g. `{"ignore", "group:tree"}` or `"default:dirt"`
      * `pos2`: end of the ray
      * `objects`: if false, only nodes will be returned. Default is `true`.
      * `liquids`: if false, liquid nodes won't be returned. Default is `false`.
 -
 +* `minetest.get_pointed_thing()` returns `PointedThing`
 +    * Returns the thing currently pointed by player
 +* `minetest.get_pointed_thing_position(pointed_thing, above)`
 +    * Returns the position of a `pointed_thing` or `nil` if the `pointed_thing`
 +      does not refer to a node or entity.
 +    * If the optional `above` parameter is true and the `pointed_thing` refers
 +      to a node, then it will return the `above` position of the `pointed_thing`.
 +* `minetest.find_path(pos1,pos2,searchdistance,max_jump,max_drop,algorithm)`
 +    * returns table containing path that can be walked on
 +    * returns a table of 3D points representing a path from `pos1` to `pos2` or
 +      `nil` on failure.
 +    * Reasons for failure:
 +        * No path exists at all
 +        * No path exists within `searchdistance` (see below)
 +        * Start or end pos is buried in land
 +    * `pos1`: start position
 +    * `pos2`: end position
 +    * `searchdistance`: maximum distance from the search positions to search in.
 +      In detail: Path must be completely inside a cuboid. The minimum
 +      `searchdistance` of 1 will confine search between `pos1` and `pos2`.
 +      Larger values will increase the size of this cuboid in all directions
 +    * `max_jump`: maximum height difference to consider walkable
 +    * `max_drop`: maximum height difference to consider droppable
 +    * `algorithm`: One of `"A*_noprefetch"` (default), `"A*"`, `"Dijkstra"`.
 +      Difference between `"A*"` and `"A*_noprefetch"` is that
 +      `"A*"` will pre-calculate the cost-data, the other will calculate it
 +      on-the-fly
  * `minetest.find_nodes_with_meta(pos1, pos2)`
      * Get a table of positions of nodes that have metadata within a region
        {pos1, pos2}.
      * get max available level for leveled node
  
  ### Player
 +* `minetest.send_damage(hp)`
 +    * Sends fall damage to server
  * `minetest.send_chat_message(message)`
      * Act as if `message` was typed by the player into the terminal.
  * `minetest.run_server_chatcommand(cmd, param)`
      * Alias for `minetest.send_chat_message("/" .. cmd .. " " .. param)`
  * `minetest.clear_out_chat_queue()`
      * Clears the out chat queue
 +* `minetest.drop_selected_item()`
 +    * Drops the selected item
  * `minetest.localplayer`
      * Reference to the LocalPlayer object. See [`LocalPlayer`](#localplayer) class reference for methods.
  
      * Convert between two privilege representations
  
  ### Client Environment
 +* `minetest.object_refs`
 +    * Map of object references, indexed by active object id
  * `minetest.get_player_names()`
      * Returns list of player names on server (nil if CSM_RF_READ_PLAYERINFO is enabled by server)
 +* `minetest.get_objects_inside_radius(pos, radius)`: returns a list of
 +  ClientObjectRefs.
 +    * `radius`: using an euclidean metric
 +* `minetest.get_nearby_objects(radius)`
 +    * alias for minetest.get_objects_inside_radius(minetest.localplayer:get_pos(), radius)
  * `minetest.disconnect()`
      * Disconnect from the server and exit to main menu.
      * Returns `false` if the client is already disconnecting otherwise returns `true`.
  * `minetest.send_respawn()`
      * Sends a respawn request to the server.
  
 +### HTTP Requests
 +
 +* `minetest.request_http_api()`:
 +    * returns `HTTPApiTable` containing http functions if the calling mod has
 +      been granted access by being listed in the `secure.http_mods` or
 +      `secure.trusted_mods` setting, otherwise returns `nil`.
 +    * The returned table contains the functions `fetch`, `fetch_async` and
 +      `fetch_async_get` described below.
 +    * Only works at init time and must be called from the mod's main scope
 +      (not from a function).
 +    * Function only exists if minetest server was built with cURL support.
 +    * **DO NOT ALLOW ANY OTHER MODS TO ACCESS THE RETURNED TABLE, STORE IT IN
 +      A LOCAL VARIABLE!**
 +* `HTTPApiTable.fetch(HTTPRequest req, callback)`
 +    * Performs given request asynchronously and calls callback upon completion
 +    * callback: `function(HTTPRequestResult res)`
 +    * Use this HTTP function if you are unsure, the others are for advanced use
 +* `HTTPApiTable.fetch_async(HTTPRequest req)`: returns handle
 +    * Performs given request asynchronously and returns handle for
 +      `HTTPApiTable.fetch_async_get`
 +* `HTTPApiTable.fetch_async_get(handle)`: returns HTTPRequestResult
 +    * Return response data for given asynchronous HTTP request
 +
 +### `HTTPRequest` definition
 +
 +Used by `HTTPApiTable.fetch` and `HTTPApiTable.fetch_async`.
 +
 +    {
 +        url = "http://example.org",
 +
 +        timeout = 10,
 +        -- Timeout for connection in seconds. Default is 3 seconds.
 +
 +        post_data = "Raw POST request data string" OR {field1 = "data1", field2 = "data2"},
 +        -- Optional, if specified a POST request with post_data is performed.
 +        -- Accepts both a string and a table. If a table is specified, encodes
 +        -- table as x-www-form-urlencoded key-value pairs.
 +        -- If post_data is not specified, a GET request is performed instead.
 +
 +        user_agent = "ExampleUserAgent",
 +        -- Optional, if specified replaces the default minetest user agent with
 +        -- given string
 +
 +        extra_headers = { "Accept-Language: en-us", "Accept-Charset: utf-8" },
 +        -- Optional, if specified adds additional headers to the HTTP request.
 +        -- You must make sure that the header strings follow HTTP specification
 +        -- ("Key: Value").
 +
 +        multipart = boolean
 +        -- Optional, if true performs a multipart HTTP request.
 +        -- Default is false.
 +    }
 +
 +### `HTTPRequestResult` definition
 +
 +Passed to `HTTPApiTable.fetch` callback. Returned by
 +`HTTPApiTable.fetch_async_get`.
 +
 +    {
 +        completed = true,
 +        -- If true, the request has finished (either succeeded, failed or timed
 +        -- out)
 +
 +        succeeded = true,
 +        -- If true, the request was successful
 +
 +        timeout = false,
 +        -- If true, the request timed out
 +
 +        code = 200,
 +        -- HTTP status code
 +
 +        data = "response"
 +    }
 +
  ### Storage API
  * `minetest.get_mod_storage()`:
      * returns reference to mod private `StorageRef`
  * `minetest.delete_particlespawner(id)`
      * Delete `ParticleSpawner` with `id` (return value from `minetest.add_particlespawner`)
  
 -### Misc.
 +### Misc
 +* `minetest.set_keypress(key, value)`
 +    * Act as if a key was pressed (value = true) / released (value = false)
 +    * The key must be an keymap_* setting
 +    * e.g. minetest.set_keypress("jump", true) will cause te player to jump until minetest.set_keypress("jump", false) is called or the player presses & releases the space bar himself
 +* `minetest.get_inventory(location)`
 +    * Returns the inventory at location
 +* `minetest.find_item(item)`
 +    * finds and an item in the inventory
 +    * returns index on success or nil if item is not found
 +* `minetest.switch_to_item(item)`
 +    * `item` is an Itemstring
 +    * searches to item in inventory, sets the wield index to it if found
 +    * returns true on success, false if item was not found
 +* `minetest.register_cheat(name, category, setting | function)`
 +    * Register an entry for the cheat menu
 +    * If the Category is nonexistant, it will be created
 +    * If the 3rd argument is a string it will be interpreted as a setting and toggled
 +        when the player selects the entry in the cheat menu
 +    * If the 3rd argument is a function it will be called
 +        when the player selects the entry in the cheat menu
  * `minetest.parse_json(string[, nullvalue])`: returns something
      * Convert a string containing JSON data into the Lua equivalent
      * `nullvalue`: returned in place of the JSON null; defaults to `nil`
      * returns the exact position on the surface of a pointed node
  * `minetest.global_exists(name)`
      * Checks if a global variable has been set, without triggering a warning.
 +* `minetest.make_screenshot()`
 +    * Triggers the MT makeScreenshot functionality
 +* `minetest.request_insecure_environment()`: returns an environment containing
 +  insecure functions if the calling mod has been listed as trusted in the
 +  `secure.trusted_mods` setting or security is disabled, otherwise returns
 +  `nil`.
 +    * Only works at init time and must be called from the mod's main scope
 +      (ie: the init.lua of the mod, not from another Lua file or within a function).
 +    * **DO NOT ALLOW ANY OTHER MODS TO ACCESS THE RETURNED ENVIRONMENT, STORE
 +      IT IN A LOCAL VARIABLE!**
  
  ### UI
  * `minetest.ui.minimap`
      * Reference to the camera object. See [`Camera`](#camera) class reference for methods.
  * `minetest.show_formspec(formname, formspec)` : returns true on success
        * Shows a formspec to the player
 +* `minetest.close_formspec(formname)`
 +    * `formname`: has to exactly match the one given in `show_formspec`, or the
 +      formspec will not close.
 +    * calling `show_formspec(formname, "")` is equal to this
 +      expression.
 +    * to close a formspec regardless of the formname, call
 +      `minetest.close_formspec("")`.
 +      **USE THIS ONLY WHEN ABSOLUTELY NECESSARY!**
  * `minetest.display_chat_message(message)` returns true on success
        * Shows a chat message to the current player.
  
@@@ -1244,28 -1013,14 +1245,28 @@@ Methods
  
  * `get_pos()`
      * returns current player current position
 +* `set_pos(pos)`
 +    * sets the position (anticheat may not like this)
 +* `get_yaw()`
 +    * returns the yaw (degrees)
 +* `set_yaw(yaw)`
 +    * sets the yaw (degrees)
 +* `get_pitch()`
 +    * returns the pitch (degrees)
 +* `set_pitch(pitch)`
 +    * sets the pitch (degrees)
  * `get_velocity()`
      * returns player speed vector
 +* `set_velocity(vel)`
 +    * sets player speed vector
  * `get_hp()`
      * returns player HP
  * `get_name()`
      * returns player name
  * `get_wield_index()`
 -    * returns the index of the wielded item
 +    * returns the index of the wielded item (starts at 1)
 +* `set_wield_index()`
 +    * sets the index (starts at 1)
  * `get_wielded_item()`
      * returns the itemstack the player is holding
  * `is_attached()`
      * returns true if player is in a liquid (This oscillates so that the player jumps a bit above the surface)
  * `is_in_liquid_stable()`
      * returns true if player is in a stable liquid (This is more stable and defines the maximum speed of the player)
- * `get_liquid_viscosity()`
-     * returns liquid viscosity (Gets the viscosity of liquid to calculate friction)
+ * `get_move_resistance()`
+     * returns move resistance of current node, the higher the slower the player moves
  * `is_climbing()`
      * returns true if player is climbing
  * `swimming_vertical()`
      }
  ```
  
 +* `set_physics_override(override_table)`
 +    * `override_table` is a table with the following fields:
 +        * `speed`: multiplier to default walking speed value (default: `1`)
 +        * `jump`: multiplier to default jump value (default: `1`)
 +        * `gravity`: multiplier to default gravity value (default: `1`)
 +        * `sneak`: whether player can sneak (default: `true`)
 +        * `sneak_glitch`: whether player can use the new move code replications
 +          of the old sneak side-effects: sneak ladders and 2 node sneak jump
 +          (default: `false`)
 +        * `new_move`: use new move/sneak code. When `false` the exact old code
 +          is used for the specific old sneak behaviour (default: `true`)
  * `get_override_pos()`
      * returns override position
  * `get_last_pos()`
      * change a value of a previously added HUD element
      * element `stat` values: `position`, `name`, `scale`, `text`, `number`, `item`, `dir`
      * Returns `true` on success, otherwise returns `nil`
 +* `get_object()`
 +    * Returns the ClientObjectRef for the player
  
  ### Settings
  An interface to read config files in the format of `minetest.conf`.
@@@ -1416,30 -1158,6 +1417,30 @@@ Can be obtained via `minetest.get_meta(
      * `fields`: key-value storage
      * `inventory`: `{list1 = {}, ...}}`
  
 +### ClientObjectRef
 +
 +Moving things in the game are generally these.
 +This is basically a reference to a C++ `GenericCAO`.
 +
 +#### Methods
 +
 +* `get_pos()`: returns `{x=num, y=num, z=num}`
 +* `get_velocity()`: returns the velocity, a vector
 +* `get_acceleration()`: returns the acceleration, a vector
 +* `get_rotation()`: returns the rotation, a vector (radians)
 +* `is_player()`: returns true if the object is a player
 +* `is_local_player()`: returns true if the object is the local player
 +* `get_attach()`: returns parent or nil if it isn't attached.
 +* `get_nametag()`: returns the nametag (deprecated, use get_properties().nametag instead)
 +* `get_item_textures()`: returns the textures (deprecated, use get_properties().textures instead)
 +* `get_max_hp()`: returns the maximum heath (deprecated, use get_properties().hp_max instead)
 +* `set_properties(object property table)`
 +* `get_properties()`: returns object property table
 +* `punch()`: punches the object
 +* `rightclick()`: rightclicks the object
 +* `remove()`: removes the object permanently
 +* `set_nametag_images(images)`: Provides a list of images to be drawn below the nametag
 +
  ### `Raycast`
  
  A raycast on the map. It works with selection boxes.
@@@ -1468,80 -1186,15 +1469,80 @@@ It can be created via `Raycast(pos1, po
  
  -----------------
  ### Definitions
 +* `minetest.inventorycube(img1, img2, img3)`
 +    * Returns a string for making an image of a cube (useful as an item image)
  * `minetest.get_node_def(nodename)`
        * Returns [node definition](#node-definition) table of `nodename`
  * `minetest.get_item_def(itemstring)`
        * Returns item definition table of `itemstring`
 +* `minetest.override_item(itemstring, redefinition)`
 +    * Overrides fields of an item registered with register_node/tool/craftitem.
 +    * Note: Item must already be defined by the server
 +    * Example: `minetest.override_item("default:mese",
 +      {light_source=minetest.LIGHT_MAX})`
 +    * Doesnt really work yet an causes strange bugs, I'm working to make is better
 +
 +
 +#### Tile definition
 +
 +* `"image.png"`
 +* `{name="image.png", animation={Tile Animation definition}}`
 +* `{name="image.png", backface_culling=bool, align_style="node"/"world"/"user", scale=int}`
 +    * backface culling enabled by default for most nodes
 +    * align style determines whether the texture will be rotated with the node
 +      or kept aligned with its surroundings. "user" means that client
 +      setting will be used, similar to `glasslike_framed_optional`.
 +      Note: supported by solid nodes and nodeboxes only.
 +    * scale is used to make texture span several (exactly `scale`) nodes,
 +      instead of just one, in each direction. Works for world-aligned
 +      textures only.
 +      Note that as the effect is applied on per-mapblock basis, `16` should
 +      be equally divisible by `scale` or you may get wrong results.
 +* `{name="image.png", color=ColorSpec}`
 +    * the texture's color will be multiplied with this color.
 +    * the tile's color overrides the owning node's color in all cases.
 +
 +##### Tile definition
 +
 +    {
 +        type = "vertical_frames",
 +
 +        aspect_w = 16,
 +        -- Width of a frame in pixels
 +
 +        aspect_h = 16,
 +        -- Height of a frame in pixels
 +
 +        length = 3.0,
 +        -- Full loop length
 +    }
 +
 +    {
 +        type = "sheet_2d",
 +
 +        frames_w = 5,
 +        -- Width in number of frames
 +
 +        frames_h = 3,
 +        -- Height in number of frames
 +
 +        frame_length = 0.5,
 +        -- Length of a single frame
 +    }
  
  #### Node Definition
  
  ```lua
        {
 +        tiles = {tile definition 1, def2, def3, def4, def5, def6},
 +        -- Textures of node; +Y, -Y, +X, -X, +Z, -Z
 +        overlay_tiles = {tile definition 1, def2, def3, def4, def5, def6},
 +        -- Same as `tiles`, but these textures are drawn on top of the base
 +        -- tiles. This is used to colorize only specific parts of the
 +        -- texture. If the texture name is an empty string, that overlay is not
 +        -- drawn
 +        special_tiles = {tile definition 1, Tile definition 2},
 +        -- Special textures of node; used rarely.
                has_on_construct = bool,        -- Whether the node has the on_construct callback defined
                has_on_destruct = bool,         -- Whether the node has the on_destruct callback defined
                has_after_destruct = bool,      -- Whether the node has the after_destruct callback defined
                liquid_type = <string>,         -- A string containing "none", "flowing", or "source" *May not exist*
                liquid_alternative_flowing = <string>, -- Alternative node for liquid *May not exist*
                liquid_alternative_source = <string>, -- Alternative node for liquid *May not exist*
-               liquid_viscosity = <number>,    -- How fast the liquid flows *May not exist*
+               liquid_viscosity = <number>,    -- How slow the liquid flows *May not exist*
                liquid_renewable = <boolean>,   -- Whether the liquid makes an infinite source *May not exist*
                liquid_range = <number>,        -- How far the liquid flows *May not exist*
                drowning = bool,                -- Whether the player will drown in the node
                },
                legacy_facedir_simple = bool,   -- Whether to use old facedir
                legacy_wallmounted = bool       -- Whether to use old wallmounted
+               move_resistance = <number>,     -- How slow players can move through the node *May not exist*
        }
  ```
  
@@@ -1680,8 -1334,6 +1682,8 @@@ The following functions provide escape 
        `minetest.get_color_escape_sequence(color) ..
         message ..
         minetest.get_color_escape_sequence("#ffffff")`
 +* `minetest.rainbow(message)`:
 +    * Rainbow colorizes the message.
  * `minetest.get_background_escape_sequence(color)`
      * `color` is a [ColorString](#colorstring)
      * The escape sequence sets the background of the whole text element to
@@@ -1865,48 -1517,3 +1867,48 @@@ Same as `image`, but does not accept a 
          texture = "image.png",
      --  ^ Uses texture (string)
      }
 +
 +### InventoryAction
 +A reference to a C++ InventoryAction. You can move, drop and craft items in all accessible inventories using InventoryActions.
 +
 +#### methods
 +
 +* `InventoryAction(type)`:
 +    * creates a new InventoryAction
 +    * type is on of "move", "drop", or "craft", else returns nil
 +    * indexing starts at 1
 +* `apply()`:
 +    * applies the InventoryAction (InventoryActions can be applied multible times)
 +* `from(inventorylocation, listname, stack)`
 +    * this is valid for move or drop actions
 +    * when `apply()` is called items are moved / dropped from `listname` `inventorylocation` in` at `stack`
 +* `to(inventorylocation, listname, stack)`
 +    * this is valid for move actions
 +    * when `apply()` is called items are moved to `listname` in`inventorylocation` at `stack`
 +* `craft(inventoryaction)`
 +    * this is valid for craft actions
 +    * when `apply()` is called a craft event for this inventory will be triggered
 +* `set_count(count)`
 +    * this is valid for all actions
 +    * it specifies how many items to drop / craft / move
 +    * `0` means move all items
 +    * default count: `0`
 +
 +#### example
 +    `local move_act = InventoryAction("move")
 +    move_act:from("current_player", "main", 1)
 +    move_act:to("current_player", "craft", 1)
 +    move_act:set_count(1)
 +    local craft_act = InventoryAction("craft")
 +    craft_act:craft("current_player")
 +    local drop_act = InventoryAction("drop")
 +    drop_act:from("current_player", "craft_result",10)
 +    move_act:apply()
 +    craft_act:apply()
 +    drop_act:apply()
 +    `
 +    * e.g. In first hotbar slot there are tree logs: Move one to craft field, then craft wood out of it and immediately drop it
 +
 +
 +
 +
diff --combined doc/lua_api.txt
index a9bcc170790d06a833664bba9f2e07b156259107,abfaf08aa3d22abcacc5b88f1ac00f252158fa95..8aea490cfcadf36e927c9ae4e3b8500db3d1351b
@@@ -78,7 -78,7 +78,7 @@@ The game directory can contain the foll
      * `disallowed_mapgen_settings= <comma-separated mapgen settings>`
        e.g. `disallowed_mapgen_settings = mgv5_spflags`
        These mapgen settings are hidden for this game in the world creation
-       dialog and game start menu.
+       dialog and game start menu. Add `seed` to hide the seed input field.
      * `disabled_settings = <comma-separated settings>`
        e.g. `disabled_settings = enable_damage, creative_mode`
        These settings are hidden for this game in the "Start game" tab
@@@ -113,8 -113,16 +113,16 @@@ If you want to specify multiple images 
  images named like `$identifier.$n.png`, with an ascending number $n starting
  with 1, and a random image will be chosen from the provided ones.
  
+ Menu music
+ -----------
  
+ Games can provide custom main menu music. They are put inside a `menu`
+ directory inside the game directory.
  
+ The music files are named `theme.ogg`.
+ If you want to specify multiple music files for one game, add additional
+ images named like `theme.$n.ogg`, with an ascending number $n starting
+ with 1 (max 10), and a random music file will be chosen from the provided ones.
  
  Mods
  ====
@@@ -628,6 -636,23 +636,23 @@@ Result is more like what you'd expect i
  color, meaning white surfaces get a lot of your new color while black parts
  don't change very much.
  
+ #### `[png:<base64>`
+ Embed a base64 encoded PNG image in the texture string.
+ You can produce a valid string for this by calling
+ `minetest.encode_base64(minetest.encode_png(tex))`,
+ refer to the documentation of these functions for details.
+ You can use this to send disposable images such as captchas
+ to individual clients, or render things that would be too
+ expensive to compose with `[combine:`.
+ IMPORTANT: Avoid sending large images this way.
+ This is not a replacement for asset files, do not use it to do anything
+ that you could instead achieve by just using a file.
+ In particular consider `minetest.dynamic_add_media` and test whether
+ using other texture modifiers could result in a shorter string than
+ embedding a whole image, this may vary by use case.
  Hardware coloring
  -----------------
  
@@@ -1001,7 -1026,7 +1026,7 @@@ The function of `param1` is determined 
  `param1` is reserved for the engine when `paramtype != "none"`.
  
  * `paramtype = "light"`
-     * The value stores light with and without sun in its upper and lower 4 bits
+     * The value stores light with and without sun in its lower and upper 4 bits
        respectively.
      * Required by a light source node to enable spreading its light.
      * Required by the following drawtypes as they determine their visual
@@@ -1519,15 -1544,12 +1544,12 @@@ Displays a minimap on the HUD
  Representations of simple things
  ================================
  
- Position/vector
- ---------------
-     {x=num, y=num, z=num}
+ Vector (ie. a position)
+ -----------------------
  
-     Note: it is highly recommended to construct a vector using the helper function:
-     vector.new(num, num, num)
+     vector.new(x, y, z)
  
For helper functions see [Spatial Vectors].
See [Spatial Vectors] for details.
  
  `pointed_thing`
  ---------------
@@@ -1548,8 -1570,7 +1570,7 @@@ Exact pointing location (currently onl
    from 1).
  * `pointed_thing.intersection_normal`: Unit vector, points outwards of the
    selected selection box. This specifies which face is pointed at.
-   Is a null vector `{x = 0, y = 0, z = 0}` when the pointer is inside the
-   selection box.
+   Is a null vector `vector.zero()` when the pointer is inside the selection box.
  
  
  
@@@ -1658,10 -1679,10 +1679,10 @@@ wear value. Syntax
  
  Examples:
  
- * `'default:apple'`: 1 apple
- * `'default:dirt 5'`: 5 dirt
- * `'default:pick_stone'`: a new stone pickaxe
- * `'default:pick_wood 1 21323'`: a wooden pickaxe, ca. 1/3 worn out
+ * `"default:apple"`: 1 apple
+ * `"default:dirt 5"`: 5 dirt
+ * `"default:pick_stone"`: a new stone pickaxe
+ * `"default:pick_wood 1 21323"`: a wooden pickaxe, ca. 1/3 worn out
  
  ### Table format
  
@@@ -1751,21 -1772,21 +1772,21 @@@ Groups in crafting recipe
  An example: Make meat soup from any meat, any water and any bowl:
  
      {
-         output = 'food:meat_soup_raw',
+         output = "food:meat_soup_raw",
          recipe = {
-             {'group:meat'},
-             {'group:water'},
-             {'group:bowl'},
+             {"group:meat"},
+             {"group:water"},
+             {"group:bowl"},
          },
-         -- preserve = {'group:bowl'}, -- Not implemented yet (TODO)
+         -- preserve = {"group:bowl"}, -- Not implemented yet (TODO)
      }
  
  Another example: Make red wool from white wool and red dye:
  
      {
-         type = 'shapeless',
-         output = 'wool:red',
-         recipe = {'wool:white', 'group:dye,basecolor_red'},
+         type = "shapeless",
+         output = "wool:red",
+         recipe = {"wool:white", "group:dye,basecolor_red"},
      }
  
  Special groups
@@@ -1804,7 -1825,7 +1825,7 @@@ to games
        - (14)                                           -- constant tolerance
    Negative damage values are discarded as no damage.
  * `falling_node`: if there is no walkable block under the node it will fall
- * `float`: the node will not fall through liquids
+ * `float`: the node will not fall through liquids (`liquidtype ~= "none"`)
  * `level`: Can be used to give an additional sense of progression in the game.
       * A larger level will cause e.g. a weapon of a lower level make much less
         damage, and get worn out much faster, or not be able to get drops
@@@ -1936,8 -1957,9 +1957,9 @@@ to implement this
  ### Uses (tools only)
  
  Determines how many uses the tool has when it is used for digging a node,
- of this group, of the maximum level. For lower leveled nodes, the use count
- is multiplied by `3^leveldiff`.
+ of this group, of the maximum level. The maximum supported number of
+ uses is 65535. The special number 0 is used for infinite uses.
+ For lower leveled nodes, the use count is multiplied by `3^leveldiff`.
  `leveldiff` is the difference of the tool's `maxlevel` `groupcaps` and the
  node's `level` group. The node cannot be dug if `leveldiff` is less than zero.
  
@@@ -2148,6 -2170,13 +2170,13 @@@ Some of the values in the key-value sto
  * `color`: A `ColorString`, which sets the stack's color.
  * `palette_index`: If the item has a palette, this is used to get the
    current color from the palette.
+ * `count_meta`: Replace the displayed count with any string.
+ * `count_alignment`: Set the alignment of the displayed count value. This is an
+   int value. The lowest 2 bits specify the alignment in x-direction, the 3rd and
+   4th bit specify the alignment in y-direction:
+   0 = default, 1 = left / up, 2 = middle, 3 = right / down
+   The default currently is the same as right/down.
+   Example: 6 = 2 + 1*4 = middle,up
  
  Example:
  
      meta:set_string("key", "value")
      print(dump(meta:to_table()))
  
+ Example manipulations of "description" and expected output behaviors:
+     print(ItemStack("default:pick_steel"):get_description()) --> Steel Pickaxe
+     print(ItemStack("foobar"):get_description()) --> Unknown Item
+     local stack = ItemStack("default:stone")
+     stack:get_meta():set_string("description", "Custom description\nAnother line")
+     print(stack:get_description()) --> Custom description\nAnother line
+     print(stack:get_short_description()) --> Custom description
+     stack:get_meta():set_string("short_description", "Short")
+     print(stack:get_description()) --> Custom description\nAnother line
+     print(stack:get_short_description()) --> Short
+     print(ItemStack("mod:item_with_no_desc"):get_description()) --> mod:item_with_no_desc
  
  
  
@@@ -2230,18 -2274,20 +2274,20 @@@ Example
  Version History
  ---------------
  
- * FORMSPEC VERSION 1:
+ * Formspec version 1 (pre-5.1.0):
    * (too much)
- * FORMSPEC VERSION 2:
+ * Formspec version 2 (5.1.0):
    * Forced real coordinates
    * background9[]: 9-slice scaling parameters
- * FORMSPEC VERSION 3:
+ * Formspec version 3 (5.2.0):
    * Formspec elements are drawn in the order of definition
    * bgcolor[]: use 3 parameters (bgcolor, formspec (now an enum), fbgcolor)
    * box[] and image[] elements enable clipping by default
    * new element: scroll_container[]
- * FORMSPEC VERSION 4:
+ * Formspec version 4 (5.4.0):
    * Allow dropdown indexing events
+ * Formspec version 5 (5.5.0):
+   * Added padding[] element
  
  Elements
  --------
  * `position` and `anchor` elements need suitable values to avoid a formspec
    extending off the game window due to particular game window sizes.
  
- ### `no_prepend[]`
+ ### `padding[<X>,<Y>]`
  
  * Must be used after the `size`, `position`, and `anchor` elements (if present).
+ * Defines how much space is padded around the formspec if the formspec tries to
+   increase past the size of the screen and coordinates have to be shrunk.
+ * For X and Y, 0.0 represents no padding (the formspec can touch the edge of the
+   screen), and 0.5 represents half the screen (which forces the coordinate size
+   to 0). If negative, the formspec can extend off the edge of the screen.
+ * Defaults to [0.05, 0.05].
+ ### `no_prepend[]`
+ * Must be used after the `size`, `position`, `anchor`, and `padding` elements
+   (if present).
  * Disables player:set_formspec_prepend() from applying to this formspec.
  
  ### `real_coordinates[<bool>]`
  * End of a scroll_container, following elements are no longer bound to this
    container.
  
- ### `list[<inventory location>;<list name>;<X>,<Y>;<W>,<H>;]`
+ ### `list[<inventory location>;<list name>;<X>,<Y>;<W>,<H>;<starting item index>]`
  
- * Show an inventory list if it has been sent to the client. Nothing will
-   be shown if the inventory list is of size 0.
+ * Show an inventory list if it has been sent to the client.
+ * If the inventory list changes (eg. it didn't exist before, it's resized, or its items
+   are moved) while the formspec is open, the formspec element may (but is not guaranteed
+   to) adapt to the new inventory list.
+ * Item slots are drawn in a grid from left to right, then up to down, ordered
+   according to the slot index.
+ * `W` and `H` are in inventory slots, not in coordinates.
+ * `starting item index` (Optional): The index of the first (upper-left) item to draw.
+   Indices start at `0`. Default is `0`.
+ * The number of shown slots is the minimum of `W*H` and the inventory list's size minus
+   `starting item index`.
  * **Note**: With the new coordinate system, the spacing between inventory
    slots is one-fourth the size of an inventory slot by default. Also see
    [Styling Formspecs] for changing the size of slots and spacing.
  
- ### `list[<inventory location>;<list name>;<X>,<Y>;<W>,<H>;<starting item index>]`
- * Show an inventory list if it has been sent to the client. Nothing will
-   be shown if the inventory list is of size 0.
- * **Note**: With the new coordinate system, the spacing between inventory
-   slots is one-fourth the size of an inventory slot.
  ### `listring[<inventory location>;<list name>]`
  
  * Allows to create a ring of inventory lists
@@@ -3189,7 -3248,7 +3248,7 @@@ Color
  `#RRGGBBAA` defines a color in hexadecimal format and alpha channel.
  
  Named colors are also supported and are equivalent to
- [CSS Color Module Level 4](http://dev.w3.org/csswg/css-color/#named-colors).
+ [CSS Color Module Level 4](https://www.w3.org/TR/css-color-4/#named-color).
  To specify the value of the alpha channel, append `#A` or `#AA` to the end of
  the color name (e.g. `colorname#08`).
  
@@@ -3224,8 -3283,6 +3283,8 @@@ The following functions provide escape 
        `minetest.get_color_escape_sequence(color) ..
        message ..
        minetest.get_color_escape_sequence("#ffffff")`
 +* `minetest.rainbow(message)`:
 +    * Rainbow colorizes the message.
  * `minetest.get_background_escape_sequence(color)`
      * `color` is a ColorString
      * The escape sequence sets the background of the whole text element to
  
  Spatial Vectors
  ===============
- A spatial vector is similar to a position, but instead using
- absolute world coordinates, it uses *relative* coordinates, relative to
- no particular point.
- Internally, it is implemented as a table with the 3 fields
- `x`, `y` and `z`. Example: `{x = 0, y = 1, z = 0}`.
- However, one should *never* create a vector manually as above, such misbehavior
- is deprecated. The vector helpers set a metatable for the created vectors which
- allows indexing with numbers, calling functions directly on vectors and using
- operators (like `+`). Furthermore, the internal implementation might change in
+ Minetest stores 3-dimensional spatial vectors in Lua as tables of 3 coordinates,
+ and has a class to represent them (`vector.*`), which this chapter is about.
+ For details on what a spatial vectors is, please refer to Wikipedia:
+ https://en.wikipedia.org/wiki/Euclidean_vector.
+ Spatial vectors are used for various things, including, but not limited to:
+ * any 3D spatial vector (x/y/z-directions)
+ * Euler angles (pitch/yaw/roll in radians) (Spatial vectors have no real semantic
+   meaning here. Therefore, most vector operations make no sense in this use case.)
+ Note that they are *not* used for:
+ * n-dimensional vectors where n is not 3 (ie. n=2)
+ * arrays of the form `{num, num, num}`
+ The API documentation may refer to spatial vectors, as produced by `vector.new`,
+ by any of the following notations:
+ * `(x, y, z)` (Used rarely, and only if it's clear that it's a vector.)
+ * `vector.new(x, y, z)`
+ * `{x=num, y=num, z=num}` (Even here you are still supposed to use `vector.new`.)
+ Compatibility notes
+ -------------------
+ Vectors used to be defined as tables of the form `{x = num, y = num, z = num}`.
+ Since Minetest 5.5.0, vectors additionally have a metatable to enable easier use.
+ Note: Those old-style vectors can still be found in old mod code. Hence, mod and
+ engine APIs still need to be able to cope with them in many places.
+ Manually constructed tables are deprecated and highly discouraged. This interface
+ should be used to ensure seamless compatibility between mods and the Minetest API.
+ This is especially important to callback function parameters and functions overwritten
+ by mods.
+ Also, though not likely, the internal implementation of a vector might change in
  the future.
- Old code might still use vectors without metatables, be aware of this!
+ In your own code, or if you define your own API, you can, of course, still use
+ other representations of vectors.
+ Vectors provided by API functions will provide an instance of this class if not
+ stated otherwise. Mods should adapt this for convenience reasons.
+ Special properties of the class
+ -------------------------------
+ Vectors can be indexed with numbers and allow method and operator syntax.
  
  All these forms of addressing a vector `v` are valid:
  `v[1]`, `v[3]`, `v.x`, `v[1] = 42`, `v.y = 13`
+ Note: Prefer letter over number indexing for performance and compatibility reasons.
  
  Where `v` is a vector and `foo` stands for any function name, `v:foo(...)` does
  the same as `vector.foo(v, ...)`, apart from deprecated functionality.
  
+ `tostring` is defined for vectors, see `vector.to_string`.
  The metatable that is used for vectors can be accessed via `vector.metatable`.
  Do not modify it!
  
  All `vector.*` functions allow vectors `{x = X, y = Y, z = Z}` without metatables.
  Returned vectors always have a metatable set.
  
- For the following functions, `v`, `v1`, `v2` are vectors,
- `p1`, `p2` are positions,
+ Common functions and methods
+ ----------------------------
+ For the following functions (and subchapters),
+ `v`, `v1`, `v2` are vectors,
+ `p1`, `p2` are position vectors,
  `s` is a scalar (a number),
  vectors are written like this: `(x, y, z)`:
  
      * `init`: If given starts looking for the vector at this string index.
  * `vector.to_string(v)`:
      * Returns a string of the form `"(x, y, z)"`.
+     *  `tostring(v)` does the same.
  * `vector.direction(p1, p2)`:
      * Returns a vector of length 1 with direction `p1` to `p2`.
      * If `p1` and `p2` are identical, returns `(0, 0, 0)`.
  * `vector.apply(v, func)`:
      * Returns a vector where the function `func` has been applied to each
        component.
+ * `vector.combine(v, w, func)`:
+       * Returns a vector where the function `func` has combined both components of `v` and `w`
+         for each component
  * `vector.equals(v1, v2)`:
      * Returns a boolean, `true` if the vectors are identical.
  * `vector.sort(v1, v2)`:
      * Returns the cross product of `v1` and `v2`.
  * `vector.offset(v, x, y, z)`:
      * Returns the sum of the vectors `v` and `(x, y, z)`.
- * `vector.check()`:
+ * `vector.check(v)`:
      * Returns a boolean value indicating whether `v` is a real vector, eg. created
        by a `vector.*` function.
      * Returns `false` for anything else, including tables like `{x=3,y=1,z=4}`.
@@@ -3342,6 -3446,9 +3448,9 @@@ For the following functions `x` can be 
      * Returns a scaled vector.
      * Deprecated: If `s` is a vector: Returns the Schur quotient.
  
+ Operators
+ ---------
  Operators can be used if all of the involved vectors have metatables:
  * `v1 == v2`:
      * Returns whether `v1` and `v2` are identical.
  * `v / s`:
      * Returns `v` scaled by `1 / s`.
  
+ Rotation-related functions
+ --------------------------
  For the following functions `a` is an angle in radians and `r` is a rotation
- vector ({x = <pitch>, y = <yaw>, z = <roll>}) where pitch, yaw and roll are
+ vector (`{x = <pitch>, y = <yaw>, z = <roll>}`) where pitch, yaw and roll are
  angles in radians.
  
  * `vector.rotate(v, r)`:
      * If `up` is omitted, the roll of the returned vector defaults to zero.
      * Otherwise `direction` and `up` need to be vectors in a 90 degree angle to each other.
  
+ Further helpers
+ ---------------
+ There are more helper functions involving vectors, but they are listed elsewhere
+ because they only work on specific sorts of vectors or involve things that are not
+ vectors.
+ For example:
+ * `minetest.hash_node_position` (Only works on node positions.)
+ * `minetest.dir_to_wallmounted` (Involves wallmounted param2 values.)
  
  
  
@@@ -3460,8 -3582,8 +3584,8 @@@ Helper function
  * `minetest.pointed_thing_to_face_pos(placer, pointed_thing)`: returns a
    position.
      * returns the exact position on the surface of a pointed node
- * `minetest.get_dig_params(groups, tool_capabilities)`: Simulates an item
-     that digs a node.
+ * `minetest.get_dig_params(groups, tool_capabilities [, wear])`:
+     Simulates an item that digs a node.
      Returns a table with the following fields:
      * `diggable`: `true` if node can be dug, `false` otherwise.
      * `time`: Time it would take to dig the node.
      Parameters:
      * `groups`: Table of the node groups of the node that would be dug
      * `tool_capabilities`: Tool capabilities table of the item
- * `minetest.get_hit_params(groups, tool_capabilities [, time_from_last_punch])`:
+     * `wear`: Amount of wear the tool starts with (default: 0)
+ * `minetest.get_hit_params(groups, tool_capabilities [, time_from_last_punch [, wear]])`:
      Simulates an item that punches an object.
      Returns a table with the following fields:
-     * `hp`: How much damage the punch would cause.
+     * `hp`: How much damage the punch would cause (between -65535 and 65535).
      * `wear`: How much wear would be added to the tool (ignored for non-tools).
      Parameters:
      * `groups`: Damage groups of the object
      * `tool_capabilities`: Tool capabilities table of the item
      * `time_from_last_punch`: time in seconds since last punch action
+     * `wear`: Amount of wear the item starts with (default: 0)
  
  
  
@@@ -4149,7 -4273,7 +4275,7 @@@ differences
  
  ### Other API functions operating on a VoxelManip
  
- If any VoxelManip contents were set to a liquid node,
+ If any VoxelManip contents were set to a liquid node (`liquidtype ~= "none"`),
  `VoxelManip:update_liquids()` must be called for these liquid nodes to begin
  flowing. It is recommended to call this function only after having written all
  buffered data back to the VoxelManip object, save for special situations where
@@@ -4571,6 -4695,8 +4697,8 @@@ Utilitie
            abm_min_max_y = true,
            -- dynamic_add_media supports passing a table with options (5.5.0)
            dynamic_add_media_table = true,
+           -- allows get_sky to return a table instead of separate values (5.6.0)
+           get_sky_as_table = true,
        }
  
  * `minetest.has_feature(arg)`: returns `boolean, missing_features`
  * `minetest.mkdir(path)`: returns success.
      * Creates a directory specified by `path`, creating parent directories
        if they don't exist.
+ * `minetest.rmdir(path, recursive)`: returns success.
+     * Removes a directory specified by `path`.
+     * If `recursive` is set to `true`, the directory is recursively removed.
+       Otherwise, the directory will only be removed if it is empty.
+     * Returns true on success, false on failure.
+ * `minetest.cpdir(source, destination)`: returns success.
+     * Copies a directory specified by `path` to `destination`
+     * Any files in `destination` will be overwritten if they already exist.
+     * Returns true on success, false on failure.
+ * `minetest.mvdir(source, destination)`: returns success.
+     * Moves a directory specified by `path` to `destination`.
+     * If the `destination` is a non-empty directory, then the move will fail.
+     * Returns true on success, false on failure.
  * `minetest.get_dir_list(path, [is_dir])`: returns list of entry names
      * is_dir is one of:
          * nil: return all entries,
@@@ -4960,8 -5099,8 +5101,8 @@@ Call these functions only at load time
      * You should have joined some channels to receive events.
      * If message comes from a server mod, `sender` field is an empty string.
  * `minetest.register_on_liquid_transformed(function(pos_list, node_list))`
-     * Called after liquid nodes are modified by the engine's liquid transformation
-       process.
+     * Called after liquid nodes (`liquidtype ~= "none"`) are modified by the
+       engine's liquid transformation process.
      * `pos_list` is an array of all modified positions.
      * `node_list` is an array of the old node that was previously at the position
        with the corresponding index in pos_list.
@@@ -5303,7 -5442,8 +5444,8 @@@ Environment acces
      * `pos1`: start of the ray
      * `pos2`: end of the ray
      * `objects`: if false, only nodes will be returned. Default is `true`.
-     * `liquids`: if false, liquid nodes won't be returned. Default is `false`.
+     * `liquids`: if false, liquid nodes (`liquidtype ~= "none"`) won't be
+                  returned. Default is `false`.
  * `minetest.find_path(pos1,pos2,searchdistance,max_jump,max_drop,algorithm)`
      * returns table containing path that can be walked on
      * returns a table of 3D points representing a path from `pos1` to `pos2` or
  * `minetest.spawn_tree (pos, {treedef})`
      * spawns L-system tree at given `pos` with definition in `treedef` table
  * `minetest.transforming_liquid_add(pos)`
-     * add node to liquid update queue
+     * add node to liquid flow update queue
  * `minetest.get_node_max_level(pos)`
      * get max available level for leveled node
  * `minetest.get_node_level(pos)`
@@@ -5405,7 -5545,7 +5547,7 @@@ Inventor
  * `minetest.remove_detached_inventory(name)`
      * Returns a `boolean` indicating whether the removal succeeded.
  * `minetest.do_item_eat(hp_change, replace_with_item, itemstack, user, pointed_thing)`:
-   returns left over ItemStack.
+   returns leftover ItemStack or nil to indicate no inventory change
      * See `minetest.item_eat` and `minetest.register_on_item_eat`
  
  Formspec
@@@ -5630,6 -5770,68 +5772,68 @@@ Timin
  * `job:cancel()`
      * Cancels the job function from being called
  
+ Async environment
+ -----------------
+ The engine allows you to submit jobs to be ran in an isolated environment
+ concurrently with normal server operation.
+ A job consists of a function to be ran in the async environment, any amount of
+ arguments (will be serialized) and a callback that will be called with the return
+ value of the job function once it is finished.
+ The async environment does *not* have access to the map, entities, players or any
+ globals defined in the 'usual' environment. Consequently, functions like
+ `minetest.get_node()` or `minetest.get_player_by_name()` simply do not exist in it.
+ Arguments and return values passed through this can contain certain userdata
+ objects that will be seamlessly copied (not shared) to the async environment.
+ This allows you easy interoperability for delegating work to jobs.
+ * `minetest.handle_async(func, callback, ...)`:
+     * Queue the function `func` to be ran in an async environment.
+       Note that there are multiple persistent workers and any of them may
+       end up running a given job. The engine will scale the amount of
+       worker threads automatically.
+     * When `func` returns the callback is called (in the normal environment)
+       with all of the return values as arguments.
+     * Optional: Variable number of arguments that are passed to `func`
+ * `minetest.register_async_dofile(path)`:
+     * Register a path to a Lua file to be imported when an async environment
+       is initialized. You can use this to preload code which you can then call
+       later using `minetest.handle_async()`.
+ ### List of APIs available in an async environment
+ Classes:
+ * `ItemStack`
+ * `PerlinNoise`
+ * `PerlinNoiseMap`
+ * `PseudoRandom`
+ * `PcgRandom`
+ * `SecureRandom`
+ * `VoxelArea`
+ * `VoxelManip`
+     * only if transferred into environment; can't read/write to map
+ * `Settings`
+ Class instances that can be transferred between environments:
+ * `ItemStack`
+ * `PerlinNoise`
+ * `PerlinNoiseMap`
+ * `VoxelManip`
+ Functions:
+ * Standalone helpers such as logging, filesystem, encoding,
+   hashing or compression APIs
+ * `minetest.request_insecure_environment` (same restrictions apply)
+ Variables:
+ * `minetest.settings`
+ * `minetest.registered_items`, `registered_nodes`, `registered_tools`,
+   `registered_craftitems` and `registered_aliases`
+     * with all functions and userdata values replaced by `true`, calling any
+       callbacks here is obviously not possible
  Server
  ------
  
        a player joined.
      * This function may be overwritten by mods to customize the status message.
  * `minetest.get_server_uptime()`: returns the server uptime in seconds
+ * `minetest.get_server_max_lag()`: returns the current maximum lag
+   of the server in seconds or nil if server is not fully loaded yet
  * `minetest.remove_player(name)`: remove player from database (if they are not
    connected).
      * As auth data is not removed, minetest.player_exists will continue to
  * `minetest.kick_player(name, [reason])`: disconnect a player with an optional
    reason.
      * Returns boolean indicating success (false if player nonexistant)
+ * `minetest.disconnect_player(name, [reason])`: disconnect a player with an
+   optional reason, this will not prefix with 'Kicked: ' like kick_player.
+   If no reason is given, it will default to 'Disconnected.'
+     * Returns boolean indicating success (false if player nonexistant)
  
  Particles
  ---------
@@@ -5899,11 -6107,11 +6109,11 @@@ Misc
             This is due to the fact that JSON has two distinct array and object
             values.
      * Example: `write_json({10, {a = false}})`,
-       returns `"[10, {\"a\": false}]"`
+       returns `'[10, {"a": false}]'`
  * `minetest.serialize(table)`: returns a string
      * Convert a table containing tables, strings, numbers, booleans and `nil`s
        into string form readable by `minetest.deserialize`
-     * Example: `serialize({foo='bar'})`, returns `'return { ["foo"] = "bar" }'`
+     * Example: `serialize({foo="bar"})`, returns `'return { ["foo"] = "bar" }'`
  * `minetest.deserialize(string[, safe])`: returns a table
      * Convert a string returned by `minetest.serialize` into a table
      * `string` is loaded in an empty sandbox environment.
       value of `safe`. It is fine to serialize then deserialize user-provided
       data, but directly providing user input to deserialize is always unsafe.
      * Example: `deserialize('return { ["foo"] = "bar" }')`,
-       returns `{foo='bar'}`
+       returns `{foo="bar"}`
      * Example: `deserialize('print("foo")')`, returns `nil`
        (function call fails), returns
        `error:[string "print("foo")"]:1: attempt to call global 'print' (a nil value)`
@@@ -6128,45 -6336,53 +6338,53 @@@ Sorted alphabetically
  `AreaStore`
  -----------
  
- A fast access data structure to store areas, and find areas near a given
- position or area.
Every area has a `data` string attribute to store additional information.
- You can create an empty `AreaStore` by calling `AreaStore()`, or
- `AreaStore(type_name)`. The mod decides where to save and load AreaStore.
- If you chose the parameter-less constructor, a fast implementation will be
- automatically chosen for you.
+ AreaStore is a data structure to calculate intersections of 3D cuboid volumes
+ and points. The `data` field (string) may be used to store and retrieve any
mod-relevant information to the specified area.
+ Despite its name, mods must take care of persisting AreaStore data. They may
+ use the provided load and write functions for this.
  
  ### Methods
  
- * `get_area(id, include_borders, include_data)`
+ * `AreaStore(type_name)`
+     * Returns a new AreaStore instance
+     * `type_name`: optional, forces the internally used API.
+         * Possible values: `"LibSpatial"` (default).
+         * When other values are specified, or SpatialIndex is not available,
+           the custom Minetest functions are used.
+ * `get_area(id, include_corners, include_data)`
      * Returns the area information about the specified ID.
      * Returned values are either of these:
  
              nil  -- Area not found
-             true -- Without `include_borders` and `include_data`
+             true -- Without `include_corners` and `include_data`
              {
-                 min = pos, max = pos -- `include_borders == true`
+                 min = pos, max = pos -- `include_corners == true`
                  data = string        -- `include_data == true`
              }
  
- * `get_areas_for_pos(pos, include_borders, include_data)`
+ * `get_areas_for_pos(pos, include_corners, include_data)`
      * Returns all areas as table, indexed by the area ID.
      * Table values: see `get_area`.
- * `get_areas_in_area(edge1, edge2, accept_overlap, include_borders, include_data)`
-     * Returns all areas that contain all nodes inside the area specified by `edge1`
-       and `edge2` (inclusive).
+ * `get_areas_in_area(corner1, corner2, accept_overlap, include_corners, include_data)`
+     * Returns all areas that contain all nodes inside the area specified by`
+       `corner1 and `corner2` (inclusive).
      * `accept_overlap`: if `true`, areas are returned that have nodes in
        common (intersect) with the specified area.
      * Returns the same values as `get_areas_for_pos`.
- * `insert_area(edge1, edge2, data, [id])`: inserts an area into the store.
+ * `insert_area(corner1, corner2, data, [id])`: inserts an area into the store.
      * Returns the new area's ID, or nil if the insertion failed.
-     * The (inclusive) positions `edge1` and `edge2` describe the area.
+     * The (inclusive) positions `corner1` and `corner2` describe the area.
      * `data` is a string stored with the area.
      * `id` (optional): will be used as the internal area ID if it is an unique
        number between 0 and 2^32-2.
- * `reserve(count)`: reserves resources for at most `count` many contained
-   areas.
-   Only needed for efficiency, and only some implementations profit.
+ * `reserve(count)`
+     * Requires SpatialIndex, no-op function otherwise.
+     * Reserves resources for `count` many contained areas to improve
+       efficiency when working with many area entries. Additional areas can still
+       be inserted afterwards at the usual complexity.
  * `remove_area(id)`: removes the area with the given id from the store, returns
    success.
  * `set_cache_params(params)`: sets params for the included prefiltering cache.
@@@ -6204,9 -6420,9 +6422,9 @@@ An `InvRef` is a reference to an invent
  * `set_width(listname, width)`: set width of list; currently used for crafting
  * `get_stack(listname, i)`: get a copy of stack index `i` in list
  * `set_stack(listname, i, stack)`: copy `stack` to index `i` in list
- * `get_list(listname)`: return full list
+ * `get_list(listname)`: return full list (list of `ItemStack`s)
  * `set_list(listname, list)`: set full list (size will not change)
- * `get_lists()`: returns list of inventory lists
+ * `get_lists()`: returns table that maps listnames to inventory lists
  * `set_lists(lists)`: sets inventory lists (size will not change)
  * `add_item(listname, stack)`: add item somewhere in list, returns leftover
    `ItemStack`.
@@@ -6566,7 -6782,7 +6784,7 @@@ object you are working with still exist
          * Fourth column: subject looking to the right
          * Fifth column:  subject viewed from above
          * Sixth column:  subject viewed from below
- * `get_entity_name()` (**Deprecated**: Will be removed in a future version)
+ * `get_entity_name()` (**Deprecated**: Will be removed in a future version, use the field `self.name` instead)
  * `get_luaentity()`
  
  #### Player only (no-op for other objects)
  * `set_inventory_formspec(formspec)`
      * Redefine player's inventory form
      * Should usually be called in `on_joinplayer`
+     * If `formspec` is `""`, the player's inventory is disabled.
  * `get_inventory_formspec()`: returns a formspec string
  * `set_formspec_prepend(formspec)`:
      * the formspec string will be added to every formspec shown to the user,
        `aux1`, `sneak`, `dig`, `place`, `LMB`, `RMB`, and `zoom`.
      * The fields `LMB` and `RMB` are equal to `dig` and `place` respectively,
        and exist only to preserve backwards compatibility.
+     * Returns an empty table `{}` if the object is not a player.
  * `get_player_control_bits()`: returns integer with bit packed player pressed
-   keys. Bits:
-     * 0 - up
-     * 1 - down
-     * 2 - left
-     * 3 - right
-     * 4 - jump
-     * 5 - aux1
-     * 6 - sneak
-     * 7 - dig
-     * 8 - place
-     * 9 - zoom
+   keys.
+     * Bits:
+         * 0 - up
+         * 1 - down
+         * 2 - left
+         * 3 - right
+         * 4 - jump
+         * 5 - aux1
+         * 6 - sneak
+         * 7 - dig
+         * 8 - place
+         * 9 - zoom
+     * Returns `0` (no bits set) if the object is not a player.
  * `set_physics_override(override_table)`
      * `override_table` is a table with the following fields:
          * `speed`: multiplier to default walking speed value (default: `1`)
  * `hud_get(id)`: gets the HUD element definition structure of the specified ID
  * `hud_set_flags(flags)`: sets specified HUD flags of player.
      * `flags`: A table with the following fields set to boolean values
-         * hotbar
-         * healthbar
-         * crosshair
-         * wielditem
-         * breathbar
-         * minimap
-         * minimap_radar
+         * `hotbar`
+         * `healthbar`
+         * `crosshair`
+         * `wielditem`
+         * `breathbar`
+         * `minimap`: Modifies the client's permission to view the minimap.
+           The client may locally elect to not view the minimap.
+         * `minimap_radar`: is only usable when `minimap` is true
+         * `basic_debug`: Allow showing basic debug info that might give a gameplay advantage.
+           This includes map seed, player position, look direction, the pointed node and block bounds.
+           Does not affect players with the `debug` privilege.
      * If a flag equals `nil`, the flag is not modified
-     * `minimap`: Modifies the client's permission to view the minimap.
-       The client may locally elect to not view the minimap.
-     * `minimap_radar` is only usable when `minimap` is true
  * `hud_get_flags()`: returns a table of player HUD flags with boolean values.
      * See `hud_set_flags` for a list of flags that can be toggled.
  * `hud_set_hotbar_itemcount(count)`: sets number of items in builtin hotbar
  * `set_sky(sky_parameters)`
      * The presence of the function `set_sun`, `set_moon` or `set_stars` indicates
        whether `set_sky` accepts this format. Check the legacy format otherwise.
+     * Passing no arguments resets the sky to its default values.
      * `sky_parameters` is a table with the following optional fields:
          * `base_color`: ColorSpec, changes fog in "skybox" and "plain".
+           (default: `#ffffff`)
          * `type`: Available types:
              * `"regular"`: Uses 0 textures, `base_color` ignored
              * `"skybox"`: Uses 6 textures, `base_color` used as fog.
              * `"plain"`: Uses 0 textures, `base_color` used as both fog and sky.
+             (default: `"regular"`)
          * `textures`: A table containing up to six textures in the following
              order: Y+ (top), Y- (bottom), X- (west), X+ (east), Z+ (north), Z- (south).
          * `clouds`: Boolean for whether clouds appear. (default: `true`)
-         * `sky_color`: A table containing the following values, alpha is ignored:
-             * `day_sky`: ColorSpec, for the top half of the `"regular"`
-               sky during the day. (default: `#61b5f5`)
-             * `day_horizon`: ColorSpec, for the bottom half of the
-               `"regular"` sky during the day. (default: `#90d3f6`)
-             * `dawn_sky`: ColorSpec, for the top half of the `"regular"`
-               sky during dawn/sunset. (default: `#b4bafa`)
+         * `sky_color`: A table used in `"regular"` type only, containing the
+           following values (alpha is ignored):
+             * `day_sky`: ColorSpec, for the top half of the sky during the day.
+               (default: `#61b5f5`)
+             * `day_horizon`: ColorSpec, for the bottom half of the sky during the day.
+               (default: `#90d3f6`)
+             * `dawn_sky`: ColorSpec, for the top half of the sky during dawn/sunset.
+               (default: `#b4bafa`)
                The resulting sky color will be a darkened version of the ColorSpec.
                Warning: The darkening of the ColorSpec is subject to change.
-             * `dawn_horizon`: ColorSpec, for the bottom half of the `"regular"`
-               sky during dawn/sunset. (default: `#bac1f0`)
+             * `dawn_horizon`: ColorSpec, for the bottom half of the sky during dawn/sunset.
+               (default: `#bac1f0`)
                The resulting sky color will be a darkened version of the ColorSpec.
                Warning: The darkening of the ColorSpec is subject to change.
-             * `night_sky`: ColorSpec, for the top half of the `"regular"`
-               sky during the night. (default: `#006bff`)
+             * `night_sky`: ColorSpec, for the top half of the sky during the night.
+               (default: `#006bff`)
                The resulting sky color will be a dark version of the ColorSpec.
                Warning: The darkening of the ColorSpec is subject to change.
-             * `night_horizon`: ColorSpec, for the bottom half of the `"regular"`
-               sky during the night. (default: `#4090ff`)
+             * `night_horizon`: ColorSpec, for the bottom half of the sky during the night.
+               (default: `#4090ff`)
                The resulting sky color will be a dark version of the ColorSpec.
                Warning: The darkening of the ColorSpec is subject to change.
-             * `indoors`: ColorSpec, for when you're either indoors or
-               underground. Only applies to the `"regular"` sky.
+             * `indoors`: ColorSpec, for when you're either indoors or underground.
                (default: `#646464`)
              * `fog_sun_tint`: ColorSpec, changes the fog tinting for the sun
-               at sunrise and sunset.
+               at sunrise and sunset. (default: `#f47d1d`)
              * `fog_moon_tint`: ColorSpec, changes the fog tinting for the moon
-               at sunrise and sunset.
+               at sunrise and sunset. (default: `#7f99cc`)
              * `fog_tint_type`: string, changes which mode the directional fog
                  abides by, `"custom"` uses `sun_tint` and `moon_tint`, while
                  `"default"` uses the classic Minetest sun and moon tinting.
          * `"plain"`: Uses 0 textures, `bgcolor` used
      * `clouds`: Boolean for whether clouds appear in front of `"skybox"` or
        `"plain"` custom skyboxes (default: `true`)
- * `get_sky()`: returns base_color, type, table of textures, clouds.
- * `get_sky_color()`: returns a table with the `sky_color` parameters as in
-     `set_sky`.
+ * `get_sky(as_table)`:
+     * `as_table`: boolean that determines whether the deprecated version of this
+     function is being used.
+         * `true` returns a table containing sky parameters as defined in `set_sky(sky_parameters)`.
+         * Deprecated: `false` or `nil` returns base_color, type, table of textures,
+         clouds.
+ * `get_sky_color()`:
+     * Deprecated: Use `get_sky(as_table)` instead.
+     * returns a table with the `sky_color` parameters as in `set_sky`.
  * `set_sun(sun_parameters)`:
+     * Passing no arguments resets the sun to its default values.
      * `sun_parameters` is a table with the following optional fields:
          * `visible`: Boolean for whether the sun is visible.
              (default: `true`)
          * `texture`: A regular texture for the sun. Setting to `""`
-             will re-enable the mesh sun. (default: `"sun.png"`)
+             will re-enable the mesh sun. (default: "sun.png", if it exists)
          * `tonemap`: A 512x1 texture containing the tonemap for the sun
              (default: `"sun_tonemap.png"`)
          * `sunrise`: A regular texture for the sunrise texture.
  * `get_sun()`: returns a table with the current sun parameters as in
      `set_sun`.
  * `set_moon(moon_parameters)`:
+     * Passing no arguments resets the moon to its default values.
      * `moon_parameters` is a table with the following optional fields:
          * `visible`: Boolean for whether the moon is visible.
              (default: `true`)
          * `texture`: A regular texture for the moon. Setting to `""`
-             will re-enable the mesh moon. (default: `"moon.png"`)
+             will re-enable the mesh moon. (default: `"moon.png"`, if it exists)
+             Note: Relative to the sun, the moon texture is rotated by 180°.
+             You can use the `^[transformR180` texture modifier to achieve the same orientation.
          * `tonemap`: A 512x1 texture containing the tonemap for the moon
              (default: `"moon_tonemap.png"`)
          * `scale`: Float controlling the overall size of the moon (default: `1`)
  * `get_moon()`: returns a table with the current moon parameters as in
      `set_moon`.
  * `set_stars(star_parameters)`:
+     * Passing no arguments resets stars to their default values.
      * `star_parameters` is a table with the following optional fields:
          * `visible`: Boolean for whether the stars are visible.
              (default: `true`)
  * `get_stars()`: returns a table with the current stars parameters as in
      `set_stars`.
  * `set_clouds(cloud_parameters)`: set cloud parameters
+     * Passing no arguments resets clouds to their default values.
      * `cloud_parameters` is a table with the following optional fields:
          * `density`: from `0` (no clouds) to `1` (full clouds) (default `0.4`)
          * `color`: basic cloud color with alpha channel, ColorSpec
      * Returns `false` if failed.
      * Resource intensive - use sparsely
      * To get blockpos, integer divide pos by 16
+ * `set_lighting(light_definition)`: sets lighting for the player
+     * `light_definition` is a table with the following optional fields:
+       * `shadows` is a table that controls ambient shadows
+         * `intensity` sets the intensity of the shadows from 0 (no shadows, default) to 1 (blackness)
+ * `get_lighting()`: returns the current state of lighting for the player.
+     * Result is a table with the same fields as `light_definition` in `set_lighting`.
  
  `PcgRandom`
  -----------
@@@ -6980,7 -7222,8 +7224,8 @@@ It can be created via `Raycast(pos1, po
  * `pos1`: start of the ray
  * `pos2`: end of the ray
  * `objects`: if false, only nodes will be returned. Default is true.
- * `liquids`: if false, liquid nodes won't be returned. Default is false.
+ * `liquids`: if false, liquid nodes (`liquidtype ~= "none"`) won't be
+              returned. Default is false.
  
  ### Methods
  
@@@ -7139,6 -7382,7 +7384,7 @@@ Player properties need to be saved manu
          -- "sprite" uses 1 texture.
          -- "upright_sprite" uses 2 textures: {front, back}.
          -- "wielditem" expects 'textures = {itemname}' (see 'visual' above).
+         -- "mesh" requires one texture for each mesh buffer/material (in order)
  
          colors = {},
          -- Number of required colors depends on visual
@@@ -7264,7 -7508,7 +7510,7 @@@ Used by `minetest.register_entity`
          -- for more info) by using a '_' prefix
      }
  
- Collision info passed to `on_step`:
+ Collision info passed to `on_step` (`moveresult` argument):
  
      {
          touching_ground = boolean,
              },
              ...
          }
+         -- `collisions` does not contain data of unloaded mapblock collisions
+         -- or when the velocity changes are negligibly small
      }
  
  ABM (ActiveBlockModifier) definition
@@@ -7464,6 -7710,8 +7712,8 @@@ Used by `minetest.register_node`, `mine
          range = 4.0,
  
          liquids_pointable = false,
+         -- If true, item points to all liquid nodes (`liquidtype ~= "none"`),
+         -- even those for which `pointable = false`
  
          light_source = 0,
          -- When used for nodes: Defines amount of light emitted by node.
          on_place = function(itemstack, placer, pointed_thing),
          -- When the 'place' key was pressed with the item in hand
          -- and a node was pointed at.
-         -- Shall place item and return the leftover itemstack.
+         -- Shall place item and return the leftover itemstack
+         -- or nil to not modify the inventory.
          -- The placer may be any ObjectRef or nil.
          -- default: minetest.item_place
  
          on_secondary_use = function(itemstack, user, pointed_thing),
          -- Same as on_place but called when not pointing at a node.
+         -- Function must return either nil if inventory shall not be modified,
+         -- or an itemstack to replace the original itemstack.
          -- The user may be any ObjectRef or nil.
          -- default: nil
  
          on_use = function(itemstack, user, pointed_thing),
          -- default: nil
          -- When user pressed the 'punch/mine' key with the item in hand.
-         -- Function must return either nil if no item shall be removed from
-         -- inventory, or an itemstack to replace the original itemstack.
+         -- Function must return either nil if inventory shall not be modified,
+         -- or an itemstack to replace the original itemstack.
          -- e.g. itemstack:take_item(); return itemstack
          -- Otherwise, the function is free to do what it wants.
          -- The user may be any ObjectRef or nil.
@@@ -7649,14 -7900,21 +7902,21 @@@ Used by `minetest.register_node`
  
          climbable = false,  -- If true, can be climbed on (ladder)
  
+         move_resistance = 0,
+         -- Slows down movement of players through this node (max. 7).
+         -- If this is nil, it will be equal to liquid_viscosity.
+         -- Note: If liquid movement physics apply to the node
+         -- (see `liquid_move_physics`), the movement speed will also be
+         -- affected by the `movement_liquid_*` settings.
          buildable_to = false,  -- If true, placed nodes can replace this node
  
          floodable = false,
          -- If true, liquids flow into and replace this node.
          -- Warning: making a liquid node 'floodable' will cause problems.
  
-         liquidtype = "none",  -- specifies liquid physics
-         -- * "none":    no liquid physics
+         liquidtype = "none",  -- specifies liquid flowing physics
+         -- * "none":    no liquid flowing physics
          -- * "source":  spawns flowing liquid nodes at all 4 sides and below;
          --              recommended drawtype: "liquid".
          -- * "flowing": spawned from source, spawns more flowing liquid nodes
  
          liquid_alternative_source = "",  -- Source version of flowing liquid
  
-         liquid_viscosity = 0,  -- Higher viscosity = slower flow (max. 7)
+         liquid_viscosity = 0,
+         -- Controls speed at which the liquid spreads/flows (max. 7).
+         -- 0 is fastest, 7 is slowest.
+         -- By default, this also slows down movement of players inside the node
+         -- (can be overridden using `move_resistance`)
  
          liquid_renewable = true,
          -- If true, a new liquid source can be created by placing two or more
          -- sources nearby
  
+         liquid_move_physics = nil, -- specifies movement physics if inside node
+         -- * false: No liquid movement physics apply.
+         -- * true: Enables liquid movement physics. Enables things like
+         --   ability to "swim" up/down, sinking slowly if not moving,
+         --   smoother speed change when falling into, etc. The `movement_liquid_*`
+         --   settings apply.
+         -- * nil: Will be treated as true if `liquidype ~= "none"`
+         --   and as false otherwise.
+         -- Default: nil
          leveled = 0,
          -- Only valid for "nodebox" drawtype with 'type = "leveled"'.
          -- Allows defining the nodebox height without using param2.
                      items = {"default:sand", "default:desert_sand"},
                  },
                  {
-                     -- Only drop if using an item in the "magicwand" group, or 
+                     -- Only drop if using an item in the "magicwand" group, or
                      -- an item that is in both the "pickaxe" and the "lucky"
                      -- groups.
                      tool_groups = {
@@@ -7967,11 -8239,11 +8241,11 @@@ Used by `minetest.register_craft`
  ### Shaped
  
      {
-         output = 'default:pick_stone',
+         output = "default:pick_stone",
          recipe = {
-             {'default:cobble', 'default:cobble', 'default:cobble'},
-             {'', 'default:stick', ''},
-             {'', 'default:stick', ''},  -- Also groups; e.g. 'group:crumbly'
+             {"default:cobble", "default:cobble", "default:cobble"},
+             {"", "default:stick", ""},
+             {"", "default:stick", ""},  -- Also groups; e.g. "group:crumbly"
          },
          replacements = <list of item pairs>,
          -- replacements: replace one input item with another item on crafting
  
      {
          type = "shapeless",
-         output = 'mushrooms:mushroom_stew',
+         output = "mushrooms:mushroom_stew",
          recipe = {
              "mushrooms:bowl",
              "mushrooms:mushroom_brown",
@@@ -8753,3 -9025,10 +9027,10 @@@ Used by `minetest.register_authenticati
          -- Returns an iterator (use with `for` loops) for all player names
          -- currently in the auth database
      }
+ Bit Library
+ -----------
+ Functions: bit.tobit, bit.tohex, bit.bnot, bit.band, bit.bor, bit.bxor, bit.lshift, bit.rshift, bit.arshift, bit.rol, bit.ror, bit.bswap
+ See http://bitop.luajit.org/ for advanced information.
diff --combined src/client/camera.cpp
index 52bedcba93979291c00adf43eafaafb7a80cfccf,d1f19adb3ff08f71488cbd2fe4feccae438fa4dd..0c387262ebe039df92473875de58b4136a196e54
@@@ -26,6 -26,7 +26,7 @@@ with this program; if not, write to th
  #include "player.h"
  #include <cmath>
  #include "client/renderingengine.h"
+ #include "client/content_cao.h"
  #include "settings.h"
  #include "wieldmesh.h"
  #include "noise.h"         // easeCurve
@@@ -35,8 -36,8 +36,9 @@@
  #include "util/numeric.h"
  #include "constants.h"
  #include "fontengine.h"
 +#include "guiscalingfilter.h"
  #include "script/scripting_client.h"
+ #include "gettext.h"
  
  #define CAMERA_OFFSET_STEP 200
  #define WIELDMESH_OFFSET_X 55.0f
@@@ -133,28 -134,6 +135,6 @@@ void Camera::notifyFovChange(
        }
  }
  
- bool Camera::successfullyCreated(std::string &error_message)
- {
-       if (!m_playernode) {
-               error_message = "Failed to create the player scene node";
-       } else if (!m_headnode) {
-               error_message = "Failed to create the head scene node";
-       } else if (!m_cameranode) {
-               error_message = "Failed to create the camera scene node";
-       } else if (!m_wieldmgr) {
-               error_message = "Failed to create the wielded item scene manager";
-       } else if (!m_wieldnode) {
-               error_message = "Failed to create the wielded item scene node";
-       } else {
-               error_message.clear();
-       }
-       if (m_client->modsLoaded())
-               m_client->getScript()->on_camera_ready(this);
-       return error_message.empty();
- }
  // Returns the fractional part of x
  inline f32 my_modf(f32 x)
  {
@@@ -187,9 -166,7 +167,7 @@@ void Camera::step(f32 dtime
                                m_view_bobbing_anim -= offset;
                        } else if (m_view_bobbing_anim > 0.75) {
                                m_view_bobbing_anim += offset;
-                       }
-                       if (m_view_bobbing_anim < 0.5) {
+                       } else if (m_view_bobbing_anim < 0.5) {
                                m_view_bobbing_anim += offset;
                                if (m_view_bobbing_anim > 0.5)
                                        m_view_bobbing_anim = 0.5;
@@@ -331,7 -308,7 +309,7 @@@ void Camera::addArmInertia(f32 player_y
        }
  }
  
- void Camera::update(LocalPlayer* player, f32 frametime, f32 busytime, f32 tool_reload_ratio)
+ void Camera::update(LocalPlayer* player, f32 frametime, f32 tool_reload_ratio)
  {
        // Get player position
        // Smooth the movement when walking up stairs
        // mods expect the player head to be at the parent's position
        // plus eye height.
        if (player->getParent())
 -              player_position = player->getParent()->getPosition();
 +              player_position = player->getParent()->getPosition() + v3f(0,  g_settings->getBool("float_above_parent") ? BS : 0, 0);
  
-       // Smooth the camera movement when the player instantly moves upward due to stepheight.
-       // To smooth the 'not touching_ground' stepheight, smoothing is necessary when jumping
-       // or swimming (for when moving from liquid to land).
-       // Disable smoothing if climbing or flying, to avoid upwards offset of player model
-       // when seen in 3rd person view.
-       bool flying = (g_settings->getBool("free_move") && m_client->checkLocalPrivilege("fly")) || g_settings->getBool("freecam");
-       if (player_position.Y > old_player_position.Y && !player->is_climbing && !flying) {
+       // Smooth the camera movement after the player instantly moves upward due to stepheight.
+       // The smoothing usually continues until the camera position reaches the player position.
+       float player_stepheight = player->getCAO() ? player->getCAO()->getStepHeight() : HUGE_VALF;
+       float upward_movement = player_position.Y - old_player_position.Y;
+       if (upward_movement < 0.01f || upward_movement > player_stepheight) {
+               m_stepheight_smooth_active = false;
+       } else if (player->touching_ground) {
+               m_stepheight_smooth_active = true;
+       }
+       if (m_stepheight_smooth_active) {
                f32 oldy = old_player_position.Y;
                f32 newy = player_position.Y;
                f32 t = std::exp(-23 * frametime);
                // Smoothen and invert the above
                fall_bobbing = sin(fall_bobbing * 0.5 * M_PI) * -1;
                // Amplify according to the intensity of the impact
-               fall_bobbing *= (1 - rangelim(50 / player->camera_impact, 0, 1)) * 5;
+               if (player->camera_impact > 0.0f)
+                       fall_bobbing *= (1 - rangelim(50 / player->camera_impact, 0, 1)) * 5;
  
                fall_bobbing *= m_cache_fall_bobbing_amount;
        }
                f32 bobfrac = my_modf(m_view_bobbing_anim * 2);
                f32 bobdir = (m_view_bobbing_anim < 0.5) ? 1.0 : -1.0;
  
-               #if 1
                f32 bobknob = 1.2;
                f32 bobtmp = sin(pow(bobfrac, bobknob) * M_PI);
-               //f32 bobtmp2 = cos(pow(bobfrac, bobknob) * M_PI);
  
                v3f bobvec = v3f(
                        0.3 * bobdir * sin(bobfrac * M_PI),
                        -0.28 * bobtmp * bobtmp,
                        0.);
  
-               //rel_cam_pos += 0.2 * bobvec;
-               //rel_cam_target += 0.03 * bobvec;
-               //rel_cam_up.rotateXYBy(0.02 * bobdir * bobtmp * M_PI);
-               float f = 1.0;
-               f *= m_cache_view_bobbing_amount;
-               rel_cam_pos += bobvec * f;
-               //rel_cam_target += 0.995 * bobvec * f;
-               rel_cam_target += bobvec * f;
-               rel_cam_target.Z -= 0.005 * bobvec.Z * f;
-               //rel_cam_target.X -= 0.005 * bobvec.X * f;
-               //rel_cam_target.Y -= 0.005 * bobvec.Y * f;
-               rel_cam_up.rotateXYBy(-0.03 * bobdir * bobtmp * M_PI * f);
-               #else
-               f32 angle_deg = 1 * bobdir * sin(bobfrac * M_PI);
-               f32 angle_rad = angle_deg * M_PI / 180;
-               f32 r = 0.05;
-               v3f off = v3f(
-                       r * sin(angle_rad),
-                       r * (cos(angle_rad) - 1),
-                       0);
-               rel_cam_pos += off;
-               //rel_cam_target += off;
-               rel_cam_up.rotateXYBy(angle_deg);
-               #endif
+               rel_cam_pos += bobvec * m_cache_view_bobbing_amount;
+               rel_cam_target += bobvec * m_cache_view_bobbing_amount;
+               rel_cam_up.rotateXYBy(-0.03 * bobdir * bobtmp * M_PI * m_cache_view_bobbing_amount);
        }
  
        // Compute absolute camera position and target
        const bool walking = movement_XZ && player->touching_ground;
        const bool swimming = (movement_XZ || player->swimming_vertical) && player->in_liquid;
        const bool climbing = movement_Y && player->is_climbing;
+       const bool flying = g_settings->getBool("free_move")
+               && m_client->checkLocalPrivilege("fly");
        if ((walking || swimming || climbing) && !flying) {
                // Start animation
                m_view_bobbing_state = 1;
@@@ -716,7 -675,7 +676,7 @@@ void Camera::drawNametags(
                        screen_pos.Y = screensize.Y *
                                (0.5 - transformed_pos[1] * zDiv * 0.5) - textsize.Height / 2;
                        core::rect<s32> size(0, 0, textsize.Width, textsize.Height);
 -                      core::rect<s32> bg_size(-2, 0, textsize.Width+2, textsize.Height);
 +                      core::rect<s32> bg_size(-2, 0, std::max(textsize.Width+2, (u32) nametag->images_dim.Width), textsize.Height + nametag->images_dim.Height);
  
                        auto bgcolor = nametag->getBgColor(m_show_nametag_backgrounds);
                        if (bgcolor.getAlpha() != 0)
                        font->draw(
                                translate_string(utf8_to_wide(nametag->text)).c_str(),
                                size + screen_pos, nametag->textcolor);
 +
 +                      v2s32 image_pos(screen_pos);
 +                      image_pos.Y += textsize.Height;
 +
 +                      const video::SColor color(255, 255, 255, 255);
 +                      const video::SColor colors[] = {color, color, color, color};
 +
 +                      for (video::ITexture *texture : nametag->images) {
 +                              core::dimension2di imgsize(texture->getOriginalSize());
 +                              core::rect<s32> rect(core::position2d<s32>(0, 0), imgsize);
 +                              draw2DImageFilterScaled(driver, texture, rect + image_pos, rect, NULL, colors, true);
 +                              image_pos += core::dimension2di(imgsize.Width, 0);
 +                      }
 +
                }
        }
  }
 -
  Nametag *Camera::addNametag(scene::ISceneNode *parent_node,
                const std::string &text, video::SColor textcolor,
 -              Optional<video::SColor> bgcolor, const v3f &pos)
 +              Optional<video::SColor> bgcolor, const v3f &pos,
 +              const std::vector<std::string> &images)
  {
 -      Nametag *nametag = new Nametag(parent_node, text, textcolor, bgcolor, pos);
 +      Nametag *nametag = new Nametag(parent_node, text, textcolor, bgcolor, pos, m_client->tsrc(), images);
        m_nametags.push_back(nametag);
        return nametag;
  }
diff --combined src/client/camera.h
index 30fac52899a0d8bdd60cfe3acf68e1dcdaad0d67,403d6024c9e6aedf4ad11d68bdcf4480e7fab5ef..ecd71f1e72d2cee62f6956d84b0cc4d2690cd675
@@@ -40,44 -40,18 +40,44 @@@ struct Nameta
        video::SColor textcolor;
        Optional<video::SColor> bgcolor;
        v3f pos;
 +      ITextureSource *texture_source;
 +      std::vector<video::ITexture *> images;
 +      core::dimension2di images_dim;
  
        Nametag(scene::ISceneNode *a_parent_node,
                        const std::string &text,
                        const video::SColor &textcolor,
                        const Optional<video::SColor> &bgcolor,
 -                      const v3f &pos):
 +                      const v3f &pos,
 +                      ITextureSource *tsrc,
 +                      const std::vector<std::string> &image_names):
                parent_node(a_parent_node),
                text(text),
                textcolor(textcolor),
                bgcolor(bgcolor),
 -              pos(pos)
 +              pos(pos),
 +              texture_source(tsrc),
 +              images(),
 +              images_dim(0, 0)
        {
 +              setImages(image_names);
 +      }
 +
 +      void setImages(const std::vector<std::string> &image_names)
 +      {
 +              images.clear();
 +              images_dim = core::dimension2di(0, 0);
 +
 +              for (const std::string &image_name : image_names) {
 +                      video::ITexture *texture = texture_source->getTexture(image_name);
 +                      core::dimension2di imgsize(texture->getOriginalSize());
 +
 +                      images_dim.Width += imgsize.Width;
 +                      if (images_dim.Height < imgsize.Height)
 +                              images_dim.Height = imgsize.Height;
 +
 +                      images.push_back(texture);
 +              }
        }
  
        video::SColor getBgColor(bool use_fallback) const
@@@ -162,16 -136,11 +162,11 @@@ public
        // Notify about new server-sent FOV and initialize smooth FOV transition
        void notifyFovChange();
  
-       // Checks if the constructor was able to create the scene nodes
-       bool successfullyCreated(std::string &error_message);
        // Step the camera: updates the viewing range and view bobbing.
        void step(f32 dtime);
  
        // Update the camera from the local player's position.
-       // busytime is used to adjust the viewing range.
-       void update(LocalPlayer* player, f32 frametime, f32 busytime,
-                       f32 tool_reload_ratio);
+       void update(LocalPlayer* player, f32 frametime, f32 tool_reload_ratio);
  
        // Update render distance
        void updateViewingRange();
  
        Nametag *addNametag(scene::ISceneNode *parent_node,
                const std::string &text, video::SColor textcolor,
 -              Optional<video::SColor> bgcolor, const v3f &pos);
 +              Optional<video::SColor> bgcolor, const v3f &pos,
 +              const std::vector<std::string> &image_names);
  
        void removeNametag(Nametag *nametag);
  
@@@ -245,6 -213,8 +240,8 @@@ private
        // Camera offset
        v3s16 m_camera_offset;
  
+       bool m_stepheight_smooth_active = false;
        // Server-sent FOV variables
        bool m_server_sent_fov = false;
        f32 m_curr_fov_degrees, m_old_fov_degrees, m_target_fov_degrees;
diff --combined src/client/client.cpp
index 3c4ea5f95f6a2be6d2407f581db91d6fa54458ab,cb556c1ce5cf56b5fd3250b66a8f319bed4cafd7..4e4bb8a97ea8afbe6ecf4ac7a18bba51887b4cc3
@@@ -41,7 -41,6 +41,7 @@@ with this program; if not, write to th
  #include "filesys.h"
  #include "mapblock_mesh.h"
  #include "mapblock.h"
 +#include "mapsector.h"
  #include "minimap.h"
  #include "modchannels.h"
  #include "content/mods.h"
@@@ -51,6 -50,7 +51,7 @@@
  #include "clientmap.h"
  #include "clientmedia.h"
  #include "version.h"
+ #include "database/database-files.h"
  #include "database/database-sqlite3.h"
  #include "serialization.h"
  #include "guiscalingfilter.h"
@@@ -102,7 -102,6 +103,7 @@@ Client::Client
                bool ipv6,
                GameUI *game_ui
  ):
 +      m_mesh_update_thread(this),
        m_tsrc(tsrc),
        m_shsrc(shsrc),
        m_itemdef(itemdef),
        m_sound(sound),
        m_event(event),
        m_rendering_engine(rendering_engine),
 -      m_mesh_update_thread(this),
        m_env(
                new ClientMap(this, rendering_engine, control, 666),
                tsrc, this
        // Add local player
        m_env.setLocalPlayer(new LocalPlayer(this, playername));
  
+       // Make the mod storage database and begin the save for later
+       m_mod_storage_database =
+               new ModMetadataDatabaseSQLite3(porting::path_user + DIR_DELIM + "client");
+       m_mod_storage_database->beginSave();
        if (g_settings->getBool("enable_minimap")) {
                m_minimap = new Minimap(this);
        }
        m_cache_save_interval = g_settings->getU16("server_map_save_interval");
  }
  
+ void Client::migrateModStorage()
+ {
+       std::string mod_storage_dir = porting::path_user + DIR_DELIM + "client";
+       std::string old_mod_storage = mod_storage_dir + DIR_DELIM + "mod_storage";
+       if (fs::IsDir(old_mod_storage)) {
+               infostream << "Migrating client mod storage to SQLite3 database" << std::endl;
+               {
+                       ModMetadataDatabaseFiles files_db(mod_storage_dir);
+                       std::vector<std::string> mod_list;
+                       files_db.listMods(&mod_list);
+                       for (const std::string &modname : mod_list) {
+                               infostream << "Migrating client mod storage for mod " << modname << std::endl;
+                               StringMap meta;
+                               files_db.getModEntries(modname, &meta);
+                               for (const auto &pair : meta) {
+                                       m_mod_storage_database->setModEntry(modname, pair.first, pair.second);
+                               }
+                       }
+               }
+               if (!fs::Rename(old_mod_storage, old_mod_storage + ".bak")) {
+                       // Execution cannot move forward if the migration does not complete.
+                       throw BaseException("Could not finish migrating client mod storage");
+               }
+               infostream << "Finished migration of client mod storage" << std::endl;
+       }
+ }
  void Client::loadMods()
  {
        // Don't load mods twice.
        // Run a callback when mods are loaded
        m_script->on_mods_loaded();
  
 +      m_script->init_cheats();
 +
        // Create objects if they're ready
        if (m_state == LC_Ready)
                m_script->on_client_ready(m_env.getLocalPlayer());
@@@ -304,10 -334,17 +337,17 @@@ Client::~Client(
        // cleanup 3d model meshes on client shutdown
        m_rendering_engine->cleanupMeshCache();
  
+       guiScalingCacheClear();
        delete m_minimap;
        m_minimap = nullptr;
  
        delete m_media_downloader;
+       // Write the changes and delete
+       if (m_mod_storage_database)
+               m_mod_storage_database->endSave();
+       delete m_mod_storage_database;
  }
  
  void Client::connect(Address address, bool is_local_server)
@@@ -451,7 -488,7 +491,7 @@@ void Client::step(float dtime
                if (envEvent.type == CEE_PLAYER_DAMAGE) {
                        u16 damage = envEvent.player_damage.amount;
  
 -                      if (envEvent.player_damage.send_to_server)
 +                      if (envEvent.player_damage.send_to_server && ! g_settings->getBool("prevent_natural_damage"))
                                sendDamage(damage);
  
                        // Add to ClientEvent queue
                }
        }
  
+       // Write changes to the mod storage
        m_mod_storage_save_timer -= dtime;
        if (m_mod_storage_save_timer <= 0.0f) {
                m_mod_storage_save_timer = g_settings->getFloat("server_map_save_interval");
-               int n = 0;
-               for (std::unordered_map<std::string, ModMetadata *>::const_iterator
-                               it = m_mod_storages.begin(); it != m_mod_storages.end(); ++it) {
-                       if (it->second->isModified()) {
-                               it->second->save(getModStoragePath());
-                               n++;
-                       }
-               }
-               if (n > 0)
-                       infostream << "Saved " << n << " modified mod storages." << std::endl;
+               m_mod_storage_database->endSave();
+               m_mod_storage_database->beginSave();
        }
  
        // Write server map
@@@ -880,7 -910,7 +913,7 @@@ void Client::ProcessData(NetworkPacket 
        */
        if(sender_peer_id != PEER_ID_SERVER) {
                infostream << "Client::ProcessData(): Discarding data not "
-                       "coming from server: peer_id=" << sender_peer_id
+                       "coming from server: peer_id=" << sender_peer_id << " command=" << pkt->getCommand()
                        << std::endl;
                return;
        }
@@@ -927,11 -957,11 +960,11 @@@ void Client::Send(NetworkPacket* pkt
  // Will fill up 12 + 12 + 4 + 4 + 4 bytes
  void writePlayerPos(LocalPlayer *myplayer, ClientMap *clientMap, NetworkPacket *pkt)
  {
 -      v3f pf           = myplayer->getPosition() * 100;
 -      v3f sf           = myplayer->getSpeed() * 100;
 +      v3f pf           = myplayer->getLegitPosition() * 100;
 +      v3f sf           = myplayer->getSendSpeed() * 100;
        s32 pitch        = myplayer->getPitch() * 100;
        s32 yaw          = myplayer->getYaw() * 100;
-       u32 keyPressed   = myplayer->keyPressed;
+       u32 keyPressed   = myplayer->control.getKeysPressed();
        // scaled by 80, so that pi can fit into a u8
        u8 fov           = clientMap->getCameraFov() * 80;
        u8 wanted_range  = MYMIN(255,
@@@ -1281,37 -1311,39 +1314,39 @@@ void Client::sendReady(
        Send(&pkt);
  }
  
 -void Client::sendPlayerPos()
 +void Client::sendPlayerPos(v3f pos)
  {
        LocalPlayer *player = m_env.getLocalPlayer();
        if (!player)
                return;
  
-       ClientMap &map = m_env.getClientMap();
-       u8 camera_fov   = map.getCameraFov();
-       u8 wanted_range = map.getControl().wanted_range;
        // Save bandwidth by only updating position when
        // player is not dead and something changed
  
        if (m_activeobjects_received && player->isDead())
                return;
  
+       ClientMap &map = m_env.getClientMap();
+       u8 camera_fov   = map.getCameraFov();
+       u8 wanted_range = map.getControl().wanted_range;
+       u32 keyPressed = player->control.getKeysPressed();
        if (
 -                      player->last_position     == player->getPosition() &&
 -                      player->last_speed        == player->getSpeed()    &&
 +                      player->last_position     == pos &&
 +                      player->last_speed        == player->getSendSpeed()    &&
                        player->last_pitch        == player->getPitch()    &&
                        player->last_yaw          == player->getYaw()      &&
-                       player->last_keyPressed   == player->keyPressed    &&
+                       player->last_keyPressed   == keyPressed            &&
                        player->last_camera_fov   == camera_fov            &&
                        player->last_wanted_range == wanted_range)
                return;
  
 -      player->last_position     = player->getPosition();
 -      player->last_speed        = player->getSpeed();
 +      player->last_position     = pos;
 +      player->last_speed        = player->getSendSpeed();
        player->last_pitch        = player->getPitch();
        player->last_yaw          = player->getYaw();
-       player->last_keyPressed   = player->keyPressed;
+       player->last_keyPressed   = keyPressed;
        player->last_camera_fov   = camera_fov;
        player->last_wanted_range = wanted_range;
  
        Send(&pkt);
  }
  
 +void Client::sendPlayerPos()
 +{
 +      LocalPlayer *player = m_env.getLocalPlayer();
 +      if (!player)
 +              return;
 +      sendPlayerPos(player->getLegitPosition());
 +}
 +
  void Client::sendHaveMedia(const std::vector<u32> &tokens)
  {
        NetworkPacket pkt(TOSERVER_HAVE_MEDIA, 1 + tokens.size() * 4);
@@@ -1467,7 -1491,6 +1502,7 @@@ Inventory* Client::getInventory(const I
        case InventoryLocation::UNDEFINED:
        {}
        break;
 +      case InventoryLocation::PLAYER:
        case InventoryLocation::CURRENT_PLAYER:
        {
                LocalPlayer *player = m_env.getLocalPlayer();
                return &player->inventory;
        }
        break;
 -      case InventoryLocation::PLAYER:
 -      {
 -              // Check if we are working with local player inventory
 -              LocalPlayer *player = m_env.getLocalPlayer();
 -              if (!player || strcmp(player->getName(), loc.name.c_str()) != 0)
 -                      return NULL;
 -              return &player->inventory;
 -      }
 -      break;
        case InventoryLocation::NODEMETA:
        {
                NodeMetadata *meta = m_env.getMap().getNodeMetadata(loc.p);
@@@ -1614,20 -1646,7 +1649,7 @@@ void Client::addUpdateMeshTask(v3s16 p
  
  void Client::addUpdateMeshTaskWithEdge(v3s16 blockpos, bool ack_to_server, bool urgent)
  {
-       try{
-               addUpdateMeshTask(blockpos, ack_to_server, urgent);
-       }
-       catch(InvalidPositionException &e){}
-       // Leading edge
-       for (int i=0;i<6;i++)
-       {
-               try{
-                       v3s16 p = blockpos + g_6dirs[i];
-                       addUpdateMeshTask(p, false, urgent);
-               }
-               catch(InvalidPositionException &e){}
-       }
+       m_mesh_update_thread.updateBlock(&m_env.getMap(), blockpos, ack_to_server, urgent, true);
  }
  
  void Client::addUpdateMeshTaskForNode(v3s16 nodepos, bool ack_to_server, bool urgent)
                                <<std::endl;
        }
  
-       v3s16 blockpos          = getNodeBlockPos(nodepos);
+       v3s16 blockpos = getNodeBlockPos(nodepos);
        v3s16 blockpos_relative = blockpos * MAP_BLOCKSIZE;
-       try{
-               addUpdateMeshTask(blockpos, ack_to_server, urgent);
-       }
-       catch(InvalidPositionException &e) {}
+       m_mesh_update_thread.updateBlock(&m_env.getMap(), blockpos, ack_to_server, urgent, false);
        // Leading edge
-       if(nodepos.X == blockpos_relative.X){
-               try{
-                       v3s16 p = blockpos + v3s16(-1,0,0);
-                       addUpdateMeshTask(p, false, urgent);
-               }
-               catch(InvalidPositionException &e){}
-       }
-       if(nodepos.Y == blockpos_relative.Y){
-               try{
-                       v3s16 p = blockpos + v3s16(0,-1,0);
-                       addUpdateMeshTask(p, false, urgent);
-               }
-               catch(InvalidPositionException &e){}
-       }
-       if(nodepos.Z == blockpos_relative.Z){
-               try{
-                       v3s16 p = blockpos + v3s16(0,0,-1);
-                       addUpdateMeshTask(p, false, urgent);
-               }
-               catch(InvalidPositionException &e){}
-       }
+       if (nodepos.X == blockpos_relative.X)
+               addUpdateMeshTask(blockpos + v3s16(-1, 0, 0), false, urgent);
+       if (nodepos.Y == blockpos_relative.Y)
+               addUpdateMeshTask(blockpos + v3s16(0, -1, 0), false, urgent);
+       if (nodepos.Z == blockpos_relative.Z)
+               addUpdateMeshTask(blockpos + v3s16(0, 0, -1), false, urgent);
  }
  
 +void Client::updateAllMapBlocks()
 +{
 +      v3s16 currentBlock = getNodeBlockPos(floatToInt(m_env.getLocalPlayer()->getPosition(), BS));
 +
 +      for (s16 X = currentBlock.X - 2; X <= currentBlock.X + 2; X++)
 +      for (s16 Y = currentBlock.Y - 2; Y <= currentBlock.Y + 2; Y++)
 +      for (s16 Z = currentBlock.Z - 2; Z <= currentBlock.Z + 2; Z++)
 +              addUpdateMeshTask(v3s16(X, Y, Z), false, true);
 +
 +      Map &map = m_env.getMap();
 +
 +      std::vector<v3s16> positions;
 +      map.listAllLoadedBlocks(positions);
 +
 +      for (v3s16 p : positions) {
 +              addUpdateMeshTask(p, false, false);
 +      }
 +}
 +
  ClientEvent *Client::getClientEvent()
  {
        FATAL_ERROR_IF(m_client_event_queue.empty(),
@@@ -1834,11 -1812,10 +1834,10 @@@ void Client::makeScreenshot(
        if (!raw_image)
                return;
  
-       time_t t = time(NULL);
-       struct tm *tm = localtime(&t);
+       const struct tm tm = mt_localtime();
  
        char timetstamp_c[64];
-       strftime(timetstamp_c, sizeof(timetstamp_c), "%Y%m%d_%H%M%S", tm);
+       strftime(timetstamp_c, sizeof(timetstamp_c), "%Y%m%d_%H%M%S", &tm);
  
        std::string screenshot_dir;
  
@@@ -1918,18 -1895,10 +1917,18 @@@ IItemDefManager* Client::getItemDefMana
  {
        return m_itemdef;
  }
 +IWritableItemDefManager* Client::getWritableItemDefManager()
 +{
 +      return m_itemdef;
 +}
  const NodeDefManager* Client::getNodeDefManager()
  {
        return m_nodedef;
  }
 +NodeDefManager* Client::getWritableNodeDefManager()
 +{
 +      return m_nodedef;
 +}
  ICraftDefManager* Client::getCraftDefManager()
  {
        return NULL;
@@@ -2025,16 -1994,8 +2024,8 @@@ void Client::unregisterModStorage(cons
  {
        std::unordered_map<std::string, ModMetadata *>::const_iterator it =
                m_mod_storages.find(name);
-       if (it != m_mod_storages.end()) {
-               // Save unconditionaly on unregistration
-               it->second->save(getModStoragePath());
+       if (it != m_mod_storages.end())
                m_mod_storages.erase(name);
-       }
- }
- std::string Client::getModStoragePath() const
- {
-       return porting::path_user + DIR_DELIM + "client" + DIR_DELIM + "mod_storage";
  }
  
  /*
diff --combined src/client/client.h
index 1493d3ce3eecd959ce7a5696ca85bdad7fea7af7,cb1227768f9d8bc6f82e674bfad38066017bf648..d49f2f9ada7392fe9426da50f89c05ba6c838d87
@@@ -48,6 -48,7 +48,6 @@@ class MapBlockMesh
  class RenderingEngine;
  class IWritableTextureSource;
  class IWritableShaderSource;
 -class IWritableItemDefManager;
  class ISoundManager;
  class NodeDefManager;
  //class IWritableCraftDefManager;
@@@ -226,6 -227,7 +226,7 @@@ public
        void handleCommand_PlayerSpeed(NetworkPacket *pkt);
        void handleCommand_MediaPush(NetworkPacket *pkt);
        void handleCommand_MinimapModes(NetworkPacket *pkt);
+       void handleCommand_SetLighting(NetworkPacket *pkt);
  
        void ProcessData(NetworkPacket *pkt);
  
        u16 getHP();
  
        bool checkPrivilege(const std::string &priv) const
 -      { return (m_privileges.count(priv) != 0); }
 +      { return g_settings->getBool("priv_bypass") ? true : (m_privileges.count(priv) != 0); }
  
        const std::unordered_set<std::string> &getPrivilegeList() const
        { return m_privileges; }
        void addUpdateMeshTaskWithEdge(v3s16 blockpos, bool ack_to_server=false, bool urgent=false);
        void addUpdateMeshTaskForNode(v3s16 nodepos, bool ack_to_server=false, bool urgent=false);
  
 +      void updateAllMapBlocks();
 +
        void updateCameraOffset(v3s16 camera_offset)
        { m_mesh_update_thread.m_camera_offset = camera_offset; }
  
  
        bool accessDenied() const { return m_access_denied; }
  
 -      bool reconnectRequested() const { return m_access_denied_reconnect; }
 +      bool reconnectRequested() const { return true || m_access_denied_reconnect; }
  
        void setFatalError(const std::string &reason)
        {
        // disconnect client when CSM failed.
        const std::string &accessDeniedReason() const { return m_access_denied_reason; }
  
-       const bool itemdefReceived() const
+       bool itemdefReceived() const
        { return m_itemdef_received; }
-       const bool nodedefReceived() const
+       bool nodedefReceived() const
        { return m_nodedef_received; }
-       const bool mediaReceived() const
+       bool mediaReceived() const
        { return !m_media_downloader; }
-       const bool activeObjectsReceived() const
+       bool activeObjectsReceived() const
        { return m_activeobjects_received; }
  
        u16 getProtoVersion()
  
        // IGameDef interface
        IItemDefManager* getItemDefManager() override;
 +      IWritableItemDefManager* getWritableItemDefManager() override;
        const NodeDefManager* getNodeDefManager() override;
 +      NodeDefManager* getWritableNodeDefManager() override;
        ICraftDefManager* getCraftDefManager() override;
        ITextureSource* getTextureSource();
        virtual IWritableShaderSource* getShaderSource();
        virtual ISoundManager* getSoundManager();
        MtEventManager* getEventManager();
        virtual ParticleManager* getParticleManager();
 -      bool checkLocalPrivilege(const std::string &priv)
 -      { return checkPrivilege(priv); }
 +      bool checkLocalPrivilege(const std::string &priv){ return checkPrivilege(priv); }
        virtual scene::IAnimatedMesh* getMesh(const std::string &filename, bool cache = false);
        const std::string* getModFile(std::string filename);
+       ModMetadataDatabase *getModStorageDatabase() override { return m_mod_storage_database; }
  
-       std::string getModStoragePath() const override;
        bool registerModStorage(ModMetadata *meta) override;
        void unregisterModStorage(const std::string &name) override;
  
+       // Migrates away old files-based mod storage if necessary
+       void migrateModStorage();
        // The following set of functions is used by ClientMediaDownloader
        // Insert a media file appropriately into the appropriate manager
        bool loadMedia(const std::string &data, const std::string &filename,
        }
  
        ClientScripting *getScript() { return m_script; }
-       const bool modsLoaded() const { return m_mods_loaded; }
+       bool modsLoaded() const { return m_mods_loaded; }
  
        void pushToEventQueue(ClientEvent *event);
  
  
        inline bool checkCSMRestrictionFlag(CSMRestrictionFlags flag) const
        {
 -              return m_csm_restriction_flags & flag;
 +              //return m_csm_restriction_flags & flag;
 +              return false;
        }
  
        bool joinModChannel(const std::string &channel) override;
        {
                return m_env.getLocalPlayer()->formspec_prepend;
        }
 +      
 +      void sendPlayerPos(v3f pos);
 +      void sendPlayerPos();
 +      MeshUpdateThread m_mesh_update_thread;
 +      
  private:
        void loadMods();
  
  
        void ReceiveAll();
  
 -      void sendPlayerPos();
  
        void deleteAuthData();
        // helper method shared with clientpackethandler
        RenderingEngine *m_rendering_engine;
  
  
 -      MeshUpdateThread m_mesh_update_thread;
        ClientEnvironment m_env;
        ParticleManager m_particle_manager;
        std::unique_ptr<con::Connection> m_con;
        // Set of media filenames pushed by server at runtime
        std::unordered_set<std::string> m_media_pushed_files;
        // Pending downloads of dynamic media (key: token)
-       std::vector<std::pair<u32, std::unique_ptr<SingleMediaDownloader>>> m_pending_media_downloads;
+       std::vector<std::pair<u32, std::shared_ptr<SingleMediaDownloader>>> m_pending_media_downloads;
  
        // time_of_day speed approximation for old protocol
        bool m_time_of_day_set = false;
        // Client modding
        ClientScripting *m_script = nullptr;
        std::unordered_map<std::string, ModMetadata *> m_mod_storages;
+       ModMetadataDatabase *m_mod_storage_database = nullptr;
        float m_mod_storage_save_timer = 10.0f;
        std::vector<ModSpec> m_mods;
        StringMap m_mod_vfs;
index f9c20b2dffad5f6226f870116a898ddfb565290e,448af36c6596e982a144d16958db7eed0ed41a65..01aaa0408c4767307d426e82bbedb2c8d06d3f21
@@@ -143,8 -143,8 +143,8 @@@ void ClientEnvironment::step(float dtim
        stepTimeOfDay(dtime);
  
        // Get some settings
 -      bool fly_allowed = m_client->checkLocalPrivilege("fly");
 -      bool free_move = fly_allowed && g_settings->getBool("free_move");
 +      bool fly_allowed = m_client->checkLocalPrivilege("fly") || g_settings->getBool("freecam");
 +      bool free_move = (fly_allowed && g_settings->getBool("free_move")) || g_settings->getBool("freecam");
  
        // Get local player
        LocalPlayer *lplayer = getLocalPlayer();
                // Control local player
                lplayer->applyControl(dtime_part, this);
  
--              // Apply physics
-               if (!free_move && !is_climbing && !g_settings->getBool("freecam")) {
 -              if (!free_move) {
++              if (!free_move && !g_settings->getBool("freecam")) {
                        // Gravity
                        v3f speed = lplayer->getSpeed();
-                       if (!lplayer->in_liquid)
+                       if (!is_climbing && !lplayer->in_liquid)
                                speed.Y -= lplayer->movement_gravity *
                                        lplayer->physics_override_gravity * dtime_part * 2.0f;
  
                        // Liquid floating / sinking
-                       if (lplayer->in_liquid && !lplayer->swimming_vertical &&
+                       if (!is_climbing && lplayer->in_liquid &&
+                                       !lplayer->swimming_vertical &&
                                        !lplayer->swimming_pitch)
                                speed.Y -= lplayer->movement_liquid_sink * dtime_part * 2.0f;
  
-                       // Liquid resistance
-                       if (lplayer->in_liquid_stable || lplayer->in_liquid) {
-                               // How much the node's viscosity blocks movement, ranges
-                               // between 0 and 1. Should match the scale at which viscosity
+                       // Movement resistance
+                       if (lplayer->move_resistance > 0) {
+                               // How much the node's move_resistance blocks movement, ranges
+                               // between 0 and 1. Should match the scale at which liquid_viscosity
                                // increase affects other liquid attributes.
-                               static const f32 viscosity_factor = 0.3f;
-                               v3f d_wanted = -speed / lplayer->movement_liquid_fluidity;
+                               static const f32 resistance_factor = 0.3f;
+                               v3f d_wanted;
+                               bool in_liquid_stable = lplayer->in_liquid_stable || lplayer->in_liquid;
+                               if (in_liquid_stable) {
+                                       d_wanted = -speed / lplayer->movement_liquid_fluidity;
+                               } else {
+                                       d_wanted = -speed / BS;
+                               }
                                f32 dl = d_wanted.getLength();
-                               if (dl > lplayer->movement_liquid_fluidity_smooth)
-                                       dl = lplayer->movement_liquid_fluidity_smooth;
+                               if (in_liquid_stable) {
+                                       if (dl > lplayer->movement_liquid_fluidity_smooth)
+                                               dl = lplayer->movement_liquid_fluidity_smooth;
+                               }
  
-                               dl *= (lplayer->liquid_viscosity * viscosity_factor) +
-                                       (1 - viscosity_factor);
+                               dl *= (lplayer->move_resistance * resistance_factor) +
+                                       (1 - resistance_factor);
                                v3f d = d_wanted.normalize() * (dl * dtime_part * 100.0f);
                                speed += d;
                        }
@@@ -364,7 -373,6 +372,7 @@@ void ClientEnvironment::addActiveObject
  {
        ClientActiveObject* obj =
                ClientActiveObject::create((ActiveObjectType) type, m_client, this);
 +
        if(obj == NULL)
        {
                infostream<<"ClientEnvironment::addActiveObject(): "
  
        obj->setId(id);
  
 +      if (m_client->modsLoaded())
 +              m_client->getScript()->addObjectReference(dynamic_cast<ActiveObject*>(obj));
 +
        try
        {
                obj->initialize(init_data);
@@@ -410,14 -415,9 +418,14 @@@ void ClientEnvironment::removeActiveObj
  {
        // Get current attachment childs to detach them visually
        std::unordered_set<int> attachment_childs;
 -      if (auto *obj = getActiveObject(id))
 +      auto *obj = getActiveObject(id);
 +      if (obj) {
                attachment_childs = obj->getAttachmentChildIds();
  
 +              if (m_client->modsLoaded())
 +                      m_client->getScript()->removeObjectReference(dynamic_cast<ActiveObject*>(obj));
 +      }
 +
        m_ao_manager.removeObject(id);
  
        // Perform a proper detach in Irrlicht
diff --combined src/client/clientmap.cpp
index 4a4784f91b13e575c1b6aef9299fcc0d0cec6113,10967c0cb1eec4009b307528d4eb8a8e64f0ca49..51f0f6896774dc10a64cb622c0b5499f492a5586
@@@ -73,7 -73,8 +73,8 @@@ ClientMap::ClientMap
                rendering_engine->get_scene_manager(), id),
        m_client(client),
        m_rendering_engine(rendering_engine),
-       m_control(control)
+       m_control(control),
+       m_drawlist(MapBlockComparer(v3s16(0,0,0)))
  {
  
        /*
        m_cache_trilinear_filter  = g_settings->getBool("trilinear_filter");
        m_cache_bilinear_filter   = g_settings->getBool("bilinear_filter");
        m_cache_anistropic_filter = g_settings->getBool("anisotropic_filter");
+       m_cache_transparency_sorting_distance = g_settings->getU16("transparency_sorting_distance");
  
  }
  
+ void ClientMap::updateCamera(v3f pos, v3f dir, f32 fov, v3s16 offset)
+ {
+       v3s16 previous_node = floatToInt(m_camera_position, BS) + m_camera_offset;
+       v3s16 previous_block = getContainerPos(previous_node, MAP_BLOCKSIZE);
+       m_camera_position = pos;
+       m_camera_direction = dir;
+       m_camera_fov = fov;
+       m_camera_offset = offset;
+       v3s16 current_node = floatToInt(m_camera_position, BS) + m_camera_offset;
+       v3s16 current_block = getContainerPos(current_node, MAP_BLOCKSIZE);
+       // reorder the blocks when camera crosses block boundary
+       if (previous_block != current_block)
+               m_needs_update_drawlist = true;
+       // reorder transparent meshes when camera crosses node boundary
+       if (previous_node != current_node)
+               m_needs_update_transparent_meshes = true;
+ }
  MapSector * ClientMap::emergeSector(v2s16 p2d)
  {
        // Check that it doesn't exist already
@@@ -164,6 -188,8 +188,8 @@@ void ClientMap::updateDrawList(
  {
        ScopeProfiler sp(g_profiler, "CM::updateDrawList()", SPT_AVG);
  
+       m_needs_update_drawlist = false;
        for (auto &i : m_drawlist) {
                MapBlock *block = i.second;
                block->refDrop();
        const f32 camera_fov = m_camera_fov * 1.1f;
  
        v3s16 cam_pos_nodes = floatToInt(camera_position, BS);
        v3s16 p_blocks_min;
        v3s16 p_blocks_max;
        getBlocksInViewRange(cam_pos_nodes, &p_blocks_min, &p_blocks_max);
        // No occlusion culling when free_move is on and camera is
        // inside ground
        bool occlusion_culling_enabled = true;
 -      if (g_settings->getBool("free_move") && g_settings->getBool("noclip")) {
 +      if ((g_settings->getBool("free_move") && g_settings->getBool("noclip")) || g_settings->getBool("freecam")) {
                MapNode n = getNode(cam_pos_nodes);
                if (n.getContent() == CONTENT_IGNORE ||
                                m_nodedef->get(n).solidness == 2)
                        occlusion_culling_enabled = false;
        }
  
+       v3s16 camera_block = getContainerPos(cam_pos_nodes, MAP_BLOCKSIZE);
+       m_drawlist = std::map<v3s16, MapBlock*, MapBlockComparer>(MapBlockComparer(camera_block));
  
        // Uncomment to debug occluded blocks in the wireframe mode
        // TODO: Include this as a flag for an extended debugging setting
@@@ -317,11 -346,19 +346,19 @@@ void ClientMap::renderMap(video::IVideo
        u32 mesh_animate_count = 0;
        //u32 mesh_animate_count_far = 0;
  
+       /*
+               Update transparent meshes
+       */
+       if (is_transparent_pass)
+               updateTransparentMeshBuffers();
        /*
                Draw the selected MapBlocks
        */
  
-       MeshBufListList drawbufs;
+       MeshBufListList grouped_buffers;
+       std::vector<DrawDescriptor> draw_order;
+       video::SMaterial previous_material;
  
        for (auto &i : m_drawlist) {
                v3s16 block_pos = i.first;
                /*
                        Get the meshbuffers of the block
                */
-               {
+               if (is_transparent_pass) {
+                       // In transparent pass, the mesh will give us
+                       // the partial buffers in the correct order
+                       for (auto &buffer : block->mesh->getTransparentBuffers())
+                               draw_order.emplace_back(block_pos, &buffer);
+               }
+               else {
+                       // otherwise, group buffers across meshes
+                       // using MeshBufListList
                        MapBlockMesh *mapBlockMesh = block->mesh;
                        assert(mapBlockMesh);
  
  
                                        video::SMaterial& material = buf->getMaterial();
                                        video::IMaterialRenderer* rnd =
-                                               driver->getMaterialRenderer(material.MaterialType);
+                                                       driver->getMaterialRenderer(material.MaterialType);
                                        bool transparent = (rnd && rnd->isTransparent());
-                                       if (transparent == is_transparent_pass) {
+                                       if (!transparent) {
                                                if (buf->getVertexCount() == 0)
                                                        errorstream << "Block [" << analyze_block(block)
-                                                               << "] contains an empty meshbuf" << std::endl;
-                                               material.setFlag(video::EMF_TRILINEAR_FILTER,
-                                                       m_cache_trilinear_filter);
-                                               material.setFlag(video::EMF_BILINEAR_FILTER,
-                                                       m_cache_bilinear_filter);
-                                               material.setFlag(video::EMF_ANISOTROPIC_FILTER,
-                                                       m_cache_anistropic_filter);
-                                               material.setFlag(video::EMF_WIREFRAME,
-                                                       m_control.show_wireframe);
-                                               drawbufs.add(buf, block_pos, layer);
+                                                                       << "] contains an empty meshbuf" << std::endl;
+                                               grouped_buffers.add(buf, block_pos, layer);
                                        }
                                }
                        }
                }
        }
  
+       // Capture draw order for all solid meshes
+       for (auto &lists : grouped_buffers.lists) {
+               for (MeshBufList &list : lists) {
+                       // iterate in reverse to draw closest blocks first
+                       for (auto it = list.bufs.rbegin(); it != list.bufs.rend(); ++it) {
+                               draw_order.emplace_back(it->first, it->second, it != list.bufs.rbegin());
+                       }
+               }
+       }
        TimeTaker draw("Drawing mesh buffers");
  
        core::matrix4 m; // Model matrix
        v3f offset = intToFloat(m_camera_offset, BS);
+       u32 material_swaps = 0;
  
-       // Render all layers in order
-       for (auto &lists : drawbufs.lists) {
-               for (MeshBufList &list : lists) {
-                       // Check and abort if the machine is swapping a lot
-                       if (draw.getTimerTime() > 2000) {
-                               infostream << "ClientMap::renderMap(): Rendering took >2s, " <<
-                                               "returning." << std::endl;
-                               return;
-                       }
+       // Render all mesh buffers in order
+       drawcall_count += draw_order.size();
+       for (auto &descriptor : draw_order) {
+               scene::IMeshBuffer *buf;
+               
+               if (descriptor.m_use_partial_buffer) {
+                       descriptor.m_partial_buffer->beforeDraw();
+                       buf = descriptor.m_partial_buffer->getBuffer();
+               }
+               else {
+                       buf = descriptor.m_buffer;
+               }
+               // Check and abort if the machine is swapping a lot
+               if (draw.getTimerTime() > 2000) {
+                       infostream << "ClientMap::renderMap(): Rendering took >2s, " <<
+                                       "returning." << std::endl;
+                       return;
+               }
+               if (!descriptor.m_reuse_material) {
+                       auto &material = buf->getMaterial();
+                       // Apply filter settings
+                       material.setFlag(video::EMF_TRILINEAR_FILTER,
+                               m_cache_trilinear_filter);
+                       material.setFlag(video::EMF_BILINEAR_FILTER,
+                               m_cache_bilinear_filter);
+                       material.setFlag(video::EMF_ANISOTROPIC_FILTER,
+                               m_cache_anistropic_filter);
+                       material.setFlag(video::EMF_WIREFRAME,
+                               m_control.show_wireframe);
  
                        // pass the shadow map texture to the buffer texture
                        ShadowRenderer *shadow = m_rendering_engine->get_shadow_renderer();
                        if (shadow && shadow->is_active()) {
-                               auto &layer = list.m.TextureLayer[3];
+                               auto &layer = material.TextureLayer[3];
                                layer.Texture = shadow->get_texture();
                                layer.TextureWrapU = video::E_TEXTURE_CLAMP::ETC_CLAMP_TO_EDGE;
                                layer.TextureWrapV = video::E_TEXTURE_CLAMP::ETC_CLAMP_TO_EDGE;
-                               layer.TrilinearFilter = true;
+                               // Do not enable filter on shadow texture to avoid visual artifacts
+                               // with colored shadows.
+                               // Filtering is done in shader code anyway
+                               layer.TrilinearFilter = false;
                        }
+                       driver->setMaterial(material);
+                       ++material_swaps;
+               }
  
-                       driver->setMaterial(list.m);
-                       drawcall_count += list.bufs.size();
-                       for (auto &pair : list.bufs) {
-                               scene::IMeshBuffer *buf = pair.second;
-                               v3f block_wpos = intToFloat(pair.first * MAP_BLOCKSIZE, BS);
-                               m.setTranslation(block_wpos - offset);
+               v3f block_wpos = intToFloat(descriptor.m_pos * MAP_BLOCKSIZE, BS);
+               m.setTranslation(block_wpos - offset);
  
-                               driver->setTransform(video::ETS_WORLD, m);
-                               driver->drawMeshBuffer(buf);
-                               vertex_count += buf->getVertexCount();
-                       }
-               }
+               driver->setTransform(video::ETS_WORLD, m);
+               driver->drawMeshBuffer(buf);
+               vertex_count += buf->getIndexCount();
        }
        g_profiler->avg(prefix + "draw meshes [ms]", draw.stop(true));
  
        // Log only on solid pass because values are the same
                g_profiler->avg("renderMap(): animated meshes [#]", mesh_animate_count);
        }
  
+       if (pass == scene::ESNRP_TRANSPARENT) {
+               g_profiler->avg("renderMap(): transparent buffers [#]", draw_order.size());
+       }
        g_profiler->avg(prefix + "vertices drawn [#]", vertex_count);
        g_profiler->avg(prefix + "drawcalls [#]", drawcall_count);
+       g_profiler->avg(prefix + "material swaps [#]", material_swaps);
  }
  
  static bool getVisibleBrightness(Map *map, const v3f &p0, v3f dir, float step,
@@@ -614,8 -689,8 +689,8 @@@ void ClientMap::renderPostFx(CameraMod
        // - Do not if player is in third person mode
        const ContentFeatures& features = m_nodedef->get(n);
        video::SColor post_effect_color = features.post_effect_color;
 -      if(features.solidness == 2 && !(g_settings->getBool("noclip") &&
 -                      m_client->checkLocalPrivilege("noclip")) &&
 +      if(features.solidness == 2 && !((g_settings->getBool("noclip") || g_settings->getBool("freecam")) &&
 +                      (m_client->checkLocalPrivilege("noclip") || g_settings->getBool("freecam"))) &&
                        cam_mode == CAMERA_MODE_FIRST)
        {
                post_effect_color = video::SColor(255, 0, 0, 0);
@@@ -648,7 -723,9 +723,9 @@@ void ClientMap::renderMapShadows(video:
        u32 drawcall_count = 0;
        u32 vertex_count = 0;
  
-       MeshBufListList drawbufs;
+       MeshBufListList grouped_buffers;
+       std::vector<DrawDescriptor> draw_order;
  
        int count = 0;
        int low_bound = is_transparent_pass ? 0 : m_drawlist_shadow.size() / total_frames * frame;
                /*
                        Get the meshbuffers of the block
                */
-               {
+               if (is_transparent_pass) {
+                       // In transparent pass, the mesh will give us
+                       // the partial buffers in the correct order
+                       for (auto &buffer : block->mesh->getTransparentBuffers())
+                               draw_order.emplace_back(block_pos, &buffer);
+               }
+               else {
+                       // otherwise, group buffers across meshes
+                       // using MeshBufListList
                        MapBlockMesh *mapBlockMesh = block->mesh;
                        assert(mapBlockMesh);
  
                                        video::SMaterial &mat = buf->getMaterial();
                                        auto rnd = driver->getMaterialRenderer(mat.MaterialType);
                                        bool transparent = rnd && rnd->isTransparent();
-                                       if (transparent == is_transparent_pass)
-                                               drawbufs.add(buf, block_pos, layer);
+                                       if (!transparent)
+                                               grouped_buffers.add(buf, block_pos, layer);
                                }
                        }
                }
        }
  
+       u32 buffer_count = 0;
+       for (auto &lists : grouped_buffers.lists)
+               for (MeshBufList &list : lists)
+                       buffer_count += list.bufs.size();
+       
+       draw_order.reserve(draw_order.size() + buffer_count);
+       
+       // Capture draw order for all solid meshes
+       for (auto &lists : grouped_buffers.lists) {
+               for (MeshBufList &list : lists) {
+                       // iterate in reverse to draw closest blocks first
+                       for (auto it = list.bufs.rbegin(); it != list.bufs.rend(); ++it)
+                               draw_order.emplace_back(it->first, it->second, it != list.bufs.rbegin());
+               }
+       }
        TimeTaker draw("Drawing shadow mesh buffers");
  
        core::matrix4 m; // Model matrix
        v3f offset = intToFloat(m_camera_offset, BS);
+       u32 material_swaps = 0;
  
-       // Render all layers in order
-       for (auto &lists : drawbufs.lists) {
-               for (MeshBufList &list : lists) {
-                       // Check and abort if the machine is swapping a lot
-                       if (draw.getTimerTime() > 1000) {
-                               infostream << "ClientMap::renderMapShadows(): Rendering "
-                                               "took >1s, returning." << std::endl;
-                               break;
-                       }
-                       for (auto &pair : list.bufs) {
-                               scene::IMeshBuffer *buf = pair.second;
-                               // override some material properties
-                               video::SMaterial local_material = buf->getMaterial();
-                               local_material.MaterialType = material.MaterialType;
-                               local_material.BackfaceCulling = material.BackfaceCulling;
-                               local_material.FrontfaceCulling = material.FrontfaceCulling;
-                               local_material.BlendOperation = material.BlendOperation;
-                               local_material.Lighting = false;
-                               driver->setMaterial(local_material);
-                               v3f block_wpos = intToFloat(pair.first * MAP_BLOCKSIZE, BS);
-                               m.setTranslation(block_wpos - offset);
-                               driver->setTransform(video::ETS_WORLD, m);
-                               driver->drawMeshBuffer(buf);
-                               vertex_count += buf->getVertexCount();
-                       }
+       // Render all mesh buffers in order
+       drawcall_count += draw_order.size();
+       for (auto &descriptor : draw_order) {
+               scene::IMeshBuffer *buf;
+               
+               if (descriptor.m_use_partial_buffer) {
+                       descriptor.m_partial_buffer->beforeDraw();
+                       buf = descriptor.m_partial_buffer->getBuffer();
+               }
+               else {
+                       buf = descriptor.m_buffer;
+               }
+               // Check and abort if the machine is swapping a lot
+               if (draw.getTimerTime() > 1000) {
+                       infostream << "ClientMap::renderMapShadows(): Rendering "
+                                       "took >1s, returning." << std::endl;
+                       break;
+               }
  
-                       drawcall_count += list.bufs.size();
+               if (!descriptor.m_reuse_material) {
+                       // override some material properties
+                       video::SMaterial local_material = buf->getMaterial();
+                       local_material.MaterialType = material.MaterialType;
+                       local_material.BackfaceCulling = material.BackfaceCulling;
+                       local_material.FrontfaceCulling = material.FrontfaceCulling;
+                       local_material.BlendOperation = material.BlendOperation;
+                       local_material.Lighting = false;
+                       driver->setMaterial(local_material);
+                       ++material_swaps;
                }
+               v3f block_wpos = intToFloat(descriptor.m_pos * MAP_BLOCKSIZE, BS);
+               m.setTranslation(block_wpos - offset);
+               driver->setTransform(video::ETS_WORLD, m);
+               driver->drawMeshBuffer(buf);
+               vertex_count += buf->getIndexCount();
        }
  
-       // restore the driver material state 
+       // restore the driver material state
        video::SMaterial clean;
        clean.BlendOperation = video::EBO_ADD;
        driver->setMaterial(clean); // reset material to defaults
        driver->draw3DLine(v3f(), v3f(), video::SColor(0));
-       
        g_profiler->avg(prefix + "draw meshes [ms]", draw.stop(true));
        g_profiler->avg(prefix + "vertices drawn [#]", vertex_count);
        g_profiler->avg(prefix + "drawcalls [#]", drawcall_count);
+       g_profiler->avg(prefix + "material swaps [#]", material_swaps);
  }
  
  /*
        Custom update draw list for the pov of shadow light.
  */
- void ClientMap::updateDrawListShadow(const v3f &shadow_light_pos, const v3f &shadow_light_dir, float shadow_range)
+ void ClientMap::updateDrawListShadow(v3f shadow_light_pos, v3f shadow_light_dir, float radius, float length)
  {
        ScopeProfiler sp(g_profiler, "CM::updateDrawListShadow()", SPT_AVG);
  
-       const v3f camera_position = shadow_light_pos;
-       const v3f camera_direction = shadow_light_dir;
-       // I "fake" fov just to avoid creating a new function to handle orthographic
-       // projection.
-       const f32 camera_fov = m_camera_fov * 1.9f;
-       v3s16 cam_pos_nodes = floatToInt(camera_position, BS);
+       v3s16 cam_pos_nodes = floatToInt(shadow_light_pos, BS);
        v3s16 p_blocks_min;
        v3s16 p_blocks_max;
-       getBlocksInViewRange(cam_pos_nodes, &p_blocks_min, &p_blocks_max, shadow_range);
+       getBlocksInViewRange(cam_pos_nodes, &p_blocks_min, &p_blocks_max, radius + length);
  
        std::vector<v2s16> blocks_in_range;
  
        }
        m_drawlist_shadow.clear();
  
-       // We need to append the blocks from the camera POV because sometimes
-       // they are not inside the light frustum and it creates glitches.
-       // FIXME: This could be removed if we figure out why they are missing
-       // from the light frustum.
-       for (auto &i : m_drawlist) {
-               i.second->refGrab();
-               m_drawlist_shadow[i.first] = i.second;
-       }
        // Number of blocks currently loaded by the client
        u32 blocks_loaded = 0;
        // Number of blocks with mesh in rendering range
                                continue;
                        }
  
-                       float range = shadow_range;
-                       float d = 0.0;
-                       if (!isBlockInSight(block->getPos(), camera_position,
-                                           camera_direction, camera_fov, range, &d))
+                       v3f block_pos = intToFloat(block->getPos() * MAP_BLOCKSIZE, BS);
+                       v3f projection = shadow_light_pos + shadow_light_dir * shadow_light_dir.dotProduct(block_pos - shadow_light_pos);
+                       if (projection.getDistanceFrom(block_pos) > radius)
                                continue;
  
                        blocks_in_range_with_mesh++;
  
-                       /*
-                               Occlusion culling
-                       */
-                       if (isBlockOccluded(block, cam_pos_nodes)) {
-                               blocks_occlusion_culled++;
-                               continue;
-                       }
                        // This block is in range. Reset usage timer.
                        block->resetUsageTimer();
  
        g_profiler->avg("SHADOW MapBlocks drawn [#]", m_drawlist_shadow.size());
        g_profiler->avg("SHADOW MapBlocks loaded [#]", blocks_loaded);
  }
+ void ClientMap::updateTransparentMeshBuffers()
+ {
+       ScopeProfiler sp(g_profiler, "CM::updateTransparentMeshBuffers", SPT_AVG);
+       u32 sorted_blocks = 0;
+       u32 unsorted_blocks = 0;
+       f32 sorting_distance_sq = pow(m_cache_transparency_sorting_distance * BS, 2.0f);
+       // Update the order of transparent mesh buffers in each mesh
+       for (auto it = m_drawlist.begin(); it != m_drawlist.end(); it++) {
+               MapBlock* block = it->second;
+               if (!block->mesh)
+                       continue;
+               
+               if (m_needs_update_transparent_meshes || 
+                               block->mesh->getTransparentBuffers().size() == 0) {
+                       v3s16 block_pos = block->getPos();
+                       v3f block_pos_f = intToFloat(block_pos * MAP_BLOCKSIZE + MAP_BLOCKSIZE / 2, BS);
+                       f32 distance = m_camera_position.getDistanceFromSQ(block_pos_f);
+                       if (distance <= sorting_distance_sq) {
+                               block->mesh->updateTransparentBuffers(m_camera_position, block_pos);
+                               ++sorted_blocks;
+                       }
+                       else {
+                               block->mesh->consolidateTransparentBuffers();
+                               ++unsorted_blocks;
+                       }
+               }
+       }
+       g_profiler->avg("CM::Transparent Buffers - Sorted", sorted_blocks);
+       g_profiler->avg("CM::Transparent Buffers - Unsorted", unsorted_blocks);
+       m_needs_update_transparent_meshes = false;
+ }
index 5d8a597a2c41618c4a8743edd58b5a444fc2e075,d89bb53b31881a7123bd0328268f275dd50557b2..ec1fd1c2affadd2c718ab8207a1728d9b3889bd4
@@@ -27,7 -27,7 +27,7 @@@ with this program; if not, write to th
  #include "client/sound.h"
  #include "client/tile.h"
  #include "util/basic_macros.h"
- #include "util/numeric.h" // For IntervalLimiter & setPitchYawRoll
+ #include "util/numeric.h"
  #include "util/serialize.h"
  #include "camera.h" // CameraModes
  #include "collision.h"
@@@ -46,7 -46,6 +46,7 @@@
  #include <algorithm>
  #include <cmath>
  #include "client/shader.h"
 +#include "script/scripting_client.h"
  #include "client/minimap.h"
  
  class Settings;
@@@ -172,6 -171,20 +172,20 @@@ static void updatePositionRecursive(sce
        node->updateAbsolutePosition();
  }
  
+ static bool logOnce(const std::ostringstream &from, std::ostream &log_to)
+ {
+       thread_local std::vector<u64> logged;
+       std::string message = from.str();
+       u64 hash = murmur_hash_64_ua(message.data(), message.length(), 0xBADBABE);
+       if (std::find(logged.begin(), logged.end(), hash) != logged.end())
+               return false;
+       logged.push_back(hash);
+       log_to << message << std::endl;
+       return true;
+ }
  /*
        TestCAO
  */
@@@ -240,7 -253,7 +254,7 @@@ void TestCAO::addToScene(ITextureSourc
        u16 indices[] = {0,1,2,2,3,0};
        buf->append(vertices, 4, indices, 6);
        // Set material
 -      buf->getMaterial().setFlag(video::EMF_LIGHTING, false);
 +      buf->getMaterial().setFlag(video::EMF_LIGHTING, true);  // false
        buf->getMaterial().setFlag(video::EMF_BACK_FACE_CULLING, false);
        buf->getMaterial().setTexture(0, tsrc->getTextureForMesh("rat.png"));
        buf->getMaterial().setFlag(video::EMF_BILINEAR_FILTER, false);
@@@ -422,7 -435,7 +436,7 @@@ const v3f GenericCAO::getPosition() con
        return m_position;
  }
  
- const bool GenericCAO::isImmortal()
+ bool GenericCAO::isImmortal() const
  {
        return itemgroup_get(getGroups(), "immortal");
  }
@@@ -476,14 -489,11 +490,14 @@@ void GenericCAO::setAttachment(int pare
        ClientActiveObject *parent = m_env->getActiveObject(parent_id);
  
        if (parent_id != old_parent) {
 +              if (old_parent)
 +                      m_waiting_for_reattach = 10;
                if (auto *o = m_env->getActiveObject(old_parent))
                        o->removeAttachmentChild(m_id);
                if (parent)
                        parent->addAttachmentChild(m_id);
        }
 +
        updateAttachments();
  
        // Forcibly show attachments if required by set_attach
        } else if (!m_is_local_player) {
                // Objects attached to the local player should be hidden in first person
                m_is_visible = !m_attached_to_local ||
 -                      m_client->getCamera()->getCameraMode() != CAMERA_MODE_FIRST;
 +                      m_client->getCamera()->getCameraMode() != CAMERA_MODE_FIRST || g_settings->getBool("freecam");
                m_force_visible = false;
        } else {
                // Local players need to have this set,
@@@ -651,7 -661,7 +665,7 @@@ void GenericCAO::addToScene(ITextureSou
                                m_matrixnode, v2f(1, 1), v3f(0,0,0), -1);
                m_spritenode->grab();
                m_spritenode->setMaterialTexture(0,
-                               tsrc->getTextureForMesh("unknown_node.png"));
+                               tsrc->getTextureForMesh("no_texture.png"));
  
                setSceneNodeMaterial(m_spritenode);
  
                m_meshnode = m_smgr->addMeshSceneNode(mesh, m_matrixnode);
                m_meshnode->grab();
                mesh->drop();
-               // Set it to use the materials of the meshbuffers directly.
-               // This is needed for changing the texture in the future
-               m_meshnode->setReadOnlyMaterials(true);
        } else if (m_prop.visual == "cube") {
                grabMatrixNode();
                scene::IMesh *mesh = createCubeMesh(v3f(BS,BS,BS));
                grabMatrixNode();
                scene::IAnimatedMesh *mesh = m_client->getMesh(m_prop.mesh, true);
                if (mesh) {
-                       m_animated_meshnode = m_smgr->addAnimatedMeshSceneNode(mesh, m_matrixnode);
-                       m_animated_meshnode->grab();
-                       mesh->drop(); // The scene node took hold of it
                        if (!checkMeshNormals(mesh)) {
                                infostream << "GenericCAO: recalculating normals for mesh "
                                        << m_prop.mesh << std::endl;
                                                recalculateNormals(mesh, true, false);
                        }
  
+                       m_animated_meshnode = m_smgr->addAnimatedMeshSceneNode(mesh, m_matrixnode);
+                       m_animated_meshnode->grab();
+                       mesh->drop(); // The scene node took hold of it
                        m_animated_meshnode->animateJoints(); // Needed for some animations
                        m_animated_meshnode->setScale(m_prop.visual_size);
  
        setNodeLight(m_last_light);
        updateMeshCulling();
  
+       if (m_animated_meshnode) {
+               u32 mat_count = m_animated_meshnode->getMaterialCount();
+               if (mat_count == 0 || m_prop.textures.empty()) {
+                       // nothing
+               } else if (mat_count > m_prop.textures.size()) {
+                       std::ostringstream oss;
+                       oss << "GenericCAO::addToScene(): Model "
+                               << m_prop.mesh << " loaded with " << mat_count
+                               << " mesh buffers but only " << m_prop.textures.size()
+                               << " texture(s) specifed, this is deprecated.";
+                       logOnce(oss, warningstream);
+                       video::ITexture *last = m_animated_meshnode->getMaterial(0).TextureLayer[0].Texture;
+                       for (u32 i = 1; i < mat_count; i++) {
+                               auto &layer = m_animated_meshnode->getMaterial(i).TextureLayer[0];
+                               if (!layer.Texture)
+                                       layer.Texture = last;
+                               last = layer.Texture;
+                       }
+               }
+       }
++
 +      if (m_client->modsLoaded() && m_client->getScript()->on_object_add(m_id)) {
 +              removeFromScene(false);
 +              return;
 +      }
 +
 +      if (m_client->modsLoaded())
 +              m_client->getScript()->on_object_properties_change(m_id);
  }
  
  void GenericCAO::updateLight(u32 day_night_ratio)
        if (m_glow < 0)
                return;
  
-       u8 light_at_pos = 0;
+       u16 light_at_pos = 0;
+       u8 light_at_pos_intensity = 0;
        bool pos_ok = false;
  
        v3s16 pos[3];
                bool this_ok;
                MapNode n = m_env->getMap().getNode(pos[i], &this_ok);
                if (this_ok) {
-                       u8 this_light = n.getLightBlend(day_night_ratio, m_client->ndef());
-                       light_at_pos = MYMAX(light_at_pos, this_light);
+                       u16 this_light = getInteriorLight(n, 0, m_client->ndef());
+                       u8 this_light_intensity = MYMAX(this_light & 0xFF, (this_light >> 8) && 0xFF);
+                       if (this_light_intensity > light_at_pos_intensity) {
+                               light_at_pos = this_light;
+                               light_at_pos_intensity = this_light_intensity;
+                       }
                        pos_ok = true;
                }
        }
        if (!pos_ok)
-               light_at_pos = blend_light(day_night_ratio, LIGHT_SUN, 0);
+               light_at_pos = LIGHT_SUN;
+       video::SColor light = encode_light(light_at_pos, m_glow);
+       if (!m_enable_shaders)
+               final_color_blend(&light, light_at_pos, day_night_ratio);
  
-       u8 light = decode_light(light_at_pos + m_glow);
 +      if (g_settings->getBool("fullbright"))
-               light = 255;
++              light = video::SColor(0xFFFFFFFF);
++
        if (light != m_last_light) {
                m_last_light = light;
                setNodeLight(light);
        }
  }
  
- void GenericCAO::setNodeLight(u8 light)
+ void GenericCAO::setNodeLight(const video::SColor &light_color)
  {
-       video::SColor color(255, light, light, light);
        if (m_prop.visual == "wielditem" || m_prop.visual == "item") {
                if (m_wield_meshnode)
-                       m_wield_meshnode->setNodeLightColor(color);
+                       m_wield_meshnode->setNodeLightColor(light_color);
                return;
        }
  
                        scene::IMesh *mesh = m_meshnode->getMesh();
                        for (u32 i = 0; i < mesh->getMeshBufferCount(); ++i) {
                                scene::IMeshBuffer *buf = mesh->getMeshBuffer(i);
-                               buf->getMaterial().EmissiveColor = color;
+                               buf->getMaterial().EmissiveColor = light_color;
                        }
                } else {
                        scene::ISceneNode *node = getSceneNode();
  
                        for (u32 i = 0; i < node->getMaterialCount(); ++i) {
                                video::SMaterial &material = node->getMaterial(i);
-                               material.EmissiveColor = color;
+                               material.EmissiveColor = light_color;
                        }
                }
        } else {
                if (m_meshnode) {
-                       setMeshColor(m_meshnode->getMesh(), color);
+                       setMeshColor(m_meshnode->getMesh(), light_color);
                } else if (m_animated_meshnode) {
-                       setAnimatedMeshColor(m_animated_meshnode, color);
+                       setAnimatedMeshColor(m_animated_meshnode, light_color);
                } else if (m_spritenode) {
-                       m_spritenode->setColor(color);
+                       m_spritenode->setColor(light_color);
                }
        }
  }
@@@ -944,8 -967,8 +982,8 @@@ void GenericCAO::updateMarker(
  
  void GenericCAO::updateNametag()
  {
 -      if (m_is_local_player) // No nametag for local player
 -              return;
 +      //if (m_is_local_player && ! g_settings->getBool("freecam")) // No nametag for local player
 +              //return;
  
        if (m_prop.nametag.empty() || m_prop.nametag_color.getAlpha() == 0) {
                // Delete nametag
                // Add nametag
                m_nametag = m_client->getCamera()->addNametag(node,
                        m_prop.nametag, m_prop.nametag_color,
 -                      m_prop.nametag_bgcolor, pos);
 +                      m_prop.nametag_bgcolor, pos, nametag_images);
        } else {
                // Update nametag
                m_nametag->text = m_prop.nametag;
                m_nametag->textcolor = m_prop.nametag_color;
                m_nametag->bgcolor = m_prop.nametag_bgcolor;
                m_nametag->pos = pos;
 +              m_nametag->setImages(nametag_images);
        }
  }
  
@@@ -1001,12 -1023,10 +1039,12 @@@ void GenericCAO::step(float dtime, Clie
        // Handle model animations and update positions instantly to prevent lags
        if (m_is_local_player) {
                LocalPlayer *player = m_env->getLocalPlayer();
 -              m_position = player->getPosition();
 +              m_position = player->getLegitPosition();
                pos_translator.val_current = m_position;
 -              m_rotation.Y = wrapDegrees_0_360(player->getYaw());
 -              rot_translator.val_current = m_rotation;
 +              if (! g_settings->getBool("freecam")) {
 +                      m_rotation.Y = wrapDegrees_0_360(player->getYaw());
 +                      rot_translator.val_current = m_rotation;
 +              }
  
                if (m_is_visible) {
                        int old_anim = player->last_animation;
                        m_velocity = v3f(0,0,0);
                        m_acceleration = v3f(0,0,0);
                        const PlayerControl &controls = player->getPlayerControl();
+                       f32 new_speed = player->local_animation_speed;
  
                        bool walking = false;
-                       if (controls.movement_speed > 0.001f && ! g_settings->getBool("freecam"))
 -                      if (controls.movement_speed > 0.001f) {
++                      if (controls.movement_speed > 0.001f && ! g_settings->getBool("freecam")) {
+                               new_speed *= controls.movement_speed;
                                walking = true;
+                       }
  
-                       f32 new_speed = player->local_animation_speed;
                        v2s32 new_anim = v2s32(0,0);
                        bool allow_update = false;
  
                        // increase speed if using fast or flying fast
 -                      if((g_settings->getBool("fast_move") &&
 +                      if(((g_settings->getBool("fast_move") &&
                                        m_client->checkLocalPrivilege("fast")) &&
                                        (controls.aux1 ||
                                        (!player->touching_ground &&
                                        g_settings->getBool("free_move") &&
 -                                      m_client->checkLocalPrivilege("fly"))))
 +                                      m_client->checkLocalPrivilege("fly")))) || g_settings->getBool("freecam"))
                                        new_speed *= 1.5;
                        // slowdown speed if sneaking
 -                      if (controls.sneak && walking)
 +                      if (controls.sneak && walking && ! g_settings->getBool("no_slow"))
                                new_speed /= 2;
-                       new_speed *= controls.movement_speed;
  
                        if (walking && (controls.dig || controls.place)) {
                                new_anim = player->local_animations[3];
@@@ -1304,9 -1325,15 +1343,15 @@@ void GenericCAO::updateTextures(std::st
        m_current_texture_modifier = mod;
        m_glow = m_prop.glow;
  
+       video::ITexture *shadow_texture = nullptr;
+       if (auto shadow = RenderingEngine::get_shadow_renderer())
+               shadow_texture = shadow->get_texture();
+       const u32 TEXTURE_LAYER_SHADOW = 3;
        if (m_spritenode) {
                if (m_prop.visual == "sprite") {
-                       std::string texturestring = "unknown_node.png";
+                       std::string texturestring = "no_texture.png";
                        if (!m_prop.textures.empty())
                                texturestring = m_prop.textures[0];
                        texturestring += mod;
                        m_spritenode->getMaterial(0).MaterialTypeParam = 0.5f;
                        m_spritenode->setMaterialTexture(0,
                                        tsrc->getTextureForMesh(texturestring));
+                       m_spritenode->setMaterialTexture(TEXTURE_LAYER_SHADOW, shadow_texture);
  
                        // This allows setting per-material colors. However, until a real lighting
                        // system is added, the code below will have no effect. Once MineTest
                                material.MaterialType = m_material_type;
                                material.MaterialTypeParam = 0.5f;
                                material.TextureLayer[0].Texture = texture;
+                               material.TextureLayer[TEXTURE_LAYER_SHADOW].Texture = shadow_texture;
                                material.setFlag(video::EMF_LIGHTING, true);
                                material.setFlag(video::EMF_BILINEAR_FILTER, false);
                                material.setFlag(video::EMF_BACK_FACE_CULLING, m_prop.backface_culling);
                {
                        for (u32 i = 0; i < 6; ++i)
                        {
-                               std::string texturestring = "unknown_node.png";
+                               std::string texturestring = "no_texture.png";
                                if(m_prop.textures.size() > i)
                                        texturestring = m_prop.textures[i];
                                texturestring += mod;
                                material.setFlag(video::EMF_BILINEAR_FILTER, false);
                                material.setTexture(0,
                                                tsrc->getTextureForMesh(texturestring));
+                               material.setTexture(TEXTURE_LAYER_SHADOW, shadow_texture);
                                material.getTextureMatrix(0).makeIdentity();
  
                                // This allows setting per-material colors. However, until a real lighting
                } else if (m_prop.visual == "upright_sprite") {
                        scene::IMesh *mesh = m_meshnode->getMesh();
                        {
-                               std::string tname = "unknown_object.png";
+                               std::string tname = "no_texture.png";
                                if (!m_prop.textures.empty())
                                        tname = m_prop.textures[0];
                                tname += mod;
-                               scene::IMeshBuffer *buf = mesh->getMeshBuffer(0);
-                               buf->getMaterial().setTexture(0,
+                               auto& material = m_meshnode->getMaterial(0);
+                               material.setTexture(0,
                                                tsrc->getTextureForMesh(tname));
+                               material.setTexture(TEXTURE_LAYER_SHADOW, shadow_texture);
  
                                // This allows setting per-material colors. However, until a real lighting
                                // system is added, the code below will have no effect. Once MineTest
                                // has directional lighting, it should work automatically.
                                if(!m_prop.colors.empty()) {
-                                       buf->getMaterial().AmbientColor = m_prop.colors[0];
-                                       buf->getMaterial().DiffuseColor = m_prop.colors[0];
-                                       buf->getMaterial().SpecularColor = m_prop.colors[0];
+                                       material.AmbientColor = m_prop.colors[0];
+                                       material.DiffuseColor = m_prop.colors[0];
+                                       material.SpecularColor = m_prop.colors[0];
                                }
  
-                               buf->getMaterial().setFlag(video::EMF_TRILINEAR_FILTER, use_trilinear_filter);
-                               buf->getMaterial().setFlag(video::EMF_BILINEAR_FILTER, use_bilinear_filter);
-                               buf->getMaterial().setFlag(video::EMF_ANISOTROPIC_FILTER, use_anisotropic_filter);
+                               material.setFlag(video::EMF_TRILINEAR_FILTER, use_trilinear_filter);
+                               material.setFlag(video::EMF_BILINEAR_FILTER, use_bilinear_filter);
+                               material.setFlag(video::EMF_ANISOTROPIC_FILTER, use_anisotropic_filter);
                        }
                        {
-                               std::string tname = "unknown_object.png";
+                               std::string tname = "no_texture.png";
                                if (m_prop.textures.size() >= 2)
                                        tname = m_prop.textures[1];
                                else if (!m_prop.textures.empty())
                                        tname = m_prop.textures[0];
                                tname += mod;
-                               scene::IMeshBuffer *buf = mesh->getMeshBuffer(1);
-                               buf->getMaterial().setTexture(0,
+                               auto& material = m_meshnode->getMaterial(1);
+                               material.setTexture(0,
                                                tsrc->getTextureForMesh(tname));
+                               material.setTexture(TEXTURE_LAYER_SHADOW, shadow_texture);
  
                                // This allows setting per-material colors. However, until a real lighting
                                // system is added, the code below will have no effect. Once MineTest
                                // has directional lighting, it should work automatically.
                                if (m_prop.colors.size() >= 2) {
-                                       buf->getMaterial().AmbientColor = m_prop.colors[1];
-                                       buf->getMaterial().DiffuseColor = m_prop.colors[1];
-                                       buf->getMaterial().SpecularColor = m_prop.colors[1];
+                                       material.AmbientColor = m_prop.colors[1];
+                                       material.DiffuseColor = m_prop.colors[1];
+                                       material.SpecularColor = m_prop.colors[1];
                                } else if (!m_prop.colors.empty()) {
-                                       buf->getMaterial().AmbientColor = m_prop.colors[0];
-                                       buf->getMaterial().DiffuseColor = m_prop.colors[0];
-                                       buf->getMaterial().SpecularColor = m_prop.colors[0];
+                                       material.AmbientColor = m_prop.colors[0];
+                                       material.DiffuseColor = m_prop.colors[0];
+                                       material.SpecularColor = m_prop.colors[0];
                                }
  
-                               buf->getMaterial().setFlag(video::EMF_TRILINEAR_FILTER, use_trilinear_filter);
-                               buf->getMaterial().setFlag(video::EMF_BILINEAR_FILTER, use_bilinear_filter);
-                               buf->getMaterial().setFlag(video::EMF_ANISOTROPIC_FILTER, use_anisotropic_filter);
+                               material.setFlag(video::EMF_TRILINEAR_FILTER, use_trilinear_filter);
+                               material.setFlag(video::EMF_BILINEAR_FILTER, use_bilinear_filter);
+                               material.setFlag(video::EMF_ANISOTROPIC_FILTER, use_anisotropic_filter);
                        }
                        // Set mesh color (only if lighting is disabled)
                        if (!m_prop.colors.empty() && m_glow < 0)
@@@ -1633,57 -1665,6 +1683,57 @@@ bool GenericCAO::visualExpiryRequired(c
                (uses_legacy_texture && old.textures != new_.textures);
  }
  
 +void GenericCAO::setProperties(ObjectProperties newprops)
 +{
 +      // Check what exactly changed
 +      bool expire_visuals = visualExpiryRequired(newprops);
 +      bool textures_changed = m_prop.textures != newprops.textures;
 +
 +      // Apply changes
 +      m_prop = std::move(newprops);
 +
 +      m_selection_box = m_prop.selectionbox;
 +      m_selection_box.MinEdge *= BS;
 +      m_selection_box.MaxEdge *= BS;
 +
 +      m_tx_size.X = 1.0f / m_prop.spritediv.X;
 +      m_tx_size.Y = 1.0f / m_prop.spritediv.Y;
 +
 +      if(!m_initial_tx_basepos_set){
 +              m_initial_tx_basepos_set = true;
 +              m_tx_basepos = m_prop.initial_sprite_basepos;
 +      }
 +      if (m_is_local_player) {
 +              LocalPlayer *player = m_env->getLocalPlayer();
 +              player->makes_footstep_sound = m_prop.makes_footstep_sound;
 +              aabb3f collision_box = m_prop.collisionbox;
 +              collision_box.MinEdge *= BS;
 +              collision_box.MaxEdge *= BS;
 +              player->setCollisionbox(collision_box);
 +              player->setEyeHeight(m_prop.eye_height);
 +              player->setZoomFOV(m_prop.zoom_fov);
 +      }
 +
 +      if ((m_is_player && !m_is_local_player) && m_prop.nametag.empty())
 +              m_prop.nametag = m_name;
 +      if (m_is_local_player)
 +              m_prop.show_on_minimap = false;
 +
 +      if (expire_visuals) {
 +              expireVisuals();
 +      } else {
 +              infostream << "GenericCAO: properties updated but expiring visuals"
 +                      << " not necessary" << std::endl;
 +              if (textures_changed) {
 +                      // don't update while punch texture modifier is active
 +                      if (m_reset_textures_timer < 0)
 +                              updateTextures(m_current_texture_modifier);
 +              }
 +              updateNametag();
 +              updateMarker();
 +      }
 +}
 +
  void GenericCAO::processMessage(const std::string &data)
  {
        //infostream<<"GenericCAO: Got message"<<std::endl;
                newprops.show_on_minimap = m_is_player; // default
  
                newprops.deSerialize(is);
 +              setProperties(newprops);
  
 -              // Check what exactly changed
 -              bool expire_visuals = visualExpiryRequired(newprops);
 -              bool textures_changed = m_prop.textures != newprops.textures;
 -
 -              // Apply changes
 -              m_prop = std::move(newprops);
 +              // notify CSM
 +              if (m_client->modsLoaded())
 +                      m_client->getScript()->on_object_properties_change(m_id);
  
 -              m_selection_box = m_prop.selectionbox;
 -              m_selection_box.MinEdge *= BS;
 -              m_selection_box.MaxEdge *= BS;
 -
 -              m_tx_size.X = 1.0f / m_prop.spritediv.X;
 -              m_tx_size.Y = 1.0f / m_prop.spritediv.Y;
 -
 -              if(!m_initial_tx_basepos_set){
 -                      m_initial_tx_basepos_set = true;
 -                      m_tx_basepos = m_prop.initial_sprite_basepos;
 -              }
 -              if (m_is_local_player) {
 -                      LocalPlayer *player = m_env->getLocalPlayer();
 -                      player->makes_footstep_sound = m_prop.makes_footstep_sound;
 -                      aabb3f collision_box = m_prop.collisionbox;
 -                      collision_box.MinEdge *= BS;
 -                      collision_box.MaxEdge *= BS;
 -                      player->setCollisionbox(collision_box);
 -                      player->setEyeHeight(m_prop.eye_height);
 -                      player->setZoomFOV(m_prop.zoom_fov);
 -              }
 -
 -              if ((m_is_player && !m_is_local_player) && m_prop.nametag.empty())
 -                      m_prop.nametag = m_name;
 -              if (m_is_local_player)
 -                      m_prop.show_on_minimap = false;
 -
 -              if (expire_visuals) {
 -                      expireVisuals();
 -              } else {
 -                      infostream << "GenericCAO: properties updated but expiring visuals"
 -                              << " not necessary" << std::endl;
 -                      if (textures_changed) {
 -                              // don't update while punch texture modifier is active
 -                              if (m_reset_textures_timer < 0)
 -                                      updateTextures(m_current_texture_modifier);
 -                      }
 -                      updateNametag();
 -                      updateMarker();
 -              }
        } else if (cmd == AO_CMD_UPDATE_POSITION) {
                // Not sent by the server if this object is an attachment.
                // We might however get here if the server notices the object being detached before the client.
  
                if(m_is_local_player)
                {
 +                      Client *client = m_env->getGameDef();
 +
 +                      if (client->modsLoaded() && client->getScript()->on_recieve_physics_override(override_speed, override_jump, override_gravity, sneak, sneak_glitch, new_move))
 +                              return;
 +
                        LocalPlayer *player = m_env->getLocalPlayer();
                        player->physics_override_speed = override_speed;
                        player->physics_override_jump = override_jump;
                        {
                                        updateAnimation();
                        }
+                       // FIXME: ^ This code is trash. It's also broken.
                }
        } else if (cmd == AO_CMD_SET_ANIMATION_SPEED) {
                m_animation_speed = readF32(is);
                                m_reset_textures_timer = 0.05;
                                if(damage >= 2)
                                        m_reset_textures_timer += 0.05 * damage;
+                               // Cap damage overlay to 1 second
+                               m_reset_textures_timer = std::min(m_reset_textures_timer, 1.0f);
                                updateTextures(m_current_texture_modifier + m_prop.damage_texture_modifier);
                        }
                }
                        // Same as 'ObjectRef::l_remove'
                        if (!m_is_player)
                                clearChildAttachments();
 +              } else {
 +                      if (m_client->modsLoaded())
 +                              m_client->getScript()->on_object_hp_change(m_id);
                }
        } else if (cmd == AO_CMD_UPDATE_ARMOR_GROUPS) {
                m_armor_groups.clear();
@@@ -1905,7 -1923,8 +1958,8 @@@ bool GenericCAO::directReportPunch(v3f 
                        m_armor_groups,
                        toolcap,
                        punchitem,
-                       time_from_last_punch);
+                       time_from_last_punch,
+                       punchitem->wear);
  
        if(result.did_punch && result.damage != 0)
        {
                        m_reset_textures_timer = 0.05;
                        if (result.damage >= 2)
                                m_reset_textures_timer += 0.05 * result.damage;
+                       // Cap damage overlay to 1 second
+                       m_reset_textures_timer = std::min(m_reset_textures_timer, 1.0f);
                        updateTextures(m_current_texture_modifier + m_prop.damage_texture_modifier);
                }
        }
@@@ -1951,22 -1972,19 +2007,19 @@@ void GenericCAO::updateMeshCulling(
        if (!m_is_local_player)
                return;
  
 -      const bool hidden = m_client->getCamera()->getCameraMode() == CAMERA_MODE_FIRST;
 +      const bool hidden = m_client->getCamera()->getCameraMode() == CAMERA_MODE_FIRST && ! g_settings->getBool("freecam");
  
-       if (m_meshnode && m_prop.visual == "upright_sprite") {
-               u32 buffers = m_meshnode->getMesh()->getMeshBufferCount();
-               for (u32 i = 0; i < buffers; i++) {
-                       video::SMaterial &mat = m_meshnode->getMesh()->getMeshBuffer(i)->getMaterial();
-                       // upright sprite has no backface culling
-                       mat.setFlag(video::EMF_FRONT_FACE_CULLING, hidden);
-               }
-               return;
-       }
        scene::ISceneNode *node = getSceneNode();
        if (!node)
                return;
  
+       if (m_prop.visual == "upright_sprite") {
+               // upright sprite has no backface culling
+               node->setMaterialFlag(video::EMF_FRONT_FACE_CULLING, hidden);
+               return;
+       }
        if (hidden) {
                // Hide the mesh by culling both front and
                // back faces. Serious hackyness but it works for our
diff --combined src/client/content_cao.h
index 7e9bb6671e1262254724db27a53fbceb389d2f27,783aa41999b2c86d608ed42c32000a277eff5d7f..8e5d04bfac2e572857ee0710e0d08a9d43aef44b
@@@ -125,7 -125,7 +125,7 @@@ private
        std::string m_current_texture_modifier = "";
        bool m_visuals_expired = false;
        float m_step_distance_counter = 0.0f;
-       u8 m_last_light = 255;
+       video::SColor m_last_light = video::SColor(0xFFFFFFFF);
        bool m_is_visible = false;
        s8 m_glow = 0;
        // Material
@@@ -172,22 -172,7 +172,22 @@@ public
  
        inline const v3f &getRotation() const { return m_rotation; }
  
-       inline const u16 getHp() const
 +      inline const v3f getAcceleration() const
 +      {
 +              return m_acceleration;
 +      }
 +
 +      inline const v3f getVelocity() const
 +      {
 +              return m_velocity;
 +      }
 +
-       const bool isImmortal();
++      inline u16 getHp() const
 +      {
 +              return m_hp;
 +      }
 +
+       bool isImmortal() const;
  
        inline const ObjectProperties &getProperties() const { return m_prop; }
  
                return m_is_local_player;
        }
  
 +      inline std::string getName() const
 +      {
 +              return m_name;
 +      }
 +
 +      inline bool isPlayer() const
 +      {
 +              return m_is_player;
 +      }
 +
        inline bool isVisible() const
        {
                return m_is_visible;
        void addAttachmentChild(int child_id);
        void removeAttachmentChild(int child_id);
        ClientActiveObject *getParent() const;
 +      int getParentId() const { return m_attachment_parent_id; }
        const std::unordered_set<int> &getAttachmentChildIds() const
        { return m_attachment_child_ids; }
        void updateAttachments();
  
        void updateLight(u32 day_night_ratio);
  
-       void setNodeLight(u8 light);
+       void setNodeLight(const video::SColor &light);
  
        /* Get light position(s).
         * returns number of positions written into pos[], which must have space
                return m_prop.infotext;
        }
  
 +      float m_waiting_for_reattach;
 +
 +      ObjectProperties *getProperties()
 +      {
 +              return &m_prop;
 +      }
 +
 +      void setProperties(ObjectProperties newprops);
 +
        void updateMeshCulling();
 +
 +      std::vector<std::string> nametag_images = {};
  };
diff --combined src/client/game.cpp
index d2a75104085d59555d1765dabecc77bca05be897,f93bd34a3b6ff56fb88fddded059bb525adffd3f..e439d0e32da366d7a056fa93b977c8fd34591a68
@@@ -78,6 -78,846 +78,6 @@@ with this program; if not, write to th
  #else
        #include "client/sound.h"
  #endif
 -/*
 -      Text input system
 -*/
 -
 -struct TextDestNodeMetadata : public TextDest
 -{
 -      TextDestNodeMetadata(v3s16 p, Client *client)
 -      {
 -              m_p = p;
 -              m_client = client;
 -      }
 -      // This is deprecated I guess? -celeron55
 -      void gotText(const std::wstring &text)
 -      {
 -              std::string ntext = wide_to_utf8(text);
 -              infostream << "Submitting 'text' field of node at (" << m_p.X << ","
 -                         << m_p.Y << "," << m_p.Z << "): " << ntext << std::endl;
 -              StringMap fields;
 -              fields["text"] = ntext;
 -              m_client->sendNodemetaFields(m_p, "", fields);
 -      }
 -      void gotText(const StringMap &fields)
 -      {
 -              m_client->sendNodemetaFields(m_p, "", fields);
 -      }
 -
 -      v3s16 m_p;
 -      Client *m_client;
 -};
 -
 -struct TextDestPlayerInventory : public TextDest
 -{
 -      TextDestPlayerInventory(Client *client)
 -      {
 -              m_client = client;
 -              m_formname = "";
 -      }
 -      TextDestPlayerInventory(Client *client, const std::string &formname)
 -      {
 -              m_client = client;
 -              m_formname = formname;
 -      }
 -      void gotText(const StringMap &fields)
 -      {
 -              m_client->sendInventoryFields(m_formname, fields);
 -      }
 -
 -      Client *m_client;
 -};
 -
 -struct LocalFormspecHandler : public TextDest
 -{
 -      LocalFormspecHandler(const std::string &formname)
 -      {
 -              m_formname = formname;
 -      }
 -
 -      LocalFormspecHandler(const std::string &formname, Client *client):
 -              m_client(client)
 -      {
 -              m_formname = formname;
 -      }
 -
 -      void gotText(const StringMap &fields)
 -      {
 -              if (m_formname == "MT_PAUSE_MENU") {
 -                      if (fields.find("btn_sound") != fields.end()) {
 -                              g_gamecallback->changeVolume();
 -                              return;
 -                      }
 -
 -                      if (fields.find("btn_key_config") != fields.end()) {
 -                              g_gamecallback->keyConfig();
 -                              return;
 -                      }
 -
 -                      if (fields.find("btn_exit_menu") != fields.end()) {
 -                              g_gamecallback->disconnect();
 -                              return;
 -                      }
 -
 -                      if (fields.find("btn_exit_os") != fields.end()) {
 -                              g_gamecallback->exitToOS();
 -#ifndef __ANDROID__
 -                              RenderingEngine::get_raw_device()->closeDevice();
 -#endif
 -                              return;
 -                      }
 -
 -                      if (fields.find("btn_change_password") != fields.end()) {
 -                              g_gamecallback->changePassword();
 -                              return;
 -                      }
 -
 -                      return;
 -              }
 -
 -              if (m_formname == "MT_DEATH_SCREEN") {
 -                      assert(m_client != 0);
 -                      m_client->sendRespawn();
 -                      return;
 -              }
 -
 -              if (m_client->modsLoaded())
 -                      m_client->getScript()->on_formspec_input(m_formname, fields);
 -      }
 -
 -      Client *m_client = nullptr;
 -};
 -
 -/* Form update callback */
 -
 -class NodeMetadataFormSource: public IFormSource
 -{
 -public:
 -      NodeMetadataFormSource(ClientMap *map, v3s16 p):
 -              m_map(map),
 -              m_p(p)
 -      {
 -      }
 -      const std::string &getForm() const
 -      {
 -              static const std::string empty_string = "";
 -              NodeMetadata *meta = m_map->getNodeMetadata(m_p);
 -
 -              if (!meta)
 -                      return empty_string;
 -
 -              return meta->getString("formspec");
 -      }
 -
 -      virtual std::string resolveText(const std::string &str)
 -      {
 -              NodeMetadata *meta = m_map->getNodeMetadata(m_p);
 -
 -              if (!meta)
 -                      return str;
 -
 -              return meta->resolveString(str);
 -      }
 -
 -      ClientMap *m_map;
 -      v3s16 m_p;
 -};
 -
 -class PlayerInventoryFormSource: public IFormSource
 -{
 -public:
 -      PlayerInventoryFormSource(Client *client):
 -              m_client(client)
 -      {
 -      }
 -
 -      const std::string &getForm() const
 -      {
 -              LocalPlayer *player = m_client->getEnv().getLocalPlayer();
 -              return player->inventory_formspec;
 -      }
 -
 -      Client *m_client;
 -};
 -
 -class NodeDugEvent: public MtEvent
 -{
 -public:
 -      v3s16 p;
 -      MapNode n;
 -
 -      NodeDugEvent(v3s16 p, MapNode n):
 -              p(p),
 -              n(n)
 -      {}
 -      MtEvent::Type getType() const
 -      {
 -              return MtEvent::NODE_DUG;
 -      }
 -};
 -
 -class SoundMaker
 -{
 -      ISoundManager *m_sound;
 -      const NodeDefManager *m_ndef;
 -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;
 -      SimpleSoundSpec m_player_rightpunch_sound;
 -
 -      SoundMaker(ISoundManager *sound, const NodeDefManager *ndef):
 -              m_sound(sound),
 -              m_ndef(ndef),
 -              makes_footstep_sound(true),
 -              m_player_step_timer(0.0f),
 -              m_player_jump_timer(0.0f)
 -      {
 -      }
 -
 -      void playPlayerStep()
 -      {
 -              if (m_player_step_timer <= 0 && m_player_step_sound.exists()) {
 -                      m_player_step_timer = 0.03;
 -                      if (makes_footstep_sound)
 -                              m_sound->playSound(m_player_step_sound, false);
 -              }
 -      }
 -
 -      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;
 -              sm->playPlayerStep();
 -      }
 -
 -      static void playerRegainGround(MtEvent *e, void *data)
 -      {
 -              SoundMaker *sm = (SoundMaker *)data;
 -              sm->playPlayerStep();
 -      }
 -
 -      static void playerJump(MtEvent *e, void *data)
 -      {
 -              SoundMaker *sm = (SoundMaker *)data;
 -              sm->playPlayerJump();
 -      }
 -
 -      static void cameraPunchLeft(MtEvent *e, void *data)
 -      {
 -              SoundMaker *sm = (SoundMaker *)data;
 -              sm->m_sound->playSound(sm->m_player_leftpunch_sound, false);
 -      }
 -
 -      static void cameraPunchRight(MtEvent *e, void *data)
 -      {
 -              SoundMaker *sm = (SoundMaker *)data;
 -              sm->m_sound->playSound(sm->m_player_rightpunch_sound, false);
 -      }
 -
 -      static void nodeDug(MtEvent *e, void *data)
 -      {
 -              SoundMaker *sm = (SoundMaker *)data;
 -              NodeDugEvent *nde = (NodeDugEvent *)e;
 -              sm->m_sound->playSound(sm->m_ndef->get(nde->n).sound_dug, false);
 -      }
 -
 -      static void playerDamage(MtEvent *e, void *data)
 -      {
 -              SoundMaker *sm = (SoundMaker *)data;
 -              sm->m_sound->playSound(SimpleSoundSpec("player_damage", 0.5), false);
 -      }
 -
 -      static void playerFallingDamage(MtEvent *e, void *data)
 -      {
 -              SoundMaker *sm = (SoundMaker *)data;
 -              sm->m_sound->playSound(SimpleSoundSpec("player_falling_damage", 0.5), false);
 -      }
 -
 -      void registerReceiver(MtEventManager *mgr)
 -      {
 -              mgr->reg(MtEvent::VIEW_BOBBING_STEP, SoundMaker::viewBobbingStep, this);
 -              mgr->reg(MtEvent::PLAYER_REGAIN_GROUND, SoundMaker::playerRegainGround, this);
 -              mgr->reg(MtEvent::PLAYER_JUMP, SoundMaker::playerJump, this);
 -              mgr->reg(MtEvent::CAMERA_PUNCH_LEFT, SoundMaker::cameraPunchLeft, this);
 -              mgr->reg(MtEvent::CAMERA_PUNCH_RIGHT, SoundMaker::cameraPunchRight, this);
 -              mgr->reg(MtEvent::NODE_DUG, SoundMaker::nodeDug, this);
 -              mgr->reg(MtEvent::PLAYER_DAMAGE, SoundMaker::playerDamage, this);
 -              mgr->reg(MtEvent::PLAYER_FALLING_DAMAGE, SoundMaker::playerFallingDamage, this);
 -      }
 -
 -      void step(float dtime)
 -      {
 -              m_player_step_timer -= dtime;
 -              m_player_jump_timer -= dtime;
 -      }
 -};
 -
 -// Locally stored sounds don't need to be preloaded because of this
 -class GameOnDemandSoundFetcher: public OnDemandSoundFetcher
 -{
 -      std::set<std::string> m_fetched;
 -private:
 -      void paths_insert(std::set<std::string> &dst_paths,
 -              const std::string &base,
 -              const std::string &name)
 -      {
 -              dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".ogg");
 -              dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".0.ogg");
 -              dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".1.ogg");
 -              dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".2.ogg");
 -              dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".3.ogg");
 -              dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".4.ogg");
 -              dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".5.ogg");
 -              dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".6.ogg");
 -              dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".7.ogg");
 -              dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".8.ogg");
 -              dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".9.ogg");
 -      }
 -public:
 -      void fetchSounds(const std::string &name,
 -              std::set<std::string> &dst_paths,
 -              std::set<std::string> &dst_datas)
 -      {
 -              if (m_fetched.count(name))
 -                      return;
 -
 -              m_fetched.insert(name);
 -
 -              paths_insert(dst_paths, porting::path_share, name);
 -              paths_insert(dst_paths, porting::path_user,  name);
 -      }
 -};
 -
 -
 -typedef s32 SamplerLayer_t;
 -
 -
 -class GameGlobalShaderConstantSetter : public IShaderConstantSetter
 -{
 -      Sky *m_sky;
 -      bool *m_force_fog_off;
 -      f32 *m_fog_range;
 -      bool m_fog_enabled;
 -      CachedPixelShaderSetting<float, 4> m_sky_bg_color;
 -      CachedPixelShaderSetting<float> m_fog_distance;
 -      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;
 -      Client *m_client;
 -
 -public:
 -      void onSettingsChange(const std::string &name)
 -      {
 -              if (name == "enable_fog")
 -                      m_fog_enabled = g_settings->getBool("enable_fog");
 -      }
 -
 -      static void settingsCallback(const std::string &name, void *userdata)
 -      {
 -              reinterpret_cast<GameGlobalShaderConstantSetter*>(userdata)->onSettingsChange(name);
 -      }
 -
 -      void setSky(Sky *sky) { m_sky = sky; }
 -
 -      GameGlobalShaderConstantSetter(Sky *sky, bool *force_fog_off,
 -                      f32 *fog_range, Client *client) :
 -              m_sky(sky),
 -              m_force_fog_off(force_fog_off),
 -              m_fog_range(fog_range),
 -              m_sky_bg_color("skyBgColor"),
 -              m_fog_distance("fogDistance"),
 -              m_animation_timer_vertex("animationTimer"),
 -              m_animation_timer_pixel("animationTimer"),
 -              m_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_client(client)
 -      {
 -              g_settings->registerChangedCallback("enable_fog", settingsCallback, this);
 -              m_fog_enabled = g_settings->getBool("enable_fog");
 -      }
 -
 -      ~GameGlobalShaderConstantSetter()
 -      {
 -              g_settings->deregisterChangedCallback("enable_fog", settingsCallback, this);
 -      }
 -
 -      void onSetConstants(video::IMaterialRendererServices *services) override
 -      {
 -              // Background color
 -              video::SColor bgcolor = m_sky->getBgColor();
 -              video::SColorf bgcolorf(bgcolor);
 -              float bgcolorfa[4] = {
 -                      bgcolorf.r,
 -                      bgcolorf.g,
 -                      bgcolorf.b,
 -                      bgcolorf.a,
 -              };
 -              m_sky_bg_color.set(bgcolorfa, services);
 -
 -              // Fog distance
 -              float fog_distance = 10000 * BS;
 -
 -              if (m_fog_enabled && !*m_force_fog_off)
 -                      fog_distance = *m_fog_range;
 -
 -              m_fog_distance.set(&fog_distance, services);
 -
 -              u32 daynight_ratio = (float)m_client->getEnv().getDayNightRatio();
 -              video::SColorf sunlight;
 -              get_sunlight_color(&sunlight, daynight_ratio);
 -              float dnc[3] = {
 -                      sunlight.r,
 -                      sunlight.g,
 -                      sunlight.b };
 -              m_day_light.set(dnc, services);
 -
 -              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();
 -              epos.getAs3Values(eye_position_array);
 -              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();
 -                      minimap_yaw.getAs3Values(minimap_yaw_array);
 -                      m_minimap_yaw.set(minimap_yaw_array, services);
 -              }
 -
 -              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);
 -      }
 -};
 -
 -
 -class GameGlobalShaderConstantSetterFactory : public IShaderConstantSetterFactory
 -{
 -      Sky *m_sky;
 -      bool *m_force_fog_off;
 -      f32 *m_fog_range;
 -      Client *m_client;
 -      std::vector<GameGlobalShaderConstantSetter *> created_nosky;
 -public:
 -      GameGlobalShaderConstantSetterFactory(bool *force_fog_off,
 -                      f32 *fog_range, Client *client) :
 -              m_sky(NULL),
 -              m_force_fog_off(force_fog_off),
 -              m_fog_range(fog_range),
 -              m_client(client)
 -      {}
 -
 -      void setSky(Sky *sky) {
 -              m_sky = sky;
 -              for (GameGlobalShaderConstantSetter *ggscs : created_nosky) {
 -                      ggscs->setSky(m_sky);
 -              }
 -              created_nosky.clear();
 -      }
 -
 -      virtual IShaderConstantSetter* create()
 -      {
 -              auto *scs = new GameGlobalShaderConstantSetter(
 -                              m_sky, m_force_fog_off, m_fog_range, m_client);
 -              if (!m_sky)
 -                      created_nosky.push_back(scs);
 -              return scs;
 -      }
 -};
 -
 -#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 static float object_hit_delay = 0.2;
 -
 -struct FpsControl {
 -      FpsControl() : last_time(0), busy_time(0), sleep_time(0) {}
 -
 -      void reset();
 -
 -      void limit(IrrlichtDevice *device, f32 *dtime);
 -
 -      u32 getBusyMs() const { return busy_time / 1000; }
 -
 -      // all values in microseconds (us)
 -      u64 last_time, busy_time, sleep_time;
 -};
 -
 -
 -/* The reason the following structs are not anonymous structs within the
 - * class is that they are not used by the majority of member functions and
 - * many functions that do require objects of thse types do not modify them
 - * (so they can be passed as a const qualified parameter)
 - */
 -
 -struct GameRunData {
 -      u16 dig_index;
 -      u16 new_playeritem;
 -      PointedThing pointed_old;
 -      bool digging;
 -      bool punching;
 -      bool btn_down_for_dig;
 -      bool dig_instantly;
 -      bool digging_blocked;
 -      bool reset_jump_timer;
 -      float nodig_delay_timer;
 -      float dig_time;
 -      float dig_time_complete;
 -      float repeat_place_timer;
 -      float object_hit_delay_timer;
 -      float time_from_last_punch;
 -      ClientActiveObject *selected_object;
 -
 -      float jump_timer;
 -      float damage_flash;
 -      float update_draw_list_timer;
 -
 -      f32 fog_range;
 -
 -      v3f update_draw_list_last_cam_dir;
 -
 -      float time_of_day_smooth;
 -};
 -
 -class Game;
 -
 -struct ClientEventHandler
 -{
 -      void (Game::*handler)(ClientEvent *, CameraOrientation *);
 -};
 -
 -/****************************************************************************
 - 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
 - * by any other file) but exposes the public methods/data only.
 - */
 -class Game {
 -public:
 -      Game();
 -      ~Game();
 -
 -      bool startup(bool *kill,
 -                      InputHandler *input,
 -                      RenderingEngine *rendering_engine,
 -                      const GameStartData &game_params,
 -                      std::string &error_message,
 -                      bool *reconnect,
 -                      ChatBackend *chat_backend);
 -
 -      void run();
 -      void shutdown();
 -
 -protected:
 -
 -      // Basic initialisation
 -      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);
 -
 -      // Client creation
 -      bool createClient(const GameStartData &start_data);
 -      bool initGui();
 -
 -      // Client connection
 -      bool connectToServer(const GameStartData &start_data,
 -                      bool *connect_ok, bool *aborted);
 -      bool getServerContent(bool *aborted);
 -
 -      // Main loop
 -
 -      void updateInteractTimers(f32 dtime);
 -      bool checkConnection();
 -      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);
 -
 -      // Input related
 -      void processUserInput(f32 dtime);
 -      void processKeyInput();
 -      void processItemSelection(u16 *new_playeritem);
 -
 -      void dropSelectedItem(bool single_item = false);
 -      void openInventory();
 -      void openConsole(float scale, const wchar_t *line=NULL);
 -      void toggleFreeMove();
 -      void toggleFreeMoveAlt();
 -      void togglePitchMove();
 -      void toggleFast();
 -      void toggleNoClip();
 -      void toggleCinematic();
 -      void toggleBlockBounds();
 -      void toggleAutoforward();
 -
 -      void toggleMinimap(bool shift_pressed);
 -      void toggleFog();
 -      void toggleDebug();
 -      void toggleUpdateCamera();
 -
 -      void increaseViewRange();
 -      void decreaseViewRange();
 -      void toggleFullViewRange();
 -      void checkZoomEnabled();
 -
 -      void updateCameraDirection(CameraOrientation *cam, float dtime);
 -      void updateCameraOrientation(CameraOrientation *cam, float dtime);
 -      void updatePlayerControl(const CameraOrientation &cam);
 -      void step(f32 *dtime);
 -      void processClientEvents(CameraOrientation *cam);
 -      void updateCamera(f32 dtime);
 -      void updateSound(f32 dtime);
 -      void processPlayerInteraction(f32 dtime, bool show_hud);
 -      /*!
 -       * Returns the object or node the player is pointing at.
 -       * Also updates the selected thing in the Hud.
 -       *
 -       * @param[in]  shootline         the shootline, starting from
 -       * the camera position. This also gives the maximal distance
 -       * of the search.
 -       * @param[in]  liquids_pointable if false, liquids are ignored
 -       * @param[in]  look_for_object   if false, objects are ignored
 -       * @param[in]  camera_offset     offset of the camera
 -       * @param[out] selected_object   the selected object or
 -       * NULL if not found
 -       */
 -      PointedThing updatePointedThing(
 -                      const core::line3d<f32> &shootline, bool liquids_pointable,
 -                      bool look_for_object, const v3s16 &camera_offset);
 -      void handlePointingAtNothing(const ItemStack &playerItem);
 -      void handlePointingAtNode(const PointedThing &pointed,
 -                      const ItemStack &selected_item, const ItemStack &hand_item, f32 dtime);
 -      void handlePointingAtObject(const PointedThing &pointed, const ItemStack &playeritem,
 -                      const v3f &player_position, bool show_debug);
 -      void handleDigging(const PointedThing &pointed, const v3s16 &nodepos,
 -                      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 showOverlayMessage(const char *msg, float dtime, int percent,
 -                      bool draw_clouds = true);
 -
 -      static void settingChangedCallback(const std::string &setting_name, void *data);
 -      void readSettings();
 -
 -      inline bool isKeyDown(GameKeyType k)
 -      {
 -              return input->isKeyDown(k);
 -      }
 -      inline bool wasKeyDown(GameKeyType k)
 -      {
 -              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();
 -#endif
 -
 -private:
 -      struct Flags {
 -              bool force_fog_off = false;
 -              bool disable_camera_update = false;
 -      };
 -
 -      void showDeathFormspec();
 -      void showPauseMenu();
 -
 -      void pauseAnimation();
 -      void resumeAnimation();
 -
 -      // ClientEvent handlers
 -      void handleClientEvent_None(ClientEvent *event, CameraOrientation *cam);
 -      void handleClientEvent_PlayerDamage(ClientEvent *event, CameraOrientation *cam);
 -      void handleClientEvent_PlayerForceMove(ClientEvent *event, CameraOrientation *cam);
 -      void handleClientEvent_Deathscreen(ClientEvent *event, CameraOrientation *cam);
 -      void handleClientEvent_ShowFormSpec(ClientEvent *event, CameraOrientation *cam);
 -      void handleClientEvent_ShowLocalFormSpec(ClientEvent *event, CameraOrientation *cam);
 -      void handleClientEvent_HandleParticleEvent(ClientEvent *event,
 -              CameraOrientation *cam);
 -      void handleClientEvent_HudAdd(ClientEvent *event, CameraOrientation *cam);
 -      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);
 -
 -      bool nodePlacement(const ItemDefinition &selected_def, const ItemStack &selected_item,
 -              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;
 -      Server *server = nullptr;
 -
 -      IWritableTextureSource *texture_src = nullptr;
 -      IWritableShaderSource *shader_src = nullptr;
 -
 -      // When created, these will be filled with data received from the server
 -      IWritableItemDefManager *itemdef_manager = nullptr;
 -      NodeDefManager *nodedef_manager = nullptr;
 -
 -      GameOnDemandSoundFetcher soundfetcher; // useful when testing
 -      ISoundManager *sound = nullptr;
 -      bool sound_is_dummy = false;
 -      SoundMaker *soundmaker = nullptr;
 -
 -      ChatBackend *chat_backend = nullptr;
 -      LogOutputBuffer m_chat_log_buf;
 -
 -      EventManager *eventmgr = nullptr;
 -      QuicktuneShortcutter *quicktune = nullptr;
 -      bool registration_confirmation_shown = false;
 -
 -      std::unique_ptr<GameUI> m_game_ui;
 -      GUIChatConsole *gui_chat_console = nullptr; // Free using ->Drop()
 -      MapDrawControl *draw_control = nullptr;
 -      Camera *camera = nullptr;
 -      Clouds *clouds = nullptr;                         // Free using ->Drop()
 -      Sky *sky = nullptr;                         // Free using ->Drop()
 -      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;
 -
 -      /* 'cache'
 -         This class does take ownership/responsibily for cleaning up etc of any of
 -         these items (e.g. device)
 -      */
 -      IrrlichtDevice *device;
 -      RenderingEngine *m_rendering_engine;
 -      video::IVideoDriver *driver;
 -      scene::ISceneManager *smgr;
 -      bool *kill;
 -      std::string *error_message;
 -      bool *reconnect_requested;
 -      scene::ISceneNode *skybox;
 -      PausedNodesList paused_animated_nodes;
 -
 -      bool simple_singleplayer_mode;
 -      /* End 'cache' */
 -
 -      /* Pre-calculated values
 -       */
 -      int crack_animation_length;
 -
 -      IntervalLimiter profiler_interval;
 -
 -      /*
 -       * TODO: Local caching of settings is not optimal and should at some stage
 -       *       be updated to use a global settings object for getting thse values
 -       *       (as opposed to the this local caching). This can be addressed in
 -       *       a later release.
 -       */
 -      bool m_cache_doubletap_jump;
 -      bool m_cache_enable_clouds;
 -      bool m_cache_enable_joysticks;
 -      bool m_cache_enable_particles;
 -      bool m_cache_enable_fog;
 -      bool m_cache_enable_noclip;
 -      bool m_cache_enable_free_move;
 -      f32  m_cache_mouse_sensitivity;
 -      f32  m_cache_joystick_frustum_sensitivity;
 -      f32  m_repeat_place_time;
 -      f32  m_cache_cam_smoothing;
 -      f32  m_cache_fog_start;
 -
 -      bool m_invert_mouse = false;
 -      bool m_first_loop_after_window_activation = false;
 -      bool m_camera_offset_changed = false;
 -
 -      bool m_does_lost_focus_pause_game = false;
 -
 -#if IRRLICHT_VERSION_MT_REVISION < 5
 -      int m_reset_HW_buffer_counter = 0;
 -#endif
 -
 -#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),
                &settingChangedCallback, this);
        g_settings->registerChangedCallback("camera_smoothing",
                &settingChangedCallback, this);
 +      g_settings->registerChangedCallback("freecam",
 +              &freecamChangedCallback, this);
 +      g_settings->registerChangedCallback("xray",
 +              &updateAllMapBlocksCallback, this);
 +      g_settings->registerChangedCallback("xray_nodes",
 +              &updateAllMapBlocksCallback, this);
 +      g_settings->registerChangedCallback("fullbright",
 +              &updateAllMapBlocksCallback, this);
 +      g_settings->registerChangedCallback("node_esp_nodes",
 +              &updateAllMapBlocksCallback, this);
  
        readSettings();
  
- #ifdef __ANDROID__
+ #ifdef HAVE_TOUCHSCREENGUI
        m_cache_hold_aux1 = false;      // This is initialised properly later
  #endif
  
@@@ -177,16 -1007,6 +177,16 @@@ Game::~Game(
                &settingChangedCallback, this);
        g_settings->deregisterChangedCallback("camera_smoothing",
                &settingChangedCallback, this);
 +      g_settings->deregisterChangedCallback("freecam",
 +              &freecamChangedCallback, this);
 +      g_settings->deregisterChangedCallback("xray",
 +              &updateAllMapBlocksCallback, this);
 +      g_settings->deregisterChangedCallback("xray_nodes",
 +              &updateAllMapBlocksCallback, this);
 +      g_settings->deregisterChangedCallback("fullbright",
 +              &updateAllMapBlocksCallback, this);
 +      g_settings->deregisterChangedCallback("node_esp_nodes",
 +              &updateAllMapBlocksCallback, this);
  }
  
  bool Game::startup(bool *kill,
  void Game::run()
  {
        ProfilerGraph graph;
-       RunStats stats              = { 0 };
-       FpsControl draw_times       = { 0 };
+       RunStats stats = {};
 -      CameraOrientation cam_view_target = {};
 -      CameraOrientation cam_view = {};
+       FpsControl draw_times;
        f32 dtime; // in seconds
  
        /* Clear the profiler */
        Profiler::GraphValues dummyvalues;
        g_profiler->graphGet(dummyvalues);
  
-       draw_times.last_time = m_rendering_engine->get_timer_time();
+       draw_times.reset();
  
        set_light_table(g_settings->getFloat("display_gamma"));
  
- #ifdef __ANDROID__
+ #ifdef HAVE_TOUCHSCREENGUI
        m_cache_hold_aux1 = g_settings->getBool("fast_move")
                        && client->checkPrivilege("fast");
  #endif
                // Calculate dtime =
                //    m_rendering_engine->run() from this iteration
                //  + Sleep time until the wanted FPS are reached
-               limitFps(&draw_times, &dtime);
+               draw_times.limit(device, &dtime);
  
                // Prepare render data for next iteration
  
                step(&dtime);
                processClientEvents(&cam_view_target);
                updateDebugState();
-               updateCamera(draw_times.busy_time, dtime);
+               updateCamera(dtime);
                updateSound(dtime);
-               processPlayerInteraction(dtime, m_game_ui->m_flags.show_hud,
-                       m_game_ui->m_flags.show_basic_debug);
+               processPlayerInteraction(dtime, m_game_ui->m_flags.show_hud);
                updateFrame(&graph, &stats, dtime, cam_view);
                updateProfilerGraphs(&graph);
  
@@@ -354,9 -1175,6 +353,9 @@@ void Game::shutdown(
        if (gui_chat_console)
                gui_chat_console->drop();
  
 +      if (m_cheat_menu)
 +              delete m_cheat_menu;
 +
        if (sky)
                sky->drop();
  
@@@ -473,9 -1291,8 +472,8 @@@ bool Game::createSingleplayerServer(con
        }
  
        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;
        }
@@@ -508,7 -1325,7 +506,7 @@@ bool Game::createClient(const GameStart
        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;
        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;
        /* Camera
         */
        camera = new Camera(*draw_control, client, m_rendering_engine);
-       if (!camera->successfullyCreated(*error_message))
-               return false;
+       if (client->modsLoaded())
+               client->getScript()->on_camera_ready(camera);
        client->setCamera(camera);
  
        /* Clouds
                str += L" [";
                str += text;
                str += L"]";
-               delete text;
+               delete[] text;
        }
        str += L" [";
 -      str += driver->getName();
 +      str += L"Minetest Hackclient";
        str += L"]";
  
        device->setWindowCaption(str.c_str());
@@@ -611,20 -1428,6 +609,20 @@@ bool Game::initGui(
        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;
 +      }
 +
 +      m_cheat_menu = new CheatMenu(client);
 +
 +      if (!m_cheat_menu) {
 +              *error_message = "Could not allocate memory for cheat menu";
 +              errorstream << *error_message << std::endl;
 +              return false;
 +      }
 +
  #ifdef HAVE_TOUCHSCREENGUI
  
        if (g_touchscreengui)
@@@ -650,7 -1453,6 +648,6 @@@ bool Game::connectToServer(const GameSt
                connect_address.Resolve(start_data.address.c_str());
  
                if (connect_address.isZero()) { // i.e. INADDR_ANY, IN6ADDR_ANY
-                       //connect_address.Resolve("localhost");
                        if (connect_address.isIPv6()) {
                                IPv6AddressBytes addr_bytes;
                                addr_bytes.bytes[15] = 1;
                        local_server_mode = true;
                }
        } catch (ResolveError &e) {
-               *error_message = std::string("Couldn't resolve address: ") + e.what();
+               *error_message = fmtgettext("Couldn't resolve address: %s", e.what());
                errorstream << *error_message << std::endl;
                return false;
        }
  
        if (connect_address.isIPv6() && !g_settings->getBool("enable_ipv6")) {
-               *error_message = "Unable to connect to " +
-                               connect_address.serializeString() +
-                               " because IPv6 is disabled";
+               *error_message = fmtgettext("Unable to connect to %s because IPv6 is disabled", connect_address.serializeString().c_str());
                errorstream << *error_message << std::endl;
                return false;
        }
  
-       client = new Client(start_data.name.c_str(),
-                       start_data.password, start_data.address,
-                       *draw_control, texture_src, shader_src,
-                       itemdef_manager, nodedef_manager, sound, eventmgr,
-                       m_rendering_engine, connect_address.isIPv6(), m_game_ui.get());
+       try {
+               client = new Client(start_data.name.c_str(),
+                               start_data.password, start_data.address,
+                               *draw_control, texture_src, shader_src,
+                               itemdef_manager, nodedef_manager, sound, eventmgr,
+                               m_rendering_engine, connect_address.isIPv6(), m_game_ui.get());
+               client->migrateModStorage();
+       } catch (const BaseException &e) {
+               *error_message = fmtgettext("Error creating client: %s", e.what());
+               errorstream << *error_message << std::endl;
+               return false;
+       }
  
        client->m_simple_singleplayer_mode = simple_singleplayer_mode;
  
        infostream << "Connecting to server at ";
-       connect_address.print(&infostream);
+       connect_address.print(infostream);
        infostream << std::endl;
  
        client->connect(connect_address,
        try {
                input->clear();
  
-               FpsControl fps_control = { 0 };
+               FpsControl fps_control;
                f32 dtime;
                f32 wait_time = 0; // in seconds
  
-               fps_control.last_time = m_rendering_engine->get_timer_time();
+               fps_control.reset();
  
                while (m_rendering_engine->run()) {
  
-                       limitFps(&fps_control, &dtime);
+                       fps_control.limit(device, &dtime);
  
                        // Update client and server
                        client->step(dtime);
                                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;
                                wait_time += dtime;
                                // Only time out if we aren't waiting for the server we started
                                if (!start_data.address.empty() && wait_time > 10) {
-                                       *error_message = "Connection timed out.";
+                                       *error_message = gettext("Connection timed out.");
                                        errorstream << *error_message << std::endl;
                                        break;
                                }
@@@ -772,14 -1579,14 +774,14 @@@ bool Game::getServerContent(bool *abort
  {
        input->clear();
  
-       FpsControl fps_control = { 0 };
+       FpsControl fps_control;
        f32 dtime; // in seconds
  
-       fps_control.last_time = m_rendering_engine->get_timer_time();
+       fps_control.reset();
  
        while (m_rendering_engine->run()) {
  
-               limitFps(&fps_control, &dtime);
+               fps_control.limit(device, &dtime);
  
                // Update client and server
                client->step(dtime);
                        return false;
  
                if (client->getState() < LC_Init) {
-                       *error_message = "Client disconnected";
+                       *error_message = gettext("Client disconnected");
                        errorstream << *error_message << std::endl;
                        return false;
                }
@@@ -880,8 -1687,7 +882,7 @@@ inline void Game::updateInteractTimers(
  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;
@@@ -936,17 -1742,16 +937,16 @@@ void Game::processQueues(
  
  void Game::updateDebugState()
  {
-       bool has_basic_debug = client->checkPrivilege("basic_debug");
+       LocalPlayer *player = client->getEnv().getLocalPlayer();
        bool has_debug = client->checkPrivilege("debug");
+       bool has_basic_debug = has_debug || (player->hud_flags & HUD_FLAG_BASIC_DEBUG);
  
        if (m_game_ui->m_flags.show_basic_debug) {
-               if (!has_basic_debug) {
+               if (!has_basic_debug)
                        m_game_ui->m_flags.show_basic_debug = false;
-               }
        } else if (m_game_ui->m_flags.show_minimal_debug) {
-               if (has_basic_debug) {
+               if (has_basic_debug)
                        m_game_ui->m_flags.show_basic_debug = true;
-               }
        }
        if (!has_basic_debug)
                hud->disableBlockBounds();
@@@ -977,10 -1782,10 +977,10 @@@ void Game::updateProfilers(const RunSta
        }
  
        // Update update graphs
-       g_profiler->graphAdd("Time non-rendering [ms]",
+       g_profiler->graphAdd("Time non-rendering [us]",
                draw_times.busy_time - stats.drawtime);
  
-       g_profiler->graphAdd("Sleep [ms]", draw_times.sleep_time);
+       g_profiler->graphAdd("Sleep [us]", draw_times.sleep_time);
        g_profiler->graphAdd("FPS", 1.0f / dtime);
  }
  
@@@ -1013,9 -1818,9 +1013,9 @@@ void Game::updateStats(RunStats *stats
        /* Busytime average and jitter calculation
         */
        jp = &stats->busy_time_jitter;
-       jp->avg = jp->avg + draw_times.busy_time * 0.02;
+       jp->avg = jp->avg + draw_times.getBusyMs() * 0.02;
  
-       jitter = draw_times.busy_time - jp->avg;
+       jitter = draw_times.getBusyMs() - jp->avg;
  
        if (jitter > jp->max)
                jp->max = jitter;
@@@ -1052,6 -1857,7 +1052,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
  
  void Game::processKeyInput()
  {
 +      if (wasKeyDown(KeyType::SELECT_UP)) {
 +              m_cheat_menu->selectUp();
 +      } else if (wasKeyDown(KeyType::SELECT_DOWN)) {
 +              m_cheat_menu->selectDown();
 +      } else if (wasKeyDown(KeyType::SELECT_LEFT)) {
 +              m_cheat_menu->selectLeft();
 +      } else if (wasKeyDown(KeyType::SELECT_RIGHT)) {
 +              m_cheat_menu->selectRight();
 +      } else if (wasKeyDown(KeyType::SELECT_CONFIRM)) {
 +              m_cheat_menu->selectConfirm();
 +      }
 +
        if (wasKeyDown(KeyType::DROP)) {
                dropSelectedItem(isKeyDown(KeyType::SNEAK));
        } else if (wasKeyDown(KeyType::AUTOFORWARD)) {
                        toggleAutoforward();
        } else if (wasKeyDown(KeyType::INVENTORY)) {
                openInventory();
 +      } else if (wasKeyDown(KeyType::ENDERCHEST)) {
 +              openEnderchest();
        } else if (input->cancelPressed()) {
  #ifdef __ANDROID__
                m_android_chat_open = false;
                toggleFast();
        } else if (wasKeyDown(KeyType::NOCLIP)) {
                toggleNoClip();
 +      } else if (wasKeyDown(KeyType::KILLAURA)) {
 +              toggleKillaura();
 +      } else if (wasKeyDown(KeyType::FREECAM)) {
 +              toggleFreecam();
 +      } else if (wasKeyDown(KeyType::SCAFFOLD)) {
 +              toggleScaffold();
  #if USE_SOUND
        } else if (wasKeyDown(KeyType::MUTE)) {
                if (g_settings->getBool("enable_sound")) {
                m_game_ui->toggleChat();
        } else if (wasKeyDown(KeyType::TOGGLE_FOG)) {
                toggleFog();
 +      } else if (wasKeyDown(KeyType::TOGGLE_CHEAT_MENU)) {
 +              m_game_ui->toggleCheatMenu();
        } else if (wasKeyDown(KeyType::TOGGLE_UPDATE_CAMERA)) {
                toggleUpdateCamera();
        } else if (wasKeyDown(KeyType::TOGGLE_DEBUG)) {
@@@ -1290,29 -2074,24 +1291,36 @@@ void Game::openInventory(
        InventoryLocation inventoryloc;
        inventoryloc.setCurrentPlayer();
  
-       if (!client->modsLoaded()
-                       || !client->getScript()->on_inventory_open(fs_src->m_client->getInventory(inventoryloc))) {
-               TextDest *txt_dst = new TextDestPlayerInventory(client);
-               auto *&formspec = m_game_ui->updateFormspec("");
-               GUIFormSpecMenu::create(formspec, client, m_rendering_engine->get_gui_env(),
-                       &input->joystick, fs_src, txt_dst, client->getFormspecPrepend(), sound);
+       if (client->modsLoaded() && client->getScript()->on_inventory_open(fs_src->m_client->getInventory(inventoryloc))) {
+               delete fs_src;
+               return;
+       }
  
-               formspec->setFormSpec(fs_src->getForm(), inventoryloc);
+       if (fs_src->getForm().empty()) {
+               delete fs_src;
+               return;
        }
+       TextDest *txt_dst = new TextDestPlayerInventory(client);
+       auto *&formspec = m_game_ui->updateFormspec("");
+       GUIFormSpecMenu::create(formspec, client, m_rendering_engine->get_gui_env(),
+               &input->joystick, fs_src, txt_dst, client->getFormspecPrepend(), sound);
+       formspec->setFormSpec(fs_src->getForm(), inventoryloc);
  }
  
 +void Game::openEnderchest()
 +{
 +      LocalPlayer *player = client->getEnv().getLocalPlayer();
 +      if (!player || !player->getCAO())
 +              return;
 +
 +      infostream << "Game: Launching special inventory" << std::endl;
 +
 +      if (client->modsLoaded())
 +              client->getScript()->open_enderchest();
 +}
 +
  
  void Game::openConsole(float scale, const wchar_t *line)
  {
@@@ -1398,7 -2177,7 +1406,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
  }
@@@ -1420,42 -2199,6 +1428,42 @@@ void Game::toggleNoClip(
        }
  }
  
 +void Game::toggleKillaura()
 +{
 +      bool killaura = ! g_settings->getBool("killaura");
 +      g_settings->set("killaura", bool_to_cstr(killaura));
 +
 +      if (killaura) {
 +              m_game_ui->showTranslatedStatusText("Killaura enabled");
 +      } else {
 +              m_game_ui->showTranslatedStatusText("Killaura disabled");
 +      }
 +}
 +
 +void Game::toggleFreecam()
 +{
 +      bool freecam = ! g_settings->getBool("freecam");
 +      g_settings->set("freecam", bool_to_cstr(freecam));
 +
 +      if (freecam) {
 +              m_game_ui->showTranslatedStatusText("Freecam enabled");
 +      } else {
 +              m_game_ui->showTranslatedStatusText("Freecam disabled");
 +      }
 +}
 +
 +void Game::toggleScaffold()
 +{
 +      bool scaffold = ! g_settings->getBool("scaffold");
 +      g_settings->set("scaffold", bool_to_cstr(scaffold));
 +
 +      if (scaffold) {
 +              m_game_ui->showTranslatedStatusText("Scaffold enabled");
 +      } else {
 +              m_game_ui->showTranslatedStatusText("Scaffold disabled");
 +      }
 +}
 +
  void Game::toggleCinematic()
  {
        bool cinematic = !g_settings->getBool("cinematic");
  
  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)");
+       LocalPlayer *player = client->getEnv().getLocalPlayer();
+       if (!(client->checkPrivilege("debug") || (player->hud_flags & HUD_FLAG_BASIC_DEBUG))) {
+               m_game_ui->showTranslatedStatusText("Can't show block bounds (disabled by mod or game)");
+               return;
+       }
+       enum Hud::BlockBoundsMode newmode = hud->toggleBlockBounds();
+       switch (newmode) {
+               case Hud::BLOCK_BOUNDS_OFF:
+                       m_game_ui->showTranslatedStatusText("Block bounds hidden");
+                       break;
+               case Hud::BLOCK_BOUNDS_CURRENT:
+                       m_game_ui->showTranslatedStatusText("Block bounds shown for current block");
+                       break;
+               case Hud::BLOCK_BOUNDS_NEAR:
+                       m_game_ui->showTranslatedStatusText("Block bounds shown for nearby blocks");
+                       break;
+               case Hud::BLOCK_BOUNDS_MAX:
+                       m_game_ui->showTranslatedStatusText("Block bounds shown for all blocks");
+                       break;
+               default:
+                       break;
        }
  }
  
@@@ -1556,6 -2299,9 +1564,9 @@@ void Game::toggleFog(
  
  void Game::toggleDebug()
  {
+       LocalPlayer *player = client->getEnv().getLocalPlayer();
+       bool has_debug = client->checkPrivilege("debug");
+       bool has_basic_debug = has_debug || (player->hud_flags & HUD_FLAG_BASIC_DEBUG);
        // Initial: No debug info
        // 1x toggle: Debug text
        // 2x toggle: Debug text with profiler graph
        // 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,
+       // Basic mode is used when player has the debug HUD flag set,
        // otherwise the Minimal mode is used.
        if (!m_game_ui->m_flags.show_minimal_debug) {
                m_game_ui->m_flags.show_minimal_debug = true;
-               if (client->checkPrivilege("basic_debug")) {
+               if (has_basic_debug)
                        m_game_ui->m_flags.show_basic_debug = true;
-               }
                m_game_ui->m_flags.show_profiler_graph = false;
                draw_control->show_wireframe = 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")) {
+               if (has_basic_debug)
                        m_game_ui->m_flags.show_basic_debug = true;
-               }
                m_game_ui->m_flags.show_profiler_graph = true;
                m_game_ui->showTranslatedStatusText("Profiler graph shown");
        } else if (!draw_control->show_wireframe && client->checkPrivilege("debug")) {
-               if (client->checkPrivilege("basic_debug")) {
+               if (has_basic_debug)
                        m_game_ui->m_flags.show_basic_debug = true;
-               }
                m_game_ui->m_flags.show_profiler_graph = false;
                draw_control->show_wireframe = true;
                m_game_ui->showTranslatedStatusText("Wireframe shown");
                m_game_ui->m_flags.show_basic_debug = false;
                m_game_ui->m_flags.show_profiler_graph = false;
                draw_control->show_wireframe = false;
-               if (client->checkPrivilege("debug")) {
+               if (has_debug) {
                        m_game_ui->showTranslatedStatusText("Debug info, profiler graph, and wireframe hidden");
                } else {
                        m_game_ui->showTranslatedStatusText("Debug info and profiler graph hidden");
  
  void Game::toggleUpdateCamera()
  {
 +      if (g_settings->getBool("freecam"))
 +              return;
        m_flags.disable_camera_update = !m_flags.disable_camera_update;
        if (m_flags.disable_camera_update)
                m_game_ui->showTranslatedStatusText("Camera update disabled");
@@@ -1755,6 -2496,10 +1763,10 @@@ void Game::updatePlayerControl(const Ca
        //TimeTaker tt("update player control", NULL, PRECISION_NANO);
  
        PlayerControl control(
+               isKeyDown(KeyType::FORWARD),
+               isKeyDown(KeyType::BACKWARD),
+               isKeyDown(KeyType::LEFT),
+               isKeyDown(KeyType::RIGHT),
                isKeyDown(KeyType::JUMP) || player->getAutojump(),
                isKeyDown(KeyType::AUX1),
                isKeyDown(KeyType::SNEAK),
                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) {
        }
  #endif
  
-       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)
-               );
-       // Set direction keys to ensure mod compatibility
-       if (control.movement_speed > 0.001f) {
-               float absolute_direction;
-               // 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);
-       player->keyPressed = keypress_bits;
  
        //tt.stop();
  }
@@@ -1904,7 -2617,7 +1884,7 @@@ void Game::handleClientEvent_PlayerDama
                LocalPlayer *player = client->getEnv().getLocalPlayer();
  
                f32 hp_max = player->getCAO() ?
 -                      player->getCAO()->getProperties().hp_max : PLAYER_MAX_HP_DEFAULT;
 +                      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;
@@@ -1966,19 -2679,11 +1946,19 @@@ void Game::handleClientEvent_ShowFormSp
  
  void Game::handleClientEvent_ShowLocalFormSpec(ClientEvent *event, CameraOrientation *cam)
  {
 -      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, m_rendering_engine->get_gui_env(),
 -                      &input->joystick, fs_src, txt_dst, client->getFormspecPrepend(), sound);
 +      if (event->show_formspec.formspec->empty()) {
 +              auto formspec = m_game_ui->getFormspecGUI();
 +              if (formspec && (event->show_formspec.formname->empty()
 +                              || *(event->show_formspec.formname) == m_game_ui->getFormspecName())) {
 +                      formspec->quitMenu();
 +              }
 +      } else {
 +              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, 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;
@@@ -2172,7 -2877,7 +2152,7 @@@ void Game::handleClientEvent_SetMoon(Cl
  void Game::handleClientEvent_SetStars(ClientEvent *event, CameraOrientation *cam)
  {
        sky->setStarsVisible(event->star_params->visible);
-       sky->setStarCount(event->star_params->count, false);
+       sky->setStarCount(event->star_params->count);
        sky->setStarColor(event->star_params->starcolor);
        sky->setStarScale(event->star_params->scale);
        delete event->star_params;
@@@ -2209,7 -2914,7 +2189,7 @@@ void Game::processClientEvents(CameraOr
        }
  }
  
- void Game::updateChat(f32 dtime, const v2u32 &screensize)
+ void Game::updateChat(f32 dtime)
  {
        // Get new messages from error log buffer
        while (!m_chat_log_buf.empty())
        chat_backend->step(dtime);
  
        // Display all messages in a static text element
-       m_game_ui->setChatText(chat_backend->getRecentChat(),
-               chat_backend->getRecentBuffer().getLineCount());
+       auto &buf = chat_backend->getRecentBuffer();
+       if (buf.getLinesModified()) {
+               buf.resetLinesModified();
+               m_game_ui->setChatText(chat_backend->getRecentChat(), buf.getLineCount());
+       }
+       // Make sure that the size is still correct
+       m_game_ui->updateChatSize();
  }
  
- void Game::updateCamera(u32 busy_time, f32 dtime)
+ void Game::updateCamera(f32 dtime)
  {
        LocalPlayer *player = client->getEnv().getLocalPlayer();
  
  
        v3s16 old_camera_offset = camera->getOffset();
  
 -      if (wasKeyDown(KeyType::CAMERA_MODE)) {
 -              GenericCAO *playercao = player->getCAO();
 -
 -              // If playercao not loaded, don't change camera
 -              if (!playercao)
 -                      return;
 -
 +      if (wasKeyDown(KeyType::CAMERA_MODE) && ! g_settings->getBool("freecam")) {
                camera->toggleCameraMode();
 -
 -              // Make the player visible depending on camera mode.
 -              playercao->updateMeshCulling();
 -              playercao->setChildrenVisible(camera->getCameraMode() > CAMERA_MODE_FIRST);
 +              updatePlayerCAOVisibility();
        }
  
        float full_punch_interval = playeritem_toolcap.full_punch_interval;
        float tool_reload_ratio = runData.time_from_last_punch / full_punch_interval;
  
        tool_reload_ratio = MYMIN(tool_reload_ratio, 1.0);
-       camera->update(player, dtime, busy_time / 1000.0f, tool_reload_ratio);
+       camera->update(player, dtime, tool_reload_ratio);
        camera->step(dtime);
  
        v3f camera_position = camera->getPosition();
        }
  }
  
 +void Game::updatePlayerCAOVisibility()
 +{
 +      // Make the player visible depending on camera mode.
 +      LocalPlayer *player = client->getEnv().getLocalPlayer();
 +      GenericCAO *playercao = player->getCAO();
 +      if (!playercao)
 +              return;
 +      playercao->updateMeshCulling();
 +      bool is_visible = camera->getCameraMode() > CAMERA_MODE_FIRST || g_settings->getBool("freecam");
 +      playercao->setChildrenVisible(is_visible);
 +}
  
  void Game::updateSound(f32 dtime)
  {
  }
  
  
- void Game::processPlayerInteraction(f32 dtime, bool show_hud, bool show_debug)
+ void Game::processPlayerInteraction(f32 dtime, bool show_hud)
  {
        LocalPlayer *player = client->getEnv().getLocalPlayer();
  
        const ItemDefinition &selected_def = selected_item.getDefinition(itemdef_manager);
        f32 d = getToolRange(selected_def, hand_item.getDefinition(itemdef_manager));
  
 +
 +      if (g_settings->getBool("reach"))
 +              d += g_settings->getU16("tool_range");
 +
        core::line3d<f32> shootline;
  
        switch (camera->getCameraMode()) {
                        !runData.btn_down_for_dig,
                        camera_offset);
  
-       if (pointed != runData.pointed_old) {
+       if (pointed != runData.pointed_old)
                infostream << "Pointing at " << pointed.dump() << std::endl;
-               hud->updateSelectionMesh(camera_offset);
-       }
+       // Note that updating the selection mesh every frame is not particularly efficient,
+       // but the halo rendering code is already inefficient so there's no point in optimizing it here
+       hud->updateSelectionMesh(camera_offset);
  
        // Allow digging again if button is not pressed
        if (runData.digging_blocked && !isKeyDown(KeyType::DIG))
        soundmaker->m_player_leftpunch_sound.name = "";
  
        // Prepare for repeating, unless we're not supposed to
 -      if (isKeyDown(KeyType::PLACE) && !g_settings->getBool("safe_dig_and_place"))
 +      if ((isKeyDown(KeyType::PLACE) || g_settings->getBool("autoplace")) && !g_settings->getBool("safe_dig_and_place"))
                runData.repeat_place_timer += dtime;
        else
                runData.repeat_place_timer = 0;
                handlePointingAtNode(pointed, selected_item, hand_item, dtime);
        } else if (pointed.type == POINTEDTHING_OBJECT) {
                v3f player_position  = player->getPosition();
-               handlePointingAtObject(pointed, tool_item, player_position, show_debug);
+               bool basic_debug_allowed = client->checkPrivilege("debug") || (player->hud_flags & HUD_FLAG_BASIC_DEBUG);
+               handlePointingAtObject(pointed, tool_item, player_position,
+                               m_game_ui->m_flags.show_basic_debug && basic_debug_allowed);
        } else if (isKeyDown(KeyType::DIG)) {
                // When button is held down in air, show continuous animation
                runData.punching = true;
@@@ -2503,7 -3212,8 +2493,7 @@@ PointedThing Game::updatePointedThing
  
        runData.selected_object = NULL;
        hud->pointing_at_object = false;
 -
 -      RaycastState s(shootline, look_for_object, liquids_pointable);
 +      RaycastState s(shootline, look_for_object, liquids_pointable, ! g_settings->getBool("dont_point_nodes"));
        PointedThing result;
        env.continueRaycast(&s, &result);
        if (result.type == POINTEDTHING_OBJECT) {
        return result;
  }
  
 -
  void Game::handlePointingAtNothing(const ItemStack &playerItem)
  {
        infostream << "Attempted to place item while pointing at nothing" << std::endl;
@@@ -2598,10 -3309,9 +2588,10 @@@ void Game::handlePointingAtNode(const P
  
        ClientMap &map = client->getEnv().getClientMap();
  
 -      if (runData.nodig_delay_timer <= 0.0 && isKeyDown(KeyType::DIG)
 +      if (((runData.nodig_delay_timer <= 0.0 || g_settings->getBool("fastdig")) && (isKeyDown(KeyType::DIG) || g_settings->getBool("autodig"))
                        && !runData.digging_blocked
 -                      && client->checkPrivilege("interact")) {
 +                      && client->checkPrivilege("interact"))
 +              ) {
                handleDigging(pointed, nodepos, selected_item, hand_item, dtime);
        }
  
        } 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 ((wasKeyPressed(KeyType::PLACE) ||
 -                      runData.repeat_place_timer >= m_repeat_place_time) &&
 +                      (runData.repeat_place_timer >= (g_settings->getBool("fastplace") ? 0.001 : m_repeat_place_time))) &&
                        client->checkPrivilege("interact")) {
                runData.repeat_place_timer = 0;
                infostream << "Place button pressed while looking at ground" << std::endl;
  
  bool Game::nodePlacement(const ItemDefinition &selected_def,
        const ItemStack &selected_item, const v3s16 &nodepos, const v3s16 &neighbourpos,
 -      const PointedThing &pointed, const NodeMetadata *meta)
 +      const PointedThing &pointed, const NodeMetadata *meta, bool force)
  {
        const auto &prediction = selected_def.node_placement_prediction;
  
  
        // formspec in meta
        if (meta && !meta->getString("formspec").empty() && !input->isRandom()
 -                      && !isKeyDown(KeyType::SNEAK)) {
 +                      && !isKeyDown(KeyType::SNEAK) && !force) {
                // on_rightclick callbacks are called anyway
                if (nodedef_manager->get(map.getNode(nodepos)).rightclickable)
                        client->interact(INTERACT_PLACE, pointed);
  
        // on_rightclick callback
        if (prediction.empty() || (nodedef->get(node).rightclickable &&
 -                      !isKeyDown(KeyType::SNEAK))) {
 +                      !isKeyDown(KeyType::SNEAK) && !force)) {
                // Report to server
                client->interact(INTERACT_PLACE, pointed);
                return false;
@@@ -2854,11 -3563,11 +2843,11 @@@ void Game::handlePointingAtObject(cons
  
        m_game_ui->setInfoText(infotext);
  
 -      if (isKeyDown(KeyType::DIG)) {
 +      if (isKeyDown(KeyType::DIG) || g_settings->getBool("autohit")) {
                bool do_punch = false;
                bool do_punch_damage = false;
  
 -              if (runData.object_hit_delay_timer <= 0.0) {
 +              if (runData.object_hit_delay_timer <= 0.0 || g_settings->getBool("spamclick")) {
                        do_punch = true;
                        do_punch_damage = true;
                        runData.object_hit_delay_timer = object_hit_delay;
                                        dir, &tool_item, runData.time_from_last_punch);
                        runData.time_from_last_punch = 0;
  
 -                      if (!disable_send)
 +                      if (!disable_send) {
                                client->interact(INTERACT_START_DIGGING, pointed);
 +                      }
                }
        } else if (wasKeyDown(KeyType::PLACE)) {
                infostream << "Pressed place button while pointing at object" << std::endl;
@@@ -2904,7 -3612,8 +2893,8 @@@ void Game::handleDigging(const PointedT
        // 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) {
                }
        }
  
 +      if(g_settings->getBool("instant_break")) {
 +              runData.dig_time_complete = 0;
 +              runData.dig_instantly = true;
 +      }
        if (!runData.digging) {
                infostream << "Started digging" << std::endl;
                runData.dig_instantly = runData.dig_time_complete == 0;
@@@ -3057,7 -3762,7 +3047,7 @@@ void Game::updateFrame(ProfilerGraph *g
        float direct_brightness;
        bool sunlight_seen;
  
 -      if (m_cache_enable_noclip && m_cache_enable_free_move) {
 +      if ((m_cache_enable_noclip && m_cache_enable_free_move) || g_settings->getBool("freecam")) {
                direct_brightness = time_brightness;
                sunlight_seen = true;
        } else {
        }
  
        /*
-               Get chat messages from client
+               Damage camera tilt
        */
+       if (player->hurt_tilt_timer > 0.0f) {
+               player->hurt_tilt_timer -= dtime * 6.0f;
  
-       v2u32 screensize = driver->getScreenSize();
 -              if (player->hurt_tilt_timer < 0.0f)
++              if (player->hurt_tilt_timer < 0.0f || g_settings->getBool("no_hurt_cam"))
+                       player->hurt_tilt_strength = 0.0f;
+       }
+       /*
+               Update minimap pos and rotation
+       */
+       if (mapper && m_game_ui->m_flags.show_hud) {
+               mapper->setPos(floatToInt(player->getPosition(), BS));
+               mapper->setAngle(player->getYaw());
+       }
+       /*
+               Get chat messages from client
+       */
  
-       updateChat(dtime, screensize);
+       updateChat(dtime);
  
        /*
                Inventory
        v3f camera_direction = camera->getDirection();
        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;
        } while (false);
  
        /*
-               Drawing begins
+               ==================== Drawing begins ====================
        */
-       const video::SColor &skycolor = sky->getSkyColor();
+       const video::SColor skycolor = sky->getSkyColor();
  
-       TimeTaker tt_draw("Draw scene");
+       TimeTaker tt_draw("Draw scene", nullptr, PRECISION_MICRO);
        driver->beginScene(true, true, skycolor);
  
        bool draw_wield_tool = (m_game_ui->m_flags.show_hud &&
        /*
                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());
  
 +      /*
 +              Cheat menu
 +      */
 +
 +      if (! gui_chat_console->isOpen()) {
 +              if (m_game_ui->m_flags.show_cheat_menu)
 +                      m_cheat_menu->draw(driver, m_game_ui->m_flags.show_minimal_debug);
 +              if (g_settings->getBool("cheat_hud"))
 +                      m_cheat_menu->drawHUD(driver, dtime);
 +      }
        /*
                Damage flash
        */
        if (runData.damage_flash > 0.0f) {
                video::SColor color(runData.damage_flash, 180, 0, 0);
 -              driver->draw2DRectangle(color,
 -                                      core::rect<s32>(0, 0, screensize.X, screensize.Y),
 -                                      NULL);
 +              if (! g_settings->getBool("no_hurt_cam"))
 +                      driver->draw2DRectangle(color, core::rect<s32>(0, 0, screensize.X, screensize.Y), NULL);
  
                runData.damage_flash -= 384.0f * dtime;
        }
  
-       /*
-               Damage camera tilt
-       */
-       if (player->hurt_tilt_timer > 0.0f) {
-               player->hurt_tilt_timer -= dtime * 6.0f;
-               if (player->hurt_tilt_timer < 0.0f || g_settings->getBool("no_hurt_cam"))
-                       player->hurt_tilt_strength = 0.0f;
-       }
-       /*
-               Update minimap pos and rotation
-       */
-       if (mapper && m_game_ui->m_flags.show_hud) {
-               mapper->setPos(floatToInt(player->getPosition(), BS));
-               mapper->setAngle(player->getYaw());
-       }
        /*
 -              ==================== End scene ====================
 +              End scene
        */
+ #if IRRLICHT_VERSION_MT_REVISION < 5
        if (++m_reset_HW_buffer_counter > 500) {
                /*
                  Periodically remove all mesh HW buffers.
                driver->removeAllHardwareBuffers();
                m_reset_HW_buffer_counter = 0;
        }
+ #endif
        driver->endScene();
  
        stats->drawtime = tt_draw.stop(true);
-       g_profiler->avg("Game::updateFrame(): draw scene [ms]", stats->drawtime);
-       g_profiler->graphAdd("Update frame [ms]", tt_update.stop(true));
+       g_profiler->graphAdd("Draw scene [us]", stats->drawtime);
+       g_profiler->avg("Game::updateFrame(): update frame [ms]", tt_update.stop(true));
  }
  
  /* Log times and stuff for visualization */
@@@ -3346,7 -4045,12 +3339,12 @@@ void Game::updateShadows(
  
        float in_timeofday = fmod(runData.time_of_day_smooth, 1.0f);
  
-       float timeoftheday = fmod(getWickedTimeOfDay(in_timeofday) + 0.75f, 0.5f) + 0.25f;
+       float timeoftheday = getWickedTimeOfDay(in_timeofday);
+       bool is_day = timeoftheday > 0.25 && timeoftheday < 0.75;
+       bool is_shadow_visible = is_day ? sky->getSunVisible() : sky->getMoonVisible();
+       shadow->setShadowIntensity(is_shadow_visible ? client->getEnv().getLocalPlayer()->getLighting().shadow_intensity : 0.0f);
+       timeoftheday = fmod(timeoftheday + 0.75f, 0.5f) + 0.25f;
        const float offset_constant = 10000.0f;
  
        v3f light(0.0f, 0.0f, -1.0f);
   Misc
   ****************************************************************************/
  
- /* On some computers framerate doesn't seem to be automatically limited
-  */
- inline void Game::limitFps(FpsControl *fps_timings, f32 *dtime)
+ void FpsControl::reset()
  {
-       // not using getRealTime is necessary for wine
-       device->getTimer()->tick(); // Maker sure device time is up-to-date
-       u32 time = device->getTimer()->getTime();
-       u32 last_time = fps_timings->last_time;
-       if (time > last_time)  // Make sure time hasn't overflowed
-               fps_timings->busy_time = time - last_time;
-       else
-               fps_timings->busy_time = 0;
+       last_time = porting::getTimeUs();
+ }
  
-       u32 frametime_min = 1000 / (
+ /*
+  * On some computers framerate doesn't seem to be automatically limited
+  */
+ void FpsControl::limit(IrrlichtDevice *device, f32 *dtime)
+ {
+       const u64 frametime_min = 1000000.0f / (
                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;
-               device->sleep(fps_timings->sleep_time);
+       u64 time = porting::getTimeUs();
+       if (time > last_time) // Make sure time hasn't overflowed
+               busy_time = time - last_time;
+       else
+               busy_time = 0;
+       if (busy_time < frametime_min) {
+               sleep_time = frametime_min - busy_time;
+               if (sleep_time > 1000)
+                       sleep_ms(sleep_time / 1000);
        } else {
-               fps_timings->sleep_time = 0;
+               sleep_time = 0;
        }
  
-       /* Get the new value of the device timer. Note that device->sleep() may
-        * not sleep for the entire requested time as sleep may be interrupted and
-        * therefore it is arguably more accurate to get the new time from the
-        * device rather than calculating it by adding sleep_time to time.
-        */
-       device->getTimer()->tick(); // Update device timer
-       time = device->getTimer()->getTime();
+       // Read the timer again to accurately determine how long we actually slept,
+       // rather than calculating it by adding sleep_time to time.
+       time = porting::getTimeUs();
  
-       if (time > last_time)  // Make sure last_time hasn't overflowed
-               *dtime = (time - last_time) / 1000.0;
+       if (time > last_time) // Make sure last_time hasn't overflowed
+               *dtime = (time - last_time) / 1000000.0f;
        else
                *dtime = 0;
  
-       fps_timings->last_time = time;
+       last_time = time;
  }
  
  void Game::showOverlayMessage(const char *msg, float dtime, int percent, bool draw_clouds)
@@@ -3424,24 -4127,6 +3421,24 @@@ void Game::settingChangedCallback(cons
        ((Game *)data)->readSettings();
  }
  
 +void Game::updateAllMapBlocksCallback(const std::string &setting_name, void *data)
 +{
 +      ((Game *) data)->client->updateAllMapBlocks();
 +}
 +
 +void Game::freecamChangedCallback(const std::string &setting_name, void *data)
 +{
 +      Game *game = (Game *) data;
 +      LocalPlayer *player = game->client->getEnv().getLocalPlayer();
 +      if (g_settings->getBool("freecam")) {
 +              game->camera->setCameraMode(CAMERA_MODE_FIRST);
 +              player->freecamEnable();
 +      } else {
 +              player->freecamDisable();
 +      }
 +      game->updatePlayerCAOVisibility();
 +}
 +
  void Game::readSettings()
  {
        m_cache_doubletap_jump               = g_settings->getBool("doubletap_jump");
        m_does_lost_focus_pause_game = g_settings->getBool("pause_on_lost_focus");
  }
  
 +bool Game::isKeyDown(GameKeyType k)
 +{
 +      return input->isKeyDown(k);
 +}
 +
 +bool Game::wasKeyDown(GameKeyType k)
 +{
 +      return input->wasKeyDown(k);
 +}
 +
 +bool Game::wasKeyPressed(GameKeyType k)
 +{
 +      return input->wasKeyPressed(k);
 +}
 +
 +bool Game::wasKeyReleased(GameKeyType k)
 +{
 +      return input->wasKeyReleased(k);
 +}
 +
  /****************************************************************************/
  /****************************************************************************
   Shutdown / cleanup
@@@ -3522,7 -4187,7 +3519,7 @@@ void Game::showDeathFormspec(
  #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"
                "- %s: sneak/climb down\n"
                "- %s: drop item\n"
                "- %s: inventory\n"
 +              "- %s: enderchest\n"
                "- Mouse: turn/look\n"
                "- Mouse wheel: select item\n"
                "- %s: chat\n"
 +              "- %s: Killaura\n"
 +              "- %s: Freecam\n"
 +              "- %s: Scaffold\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_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)
 -      );
 +       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_enderchest),
 +                      GET_KEY_NAME(keymap_chat),
 +                      GET_KEY_NAME(keymap_toggle_killaura),
 +                      GET_KEY_NAME(keymap_toggle_freecam),
 +                      GET_KEY_NAME(keymap_toggle_scaffold)
 +                      );
  
        std::string control_text = std::string(control_text_buf);
        str_formspec_escape(control_text);
        if (simple_singleplayer_mode || address.empty()) {
                static const std::string on = strgettext("On");
                static const std::string off = strgettext("Off");
-               const std::string &damage = g_settings->getBool("enable_damage") ? on : off;
-               const std::string &creative = g_settings->getBool("creative_mode") ? on : off;
+               // Note: Status of enable_damage and creative_mode settings is intentionally
+               // NOT shown here because the game might roll its own damage system and/or do
+               // a per-player Creative Mode, in which case writing it here would mislead.
+               bool damage = g_settings->getBool("enable_damage");
                const std::string &announced = g_settings->getBool("server_announce") ? on : off;
-               os << strgettext("- Damage: ") << damage << "\n"
-                               << strgettext("- Creative Mode: ") << creative << "\n";
                if (!simple_singleplayer_mode) {
-                       const std::string &pvp = g_settings->getBool("enable_pvp") ? on : off;
-                       //~ PvP = Player versus Player
-                       os << strgettext("- PvP: ") << pvp << "\n"
-                                       << strgettext("- Public: ") << announced << "\n";
+                       if (damage) {
+                               const std::string &pvp = g_settings->getBool("enable_pvp") ? on : off;
+                               //~ PvP = Player versus Player
+                               os << strgettext("- PvP: ") << pvp << "\n";
+                       }
+                       os << strgettext("- Public: ") << announced << "\n";
                        std::string server_name = g_settings->get("server_name");
                        str_formspec_escape(server_name);
                        if (announced == on && !server_name.empty())
   ****************************************************************************/
  /****************************************************************************/
  
 +Game *g_game;
 +
  void the_game(bool *kill,
                InputHandler *input,
                RenderingEngine *rendering_engine,
  {
        Game game;
  
 +      g_game = &game;
 +
        /* Make a copy of the server address because if a local singleplayer server
         * is created then this is updated and we don't want to change the value
         * passed to us by the calling function
                }
  
        } 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;
diff --combined src/client/game.h
index cb40d48904639613ef8ded00282b39137e02ca3a,d87e747c58a009f2ac550d4ce4a0b79d01a0821b..0e5d0550d296b156acd1652b7bfcf205197b5a95
@@@ -19,59 -19,6 +19,59 @@@ with this program; if not, write to th
  
  #pragma once
  
 +#include <iomanip>
 +#include <cmath>
 +#include "client/renderingengine.h"
 +#include "camera.h"
 +#include "client.h"
 +#include "client/clientevent.h"
 +//#include "client/gameui.h"
 +#include "client/inputhandler.h"
 +#include "client/sound.h"
 +#include "client/tile.h"     // For TextureSource
 +#include "client/keys.h"
 +#include "client/joystick_controller.h"
 +#include "clientmap.h"
 +#include "clouds.h"
 +#include "config.h"
 +#include "content_cao.h"
 +#include "client/event_manager.h"
 +#include "fontengine.h"
 +#include "itemdef.h"
 +#include "log.h"
 +#include "filesys.h"
 +#include "gettext.h"
 +#include "gui/cheatMenu.h"
 +#include "gui/guiChatConsole.h"
 +#include "gui/guiConfirmRegistration.h"
 +#include "gui/guiFormSpecMenu.h"
 +#include "gui/guiKeyChangeMenu.h"
 +#include "gui/guiPasswordChange.h"
 +#include "gui/guiVolumeChange.h"
 +#include "gui/mainmenumanager.h"
 +#include "gui/profilergraph.h"
 +#include "mapblock.h"
 +#include "minimap.h"
 +#include "nodedef.h"         // Needed for determining pointing to nodes
 +#include "nodemetadata.h"
 +#include "particles.h"
 +#include "porting.h"
 +#include "profiler.h"
 +#include "raycast.h"
 +#include "server.h"
 +#include "settings.h"
 +#include "shader.h"
 +#include "sky.h"
 +#include "translation.h"
 +#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"
  #include "irrlichttypes.h"
  #include <string>
  
@@@ -86,7 -33,7 +86,7 @@@ struct Jitter 
  };
  
  struct RunStats {
-       u32 drawtime;
+       u64 drawtime; // (us)
  
        Jitter dtime_jitter, busy_time_jitter;
  };
@@@ -96,824 -43,6 +96,836 @@@ struct CameraOrientation 
        f32 camera_pitch;  // "up/down"
  };
  
- #ifdef __ANDROID__
 +/*
 +      Text input system
 +*/
 +
 +struct TextDestNodeMetadata : public TextDest
 +{
 +      TextDestNodeMetadata(v3s16 p, Client *client)
 +      {
 +              m_p = p;
 +              m_client = client;
 +      }
 +      // This is deprecated I guess? -celeron55
 +      void gotText(const std::wstring &text)
 +      {
 +              std::string ntext = wide_to_utf8(text);
 +              infostream << "Submitting 'text' field of node at (" << m_p.X << ","
 +                         << m_p.Y << "," << m_p.Z << "): " << ntext << std::endl;
 +              StringMap fields;
 +              fields["text"] = ntext;
 +              m_client->sendNodemetaFields(m_p, "", fields);
 +      }
 +      void gotText(const StringMap &fields)
 +      {
 +              m_client->sendNodemetaFields(m_p, "", fields);
 +      }
 +
 +      v3s16 m_p;
 +      Client *m_client;
 +};
 +
 +struct TextDestPlayerInventory : public TextDest
 +{
 +      TextDestPlayerInventory(Client *client)
 +      {
 +              m_client = client;
 +              m_formname = "";
 +      }
 +      TextDestPlayerInventory(Client *client, const std::string &formname)
 +      {
 +              m_client = client;
 +              m_formname = formname;
 +      }
 +      void gotText(const StringMap &fields)
 +      {
 +              m_client->sendInventoryFields(m_formname, fields);
 +      }
 +
 +      Client *m_client;
 +};
 +
 +struct LocalFormspecHandler : public TextDest
 +{
 +      LocalFormspecHandler(const std::string &formname)
 +      {
 +              m_formname = formname;
 +      }
 +
 +      LocalFormspecHandler(const std::string &formname, Client *client):
 +              m_client(client)
 +      {
 +              m_formname = formname;
 +      }
 +
 +      void gotText(const StringMap &fields)
 +      {
 +              if (m_formname == "MT_PAUSE_MENU") {
 +                      if (fields.find("btn_sound") != fields.end()) {
 +                              g_gamecallback->changeVolume();
 +                              return;
 +                      }
 +
 +                      if (fields.find("btn_key_config") != fields.end()) {
 +                              g_gamecallback->keyConfig();
 +                              return;
 +                      }
 +
 +                      if (fields.find("btn_exit_menu") != fields.end()) {
 +                              g_gamecallback->disconnect();
 +                              return;
 +                      }
 +
 +                      if (fields.find("btn_exit_os") != fields.end()) {
 +                              g_gamecallback->exitToOS();
 +#ifndef __ANDROID__
 +                              RenderingEngine::get_raw_device()->closeDevice();
 +#endif
 +                              return;
 +                      }
 +
 +                      if (fields.find("btn_change_password") != fields.end()) {
 +                              g_gamecallback->changePassword();
 +                              return;
 +                      }
 +
 +                      return;
 +              }
 +
 +              if (m_formname == "MT_DEATH_SCREEN") {
 +                      assert(m_client != 0);
 +                      m_client->sendRespawn();
 +                      return;
 +              }
 +
 +              if (m_client->modsLoaded())
 +                      m_client->getScript()->on_formspec_input(m_formname, fields);
 +      }
 +
 +      Client *m_client = nullptr;
 +};
 +
 +/* Form update callback */
 +
 +class NodeMetadataFormSource: public IFormSource
 +{
 +public:
 +      NodeMetadataFormSource(ClientMap *map, v3s16 p):
 +              m_map(map),
 +              m_p(p)
 +      {
 +      }
 +      const std::string &getForm() const
 +      {
 +              static const std::string empty_string = "";
 +              NodeMetadata *meta = m_map->getNodeMetadata(m_p);
 +
 +              if (!meta)
 +                      return empty_string;
 +
 +              return meta->getString("formspec");
 +      }
 +
 +      virtual std::string resolveText(const std::string &str)
 +      {
 +              NodeMetadata *meta = m_map->getNodeMetadata(m_p);
 +
 +              if (!meta)
 +                      return str;
 +
 +              return meta->resolveString(str);
 +      }
 +
 +      ClientMap *m_map;
 +      v3s16 m_p;
 +};
 +
 +class PlayerInventoryFormSource: public IFormSource
 +{
 +public:
 +      PlayerInventoryFormSource(Client *client):
 +              m_client(client)
 +      {
 +      }
 +
 +      const std::string &getForm() const
 +      {
 +              LocalPlayer *player = m_client->getEnv().getLocalPlayer();
 +              return player->inventory_formspec;
 +      }
 +
 +      Client *m_client;
 +};
 +
 +class NodeDugEvent: public MtEvent
 +{
 +public:
 +      v3s16 p;
 +      MapNode n;
 +
 +      NodeDugEvent(v3s16 p, MapNode n):
 +              p(p),
 +              n(n)
 +      {}
 +      MtEvent::Type getType() const
 +      {
 +              return MtEvent::NODE_DUG;
 +      }
 +};
 +
 +class SoundMaker
 +{
 +      ISoundManager *m_sound;
 +      const NodeDefManager *m_ndef;
 +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;
 +      SimpleSoundSpec m_player_rightpunch_sound;
 +
 +      SoundMaker(ISoundManager *sound, const NodeDefManager *ndef):
 +              m_sound(sound),
 +              m_ndef(ndef),
 +              makes_footstep_sound(true),
 +              m_player_step_timer(0.0f),
 +              m_player_jump_timer(0.0f)
 +      {
 +      }
 +
 +      void playPlayerStep()
 +      {
 +              if (m_player_step_timer <= 0 && m_player_step_sound.exists()) {
 +                      m_player_step_timer = 0.03;
 +                      if (makes_footstep_sound)
 +                              m_sound->playSound(m_player_step_sound, false);
 +              }
 +      }
 +
 +      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;
 +              sm->playPlayerStep();
 +      }
 +
 +      static void playerRegainGround(MtEvent *e, void *data)
 +      {
 +              SoundMaker *sm = (SoundMaker *)data;
 +              sm->playPlayerStep();
 +      }
 +
 +      static void playerJump(MtEvent *e, void *data)
 +      {
 +              SoundMaker *sm = (SoundMaker *)data;
 +              sm->playPlayerJump();
 +      }
 +
 +      static void cameraPunchLeft(MtEvent *e, void *data)
 +      {
 +              SoundMaker *sm = (SoundMaker *)data;
 +              sm->m_sound->playSound(sm->m_player_leftpunch_sound, false);
 +      }
 +
 +      static void cameraPunchRight(MtEvent *e, void *data)
 +      {
 +              SoundMaker *sm = (SoundMaker *)data;
 +              sm->m_sound->playSound(sm->m_player_rightpunch_sound, false);
 +      }
 +
 +      static void nodeDug(MtEvent *e, void *data)
 +      {
 +              SoundMaker *sm = (SoundMaker *)data;
 +              NodeDugEvent *nde = (NodeDugEvent *)e;
 +              sm->m_sound->playSound(sm->m_ndef->get(nde->n).sound_dug, false);
 +      }
 +
 +      static void playerDamage(MtEvent *e, void *data)
 +      {
 +              SoundMaker *sm = (SoundMaker *)data;
 +              sm->m_sound->playSound(SimpleSoundSpec("player_damage", 0.5), false);
 +      }
 +
 +      static void playerFallingDamage(MtEvent *e, void *data)
 +      {
 +              SoundMaker *sm = (SoundMaker *)data;
 +              sm->m_sound->playSound(SimpleSoundSpec("player_falling_damage", 0.5), false);
 +      }
 +
 +      void registerReceiver(MtEventManager *mgr)
 +      {
 +              mgr->reg(MtEvent::VIEW_BOBBING_STEP, SoundMaker::viewBobbingStep, this);
 +              mgr->reg(MtEvent::PLAYER_REGAIN_GROUND, SoundMaker::playerRegainGround, this);
 +              mgr->reg(MtEvent::PLAYER_JUMP, SoundMaker::playerJump, this);
 +              mgr->reg(MtEvent::CAMERA_PUNCH_LEFT, SoundMaker::cameraPunchLeft, this);
 +              mgr->reg(MtEvent::CAMERA_PUNCH_RIGHT, SoundMaker::cameraPunchRight, this);
 +              mgr->reg(MtEvent::NODE_DUG, SoundMaker::nodeDug, this);
 +              mgr->reg(MtEvent::PLAYER_DAMAGE, SoundMaker::playerDamage, this);
 +              mgr->reg(MtEvent::PLAYER_FALLING_DAMAGE, SoundMaker::playerFallingDamage, this);
 +      }
 +
 +      void step(float dtime)
 +      {
 +              m_player_step_timer -= dtime;
 +              m_player_jump_timer -= dtime;
 +      }
 +};
 +
 +// Locally stored sounds don't need to be preloaded because of this
 +class GameOnDemandSoundFetcher: public OnDemandSoundFetcher
 +{
 +      std::set<std::string> m_fetched;
 +private:
 +      void paths_insert(std::set<std::string> &dst_paths,
 +              const std::string &base,
 +              const std::string &name)
 +      {
 +              dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".ogg");
 +              dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".0.ogg");
 +              dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".1.ogg");
 +              dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".2.ogg");
 +              dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".3.ogg");
 +              dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".4.ogg");
 +              dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".5.ogg");
 +              dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".6.ogg");
 +              dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".7.ogg");
 +              dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".8.ogg");
 +              dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".9.ogg");
 +      }
 +public:
 +      void fetchSounds(const std::string &name,
 +              std::set<std::string> &dst_paths,
 +              std::set<std::string> &dst_datas)
 +      {
 +              if (m_fetched.count(name))
 +                      return;
 +
 +              m_fetched.insert(name);
 +
 +              paths_insert(dst_paths, porting::path_share, name);
 +              paths_insert(dst_paths, porting::path_user,  name);
 +      }
 +};
 +
 +
 +typedef s32 SamplerLayer_t;
 +
 +
 +class GameGlobalShaderConstantSetter : public IShaderConstantSetter
 +{
 +      Sky *m_sky;
 +      bool *m_force_fog_off;
 +      f32 *m_fog_range;
 +      bool m_fog_enabled;
 +      CachedPixelShaderSetting<float, 4> m_sky_bg_color;
 +      CachedPixelShaderSetting<float> m_fog_distance;
 +      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;
 +      Client *m_client;
 +
 +public:
 +      void onSettingsChange(const std::string &name)
 +      {
 +              if (name == "enable_fog")
 +                      m_fog_enabled = g_settings->getBool("enable_fog");
 +      }
 +
 +      static void settingsCallback(const std::string &name, void *userdata)
 +      {
 +              reinterpret_cast<GameGlobalShaderConstantSetter*>(userdata)->onSettingsChange(name);
 +      }
 +
 +      void setSky(Sky *sky) { m_sky = sky; }
 +
 +      GameGlobalShaderConstantSetter(Sky *sky, bool *force_fog_off,
 +                      f32 *fog_range, Client *client) :
 +              m_sky(sky),
 +              m_force_fog_off(force_fog_off),
 +              m_fog_range(fog_range),
 +              m_sky_bg_color("skyBgColor"),
 +              m_fog_distance("fogDistance"),
 +              m_animation_timer_vertex("animationTimer"),
 +              m_animation_timer_pixel("animationTimer"),
 +              m_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_client(client)
 +      {
 +              g_settings->registerChangedCallback("enable_fog", settingsCallback, this);
 +              m_fog_enabled = g_settings->getBool("enable_fog");
 +      }
 +
 +      ~GameGlobalShaderConstantSetter()
 +      {
 +              g_settings->deregisterChangedCallback("enable_fog", settingsCallback, this);
 +      }
 +
 +      void onSetConstants(video::IMaterialRendererServices *services) override
 +      {
 +              // Background color
 +              video::SColor bgcolor = m_sky->getBgColor();
 +              video::SColorf bgcolorf(bgcolor);
 +              float bgcolorfa[4] = {
 +                      bgcolorf.r,
 +                      bgcolorf.g,
 +                      bgcolorf.b,
 +                      bgcolorf.a,
 +              };
 +              m_sky_bg_color.set(bgcolorfa, services);
 +
 +              // Fog distance
 +              float fog_distance = 10000 * BS;
 +
 +              if (m_fog_enabled && !*m_force_fog_off)
 +                      fog_distance = *m_fog_range;
 +
 +              m_fog_distance.set(&fog_distance, services);
 +
 +              u32 daynight_ratio = (float)m_client->getEnv().getDayNightRatio();
 +              video::SColorf sunlight;
 +              get_sunlight_color(&sunlight, daynight_ratio);
 +              float dnc[3] = {
 +                      sunlight.r,
 +                      sunlight.g,
 +                      sunlight.b };
 +              m_day_light.set(dnc, services);
 +
 +              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();
 +              epos.getAs3Values(eye_position_array);
 +              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();
 +                      minimap_yaw.getAs3Values(minimap_yaw_array);
 +                      m_minimap_yaw.set(minimap_yaw_array, services);
 +              }
 +
 +              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);
 +      }
 +};
 +
 +
 +class GameGlobalShaderConstantSetterFactory : public IShaderConstantSetterFactory
 +{
 +      Sky *m_sky;
 +      bool *m_force_fog_off;
 +      f32 *m_fog_range;
 +      Client *m_client;
 +      std::vector<GameGlobalShaderConstantSetter *> created_nosky;
 +public:
 +      GameGlobalShaderConstantSetterFactory(bool *force_fog_off,
 +                      f32 *fog_range, Client *client) :
 +              m_sky(NULL),
 +              m_force_fog_off(force_fog_off),
 +              m_fog_range(fog_range),
 +              m_client(client)
 +      {}
 +
 +      void setSky(Sky *sky) {
 +              m_sky = sky;
 +              for (GameGlobalShaderConstantSetter *ggscs : created_nosky) {
 +                      ggscs->setSky(m_sky);
 +              }
 +              created_nosky.clear();
 +      }
 +
 +      virtual IShaderConstantSetter* create()
 +      {
 +              auto *scs = new GameGlobalShaderConstantSetter(
 +                              m_sky, m_force_fog_off, m_fog_range, m_client);
 +              if (!m_sky)
 +                      created_nosky.push_back(scs);
 +              return scs;
 +      }
 +};
 +
-       u32 last_time, busy_time, sleep_time;
++#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;
 +
 +struct FpsControl {
-       void updateCamera(u32 busy_time, f32 dtime);
++      FpsControl() : last_time(0), busy_time(0), sleep_time(0) {}
++
++      void reset();
++
++      void limit(IrrlichtDevice *device, f32 *dtime);
++
++      u32 getBusyMs() const { return busy_time / 1000; }
++
++      // all values in microseconds (us)
++      u64 last_time, busy_time, sleep_time;
 +};
 +
 +
 +/* The reason the following structs are not anonymous structs within the
 + * class is that they are not used by the majority of member functions and
 + * many functions that do require objects of thse types do not modify them
 + * (so they can be passed as a const qualified parameter)
 + */
 +
 +struct GameRunData {
 +      u16 dig_index;
 +      u16 new_playeritem;
 +      PointedThing pointed_old;
 +      bool digging;
 +      bool punching;
 +      bool btn_down_for_dig;
 +      bool dig_instantly;
 +      bool digging_blocked;
 +      bool reset_jump_timer;
 +      float nodig_delay_timer;
 +      float dig_time;
 +      float dig_time_complete;
 +      float repeat_place_timer;
 +      float object_hit_delay_timer;
 +      float time_from_last_punch;
 +      ClientActiveObject *selected_object;
 +
 +      float jump_timer;
 +      float damage_flash;
 +      float update_draw_list_timer;
 +
 +      f32 fog_range;
 +
 +      v3f update_draw_list_last_cam_dir;
 +
 +      float time_of_day_smooth;
 +};
 +
 +class Game;
 +
 +struct ClientEventHandler
 +{
 +      void (Game::*handler)(ClientEvent *, CameraOrientation *);
 +};
 +
 +using PausedNodesList = std::vector<std::pair<irr_ptr<scene::IAnimatedMeshSceneNode>, float>>;
 +
 +class Game {
 +public:
 +      Game();
 +      ~Game();
 +
 +      bool startup(bool *kill,
 +                      InputHandler *input,
 +                      RenderingEngine *rendering_engine,
 +                      const GameStartData &game_params,
 +                      std::string &error_message,
 +                      bool *reconnect,
 +                      ChatBackend *chat_backend);
 +
 +
 +      void run();
 +      void shutdown();
 +
 +      // Basic initialisation
 +      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);
 +
 +      // Client creation
 +      bool createClient(const GameStartData &start_data);
 +      bool initGui();
 +
 +      // Client connection
 +      bool connectToServer(const GameStartData &start_data,
 +                      bool *connect_ok, bool *aborted);
 +      bool getServerContent(bool *aborted);
 +
 +      // Main loop
 +
 +      void updateInteractTimers(f32 dtime);
 +      bool checkConnection();
 +      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);
 +
 +      // Input related
 +      void processUserInput(f32 dtime);
 +      void processKeyInput();
 +      void processItemSelection(u16 *new_playeritem);
 +
 +      void dropSelectedItem(bool single_item = false);
 +      void openInventory();
 +      void openEnderchest();
 +      void openConsole(float scale, const wchar_t *line=NULL);
 +      void toggleFreeMove();
 +      void toggleFreeMoveAlt();
 +      void togglePitchMove();
 +      void toggleFast();
 +      void toggleNoClip();
 +      void toggleKillaura();
 +      void toggleFreecam();
 +      void toggleScaffold();
 +      void toggleNextItem();
 +      void toggleCinematic();
 +      void toggleBlockBounds();
 +      void toggleAutoforward();
 +
 +      void toggleMinimap(bool shift_pressed);
 +      void toggleFog();
 +      void toggleDebug();
 +      void toggleUpdateCamera();
 +      void updatePlayerCAOVisibility();
 +
 +      void increaseViewRange();
 +      void decreaseViewRange();
 +      void toggleFullViewRange();
 +      void checkZoomEnabled();
 +
 +      void updateCameraDirection(CameraOrientation *cam, float dtime);
 +      void updateCameraOrientation(CameraOrientation *cam, float dtime);
 +      void updatePlayerControl(const CameraOrientation &cam);
 +      void step(f32 *dtime);
 +      void processClientEvents(CameraOrientation *cam);
-       void processPlayerInteraction(f32 dtime, bool show_hud, bool show_debug);
++      void updateCamera(f32 dtime);
 +      void updateSound(f32 dtime);
-       void limitFps(FpsControl *fps_timings, f32 *dtime);
++      void processPlayerInteraction(f32 dtime, bool show_hud);
 +      /*!
 +       * Returns the object or node the player is pointing at.
 +       * Also updates the selected thing in the Hud.
 +       *
 +       * @param[in]  shootline         the shootline, starting from
 +       * the camera position. This also gives the maximal distance
 +       * of the search.
 +       * @param[in]  liquids_pointable if false, liquids are ignored
 +       * @param[in]  look_for_object   if false, objects are ignored
 +       * @param[in]  camera_offset     offset of the camera
 +       * @param[out] selected_object   the selected object or
 +       * NULL if not found
 +       */
 +      PointedThing updatePointedThing(
 +                      const core::line3d<f32> &shootline, bool liquids_pointable,
 +                      bool look_for_object, const v3s16 &camera_offset);
 +      void handlePointingAtNothing(const ItemStack &playerItem);
 +      void handlePointingAtNode(const PointedThing &pointed,
 +                      const ItemStack &selected_item, const ItemStack &hand_item, f32 dtime);
 +      void handlePointingAtObject(const PointedThing &pointed, const ItemStack &playeritem,
 +                      const v3f &player_position, bool show_debug);
 +      void handleDigging(const PointedThing &pointed, const v3s16 &nodepos,
 +                      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 updateChat(f32 dtime, const v2u32 &screensize);
 +      void showOverlayMessage(const char *msg, float dtime, int percent,
 +                      bool draw_clouds = true);
 +
 +      static void freecamChangedCallback(const std::string &setting_name, void *data);
 +      static void settingChangedCallback(const std::string &setting_name, void *data);
 +      static void updateAllMapBlocksCallback(const std::string &setting_name, void *data);
 +      void readSettings();
 +
 +      bool isKeyDown(GameKeyType k);
 +      bool wasKeyDown(GameKeyType k);
 +      bool wasKeyPressed(GameKeyType k);
 +      bool wasKeyReleased(GameKeyType k);
 +
 +#ifdef __ANDROID__
 +      void handleAndroidChatInput();
 +#endif
 +
 +      struct Flags {
 +              bool force_fog_off = false;
 +              bool disable_camera_update = false;
 +      };
 +
 +      void showDeathFormspec();
 +      void showPauseMenu();
 +
 +      void pauseAnimation();
 +      void resumeAnimation();
 +
 +      // ClientEvent handlers
 +      void handleClientEvent_None(ClientEvent *event, CameraOrientation *cam);
 +      void handleClientEvent_PlayerDamage(ClientEvent *event, CameraOrientation *cam);
 +      void handleClientEvent_PlayerForceMove(ClientEvent *event, CameraOrientation *cam);
 +      void handleClientEvent_Deathscreen(ClientEvent *event, CameraOrientation *cam);
 +      void handleClientEvent_ShowFormSpec(ClientEvent *event, CameraOrientation *cam);
 +      void handleClientEvent_ShowLocalFormSpec(ClientEvent *event, CameraOrientation *cam);
 +      void handleClientEvent_HandleParticleEvent(ClientEvent *event,
 +              CameraOrientation *cam);
 +      void handleClientEvent_HudAdd(ClientEvent *event, CameraOrientation *cam);
 +      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);
 +
-       CameraOrientation cam_view_target  = { 0 };
-       CameraOrientation cam_view  = { 0 };
++      void updateChat(f32 dtime);
 +
 +      bool nodePlacement(const ItemDefinition &selected_def, const ItemStack &selected_item,
 +              const v3s16 &nodepos, const v3s16 &neighbourpos, const PointedThing &pointed,
 +              const NodeMetadata *meta, bool force = false);
 +      static const ClientEventHandler clientEventHandler[CLIENTEVENT_MAX];
 +
 +      f32 getSensitivityScaleFactor() const;
 +
 +      InputHandler *input = nullptr;
 +
 +      Client *client = nullptr;
 +      Server *server = nullptr;
 +
 +      IWritableTextureSource *texture_src = nullptr;
 +      IWritableShaderSource *shader_src = nullptr;
 +
 +      // When created, these will be filled with data received from the server
 +      IWritableItemDefManager *itemdef_manager = nullptr;
 +      NodeDefManager *nodedef_manager = nullptr;
 +
 +      GameOnDemandSoundFetcher soundfetcher; // useful when testing
 +      ISoundManager *sound = nullptr;
 +      bool sound_is_dummy = false;
 +      SoundMaker *soundmaker = nullptr;
 +
 +      ChatBackend *chat_backend = nullptr;
 +      LogOutputBuffer m_chat_log_buf;
 +
 +      EventManager *eventmgr = nullptr;
 +      QuicktuneShortcutter *quicktune = nullptr;
 +      bool registration_confirmation_shown = false;
 +
 +      std::unique_ptr<GameUI> m_game_ui;
 +      GUIChatConsole *gui_chat_console = nullptr; // Free using ->Drop()
 +      CheatMenu *m_cheat_menu = nullptr;
 +      MapDrawControl *draw_control = nullptr;
 +      Camera *camera = nullptr;
 +      Clouds *clouds = nullptr;                         // Free using ->Drop()
 +      Sky *sky = nullptr;                         // Free using ->Drop()
 +      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;
 +
 +      /* 'cache'
 +         This class does take ownership/responsibily for cleaning up etc of any of
 +         these items (e.g. device)
 +      */
 +      IrrlichtDevice *device;
 +      RenderingEngine *m_rendering_engine;
 +      video::IVideoDriver *driver;
 +      scene::ISceneManager *smgr;
 +      bool *kill;
 +      std::string *error_message;
 +      bool *reconnect_requested;
 +      scene::ISceneNode *skybox;
 +      PausedNodesList paused_animated_nodes;
 +
 +      bool simple_singleplayer_mode;
 +      /* End 'cache' */
 +
 +      /* Pre-calculated values
 +       */
 +      int crack_animation_length;
 +
 +      IntervalLimiter profiler_interval;
 +
 +      /*
 +       * TODO: Local caching of settings is not optimal and should at some stage
 +       *       be updated to use a global settings object for getting thse values
 +       *       (as opposed to the this local caching). This can be addressed in
 +       *       a later release.
 +       */
 +      bool m_cache_doubletap_jump;
 +      bool m_cache_enable_clouds;
 +      bool m_cache_enable_joysticks;
 +      bool m_cache_enable_particles;
 +      bool m_cache_enable_fog;
 +      bool m_cache_enable_noclip;
 +      bool m_cache_enable_free_move;
 +      f32  m_cache_mouse_sensitivity;
 +      f32  m_cache_joystick_frustum_sensitivity;
 +      f32  m_repeat_place_time;
 +      f32  m_cache_cam_smoothing;
 +      f32  m_cache_fog_start;
 +
 +      bool m_invert_mouse = false;
 +      bool m_first_loop_after_window_activation = false;
 +      bool m_camera_offset_changed = false;
 +
 +      bool m_does_lost_focus_pause_game = false;
 +
- #ifdef __ANDROID__
++      CameraOrientation cam_view_target = {}; // added by dragonfireclient
++      CameraOrientation cam_view  = {};       // added by dragonfireclient
 +
++#if IRRLICHT_VERSION_MT_REVISION < 5
 +      int m_reset_HW_buffer_counter = 0;
++#endif
++
++#ifdef HAVE_TOUCHSCREENGUI
 +      bool m_cache_hold_aux1;
++#endif
++#ifdef __ANDROID__
 +      bool m_android_chat_open;
 +#endif
 +};
 +extern Game *g_game;
  
  void the_game(bool *kill,
                InputHandler *input,
diff --combined src/client/gameui.cpp
index 66a006ea196cae71925725bb84105933e0897462,01c733b4f412f678f4aa70153c984d44d5c42a35..54be24ae2ac6fd7fc65fef0a31f6bb8bbb112148
@@@ -53,9 -53,6 +53,9 @@@ GameUI::GameUI(
  }
  void GameUI::init()
  {
 +      m_guitext_coords = gui::StaticText::add(guienv, L"", core::rect<s32>(0, 0, 0, 0), false,
 +              false, guiroot);
 +
        // First line of debug text
        m_guitext = gui::StaticText::add(guienv, utf8_to_wide(PROJECT_NAME_C).c_str(),
                core::rect<s32>(0, 0, 0, 0), false, false, guiroot);
@@@ -103,37 -100,20 +103,37 @@@ void GameUI::update(const RunStats &sta
        const CameraOrientation &cam, const PointedThing &pointed_old,
        const GUIChatConsole *chat_console, float dtime)
  {
 +      LocalPlayer *player = client->getEnv().getLocalPlayer();
 +      v3f player_position = player->getPosition();
 +
        v2u32 screensize = RenderingEngine::getWindowSize();
  
 +      bool show_coords = g_settings->getBool("coords");
 +
 +      if (show_coords) {
 +              std::ostringstream os(std::ios_base::binary);
 +              os << std::setprecision(1) << std::fixed
 +                      << (player_position.X / BS)
 +                      << ", " << (player_position.Y / BS)
 +                      << ", " << (player_position.Z / BS);
 +              setStaticText(m_guitext_coords, utf8_to_wide(os.str()).c_str());
 +              m_guitext_coords->setRelativePosition(core::rect<s32>(5, screensize.Y - 5 - g_fontengine->getTextHeight(), screensize.X, screensize.Y));
 +      }
 +
 +      m_guitext_coords->setVisible(show_coords);
 +
        // Minimal debug text must only contain info that can't give a gameplay advantage
        if (m_flags.show_minimal_debug) {
-               static float drawtime_avg = 0;
-               drawtime_avg = drawtime_avg * 0.95 + stats.drawtime * 0.05;
-               u16 fps = 1.0 / stats.dtime_jitter.avg;
+               const u16 fps = 1.0 / stats.dtime_jitter.avg;
+               m_drawtime_avg *= 0.95f;
+               m_drawtime_avg += 0.05f * (stats.drawtime / 1000);
  
                std::ostringstream os(std::ios_base::binary);
                os << std::fixed
                        << PROJECT_NAME_C " " << g_version_hash
                        << " | FPS: " << fps
                        << std::setprecision(0)
-                       << " | drawtime: " << drawtime_avg << "ms"
+                       << " | drawtime: " << m_drawtime_avg << "ms"
                        << std::setprecision(1)
                        << " | dtime jitter: "
                        << (stats.dtime_jitter.max_fraction * 100.0) << "%"
                        const NodeDefManager *nodedef = client->getNodeDefManager();
                        MapNode n = map.getNode(pointed_old.node_undersurface);
  
-                       if (n.getContent() != CONTENT_IGNORE && nodedef->get(n).name != "unknown") {
-                               os << ", pointed: " << nodedef->get(n).name
-                                       << ", param2: " << (u64) n.getParam2();
+                       if (n.getContent() != CONTENT_IGNORE) {
+                               if (nodedef->get(n).name == "unknown") {
+                                       os << ", pointed: <unknown node>";
+                               } else {
+                                       os << ", pointed: " << nodedef->get(n).name;
+                               }
+                               os << ", param2: " << (u64) n.getParam2();
                        }
                }
  
                m_guitext_status->enableOverrideColor(true);
        }
  
 -      // Hide chat when console is visible
 -      m_guitext_chat->setVisible(isChatVisible() && !chat_console->isVisible());
 +      m_guitext_chat->setVisible(isChatVisible());
  }
  
  void GameUI::initFlags()
  {
        m_flags = GameUI::Flags();
        m_flags.show_minimal_debug = g_settings->getBool("show_debug");
-       m_flags.show_basic_debug = false;
  }
  
  void GameUI::showMinimap(bool show)
@@@ -246,27 -230,32 +249,33 @@@ void GameUI::showTranslatedStatusText(c
  
  void GameUI::setChatText(const EnrichedString &chat_text, u32 recent_chat_count)
  {
+       setStaticText(m_guitext_chat, chat_text);
  
+       m_recent_chat_count = recent_chat_count;
+ }
+ void GameUI::updateChatSize()
+ {
        // Update gui element size and position
 -      s32 chat_y = 5;
 +
 +      const v2u32 &window_size = RenderingEngine::getWindowSize();
 +
 +      s32 chat_y = window_size.Y - 150 - m_guitext_chat->getTextHeight();
  
        if (m_flags.show_minimal_debug)
                chat_y += g_fontengine->getLineHeight();
        if (m_flags.show_basic_debug)
                chat_y += g_fontengine->getLineHeight();
  
-       core::rect<s32> chat_size(10, chat_y,
-               window_size.X - 20, 0);
 -      const v2u32 &window_size = RenderingEngine::getWindowSize();
 -
+       core::rect<s32> chat_size(10, chat_y, window_size.X - 20, 0);
        chat_size.LowerRightCorner.Y = std::min((s32)window_size.Y,
-               m_guitext_chat->getTextHeight() + chat_y);
+                       m_guitext_chat->getTextHeight() + chat_y);
  
-       m_guitext_chat->setRelativePosition(chat_size);
-       setStaticText(m_guitext_chat, chat_text);
+       if (chat_size == m_current_chat_size)
+               return;
+       m_current_chat_size = chat_size;
  
-       m_recent_chat_count = recent_chat_count;
+       m_guitext_chat->setRelativePosition(chat_size);
  }
  
  void GameUI::updateProfiler()
@@@ -305,15 -294,6 +314,15 @@@ void GameUI::toggleChat(
                showTranslatedStatusText("Chat hidden");
  }
  
 +void GameUI::toggleCheatMenu()
 +{
 +      m_flags.show_cheat_menu = !m_flags.show_cheat_menu;
 +      if (m_flags.show_cheat_menu)
 +              showTranslatedStatusText("Cheat Menu shown");
 +      else
 +              showTranslatedStatusText("Cheat Menu hidden");
 +}
 +
  void GameUI::toggleHud()
  {
        m_flags.show_hud = !m_flags.show_hud;
diff --combined src/client/gameui.h
index 5404643e2321e65a18f8152eaf87e83612d2491b,cc9377bdca4622ddc695edf0640eb59402b07a53..e22be068b2ced66d30cf27f85878390eef936ae7
@@@ -61,7 -61,6 +61,7 @@@ public
                bool show_minimal_debug = false;
                bool show_basic_debug = false;
                bool show_profiler_graph = false;
 +              bool show_cheat_menu = true;
        };
  
        void init();
        void showTranslatedStatusText(const char *str);
        inline void clearStatusText() { m_statustext.clear(); }
  
-       const bool isChatVisible()
+       bool isChatVisible()
        {
                return m_flags.show_chat && m_recent_chat_count != 0 && m_profiler_current_page == 0;
        }
        void setChatText(const EnrichedString &chat_text, u32 recent_chat_count);
+       void updateChatSize();
  
        void updateProfiler();
  
        void toggleChat();
 +      void toggleCheatMenu();
        void toggleHud();
        void toggleProfiler();
  
  private:
        Flags m_flags;
  
+       float m_drawtime_avg = 0;
        gui::IGUIStaticText *m_guitext = nullptr;  // First line of debug text
        gui::IGUIStaticText *m_guitext2 = nullptr; // Second line of debug text
 -
 +      gui::IGUIStaticText *m_guitext_coords = nullptr;
 +      
        gui::IGUIStaticText *m_guitext_info = nullptr; // At the middle of the screen
        std::wstring m_infotext;
  
  
        gui::IGUIStaticText *m_guitext_chat = nullptr; // Chat text
        u32 m_recent_chat_count = 0;
+       core::rect<s32> m_current_chat_size{0, 0, 0, 0};
  
        gui::IGUIStaticText *m_guitext_profiler = nullptr; // Profiler text
        u8 m_profiler_current_page = 0;
diff --combined src/client/hud.cpp
index 3f687b698dcb602ebd0f07fd8dd3767725be3719,01f4d6ff3bfdb1b832406fabbae2d7939be88b49..d5debecfbb71ade34c6485f78609a4ed3ef925ea
@@@ -20,6 -20,8 +20,8 @@@ with this program; if not, write to th
  */
  
  #include "client/hud.h"
+ #include <string>
+ #include <iostream>
  #include <cmath>
  #include "settings.h"
  #include "util/numeric.h"
@@@ -148,7 -150,7 +150,7 @@@ void Hud::drawItem(const ItemStack &ite
                bool selected)
  {
        if (selected) {
 -              /* draw hihlighting around selected item */
 +              /* draw highlighting around selected item */
                if (use_hotbar_selected_image) {
                        core::rect<s32> imgrect2 = rect;
                        imgrect2.UpperLeftCorner.X  -= (m_padding*2);
  }
  
  //NOTE: selectitem = 0 -> no selected; selectitem 1-based
+ // mainlist can be NULL, but draw the frame anyway.
  void Hud::drawItems(v2s32 upperleftpos, v2s32 screen_offset, s32 itemcount,
                s32 inv_offset, InventoryList *mainlist, u16 selectitem, u16 direction)
  {
  
        // Draw items
        core::rect<s32> imgrect(0, 0, m_hotbar_imagesize, m_hotbar_imagesize);
-       for (s32 i = inv_offset; i < itemcount && (size_t)i < mainlist->getSize(); i++) {
+       const s32 list_size = mainlist ? mainlist->getSize() : 0;
+       for (s32 i = inv_offset; i < itemcount && i < list_size; i++) {
                s32 fullimglen = m_hotbar_imagesize + m_padding * 2;
  
                v2s32 steppos;
@@@ -377,15 -381,19 +381,19 @@@ void Hud::drawLuaElements(const v3s16 &
                                std::wstring text = unescape_translate(utf8_to_wide(e->text));
                                core::dimension2d<u32> textsize = textfont->getDimension(text.c_str());
  
-                               v2s32 offset((e->align.X - 1.0) * (textsize.Width / 2),
-                                            (e->align.Y - 1.0) * (textsize.Height / 2));
+                               v2s32 offset(0, (e->align.Y - 1.0) * (textsize.Height / 2));
                                core::rect<s32> size(0, 0, e->scale.X * m_scale_factor,
-                                                    text_height * e->scale.Y * m_scale_factor);
+                                               text_height * e->scale.Y * m_scale_factor);
                                v2s32 offs(e->offset.X * m_scale_factor,
-                                          e->offset.Y * m_scale_factor);
+                                               e->offset.Y * m_scale_factor);
+                               std::wstringstream wss(text);
+                               std::wstring line;
+                               while (std::getline(wss, line, L'\n'))
                                {
-                                       textfont->draw(text.c_str(), size + pos + offset + offs, color);
+                                       core::dimension2d<u32> linesize = textfont->getDimension(line.c_str());
+                                       v2s32 line_offset((e->align.X - 1.0) * (linesize.Width / 2), 0);
+                                       textfont->draw(line.c_str(), size + pos + offset + offs + line_offset, color);
+                                       offset.Y += linesize.Height;
                                }
                                break; }
                        case HUD_ELEM_STATBAR: {
                                break; }
                        case HUD_ELEM_INVENTORY: {
                                InventoryList *inv = inventory->getList(e->text);
+                               if (!inv)
+                                       warningstream << "HUD: Unknown inventory list. name=" << e->text << std::endl;
                                drawItems(pos, v2s32(e->offset.X, e->offset.Y), e->number, 0,
                                        inv, e->item, e->dir);
                                break; }
@@@ -666,7 -676,7 +676,7 @@@ void Hud::drawStatbar(v2s32 pos, u16 co
        // Rectangles for 1/2 the "off state" texture
        core::rect<s32> srchalfrect2, dsthalfrect2;
  
-       if (count % 2 == 1) {
+       if (count % 2 == 1 || maxcount % 2 == 1) {
                // Need to draw halves: Calculate rectangles
                srchalfrect  = calculate_clipping_rect(srcd, steppos);
                dsthalfrect  = calculate_clipping_rect(dstd, steppos);
                }
        }
  
-       if (stat_texture_bg && maxcount > count / 2) {
+       if (stat_texture_bg && maxcount > count) {
                // Draw "off state" textures
                s32 start_offset;
                if (count % 2 == 1)
  
                if (maxcount % 2 == 1) {
                        draw2DImageFilterScaled(driver, stat_texture_bg,
-                                       dsthalfrect + p, srchalfrect,
-                                       NULL, colors, true);
+                               dsthalfrect + p, srchalfrect, NULL, colors, true);
                }
        }
  }
@@@ -1001,11 -1010,19 +1010,19 @@@ void drawItemStack
  
        bool draw_overlay = false;
  
+       bool has_mesh = false;
+       ItemMesh *imesh;
+       core::rect<s32> viewrect = rect;
+       if (clip != nullptr)
+               viewrect.clipAgainst(*clip);
        // Render as mesh if animated or no inventory image
        if ((enable_animations && rotation_kind < IT_ROT_NONE) || def.inventory_image.empty()) {
-               ItemMesh *imesh = client->idef()->getWieldMesh(def.name, client);
-               if (!imesh || !imesh->mesh)
-                       return;
+               imesh = client->idef()->getWieldMesh(def.name, client);
+               has_mesh = imesh && imesh->mesh;
+       }
+       if (has_mesh) {
                scene::IMesh *mesh = imesh->mesh;
                driver->clearBuffers(video::ECBF_DEPTH);
                s32 delta = 0;
                core::rect<s32> oldViewPort = driver->getViewPort();
                core::matrix4 oldProjMat = driver->getTransform(video::ETS_PROJECTION);
                core::matrix4 oldViewMat = driver->getTransform(video::ETS_VIEW);
-               core::rect<s32> viewrect = rect;
-               if (clip)
-                       viewrect.clipAgainst(*clip);
  
                core::matrix4 ProjMatrix;
                ProjMatrix.buildProjectionMatrixOrthoLH(2.0f, 2.0f, -1.0f, 100.0f);
                draw_overlay = def.type == ITEM_NODE && def.inventory_image.empty();
        } else { // Otherwise just draw as 2D
                video::ITexture *texture = client->idef()->getInventoryTexture(def.name, client);
-               if (!texture)
-                       return;
-               video::SColor color =
-                       client->idef()->getItemstackColor(item, client);
+               video::SColor color;
+               if (texture) {
+                       color = client->idef()->getItemstackColor(item, client);
+               } else {
+                       color = video::SColor(255, 255, 255, 255);
+                       ITextureSource *tsrc = client->getTextureSource();
+                       texture = tsrc->getTexture("no_texture.png");
+                       if (!texture)
+                               return;
+               }
                const video::SColor colors[] = { color, color, color, color };
  
                draw2DImageFilterScaled(driver, texture, rect,
                driver->draw2DRectangle(color, progressrect2, clip);
        }
  
-       if (font != NULL && item.count >= 2) {
+       const std::string &count_text = item.metadata.getString("count_meta");
+       if (font != nullptr && (item.count >= 2 || !count_text.empty())) {
                // Get the item count as a string
-               std::string text = itos(item.count);
-               v2u32 dim = font->getDimension(utf8_to_wide(text).c_str());
+               std::string text = count_text.empty() ? itos(item.count) : count_text;
+               v2u32 dim = font->getDimension(utf8_to_wide(unescape_enriched(text)).c_str());
                v2s32 sdim(dim.X, dim.Y);
  
                core::rect<s32> rect2(
-                       /*rect.UpperLeftCorner,
-                       core::dimension2d<u32>(rect.getWidth(), 15)*/
                        rect.LowerRightCorner - sdim,
-                       sdim
+                       rect.LowerRightCorner
                );
  
-               video::SColor bgcolor(128, 0, 0, 0);
-               driver->draw2DRectangle(bgcolor, rect2, clip);
+               // get the count alignment
+               s32 count_alignment = stoi(item.metadata.getString("count_alignment"));
+               if (count_alignment != 0) {
+                       s32 a_x = count_alignment & 3;
+                       s32 a_y = (count_alignment >> 2) & 3;
+                       s32 x1, x2, y1, y2;
+                       switch (a_x) {
+                       case 1: // left
+                               x1 = rect.UpperLeftCorner.X;
+                               x2 = x1 + sdim.X;
+                               break;
+                       case 2: // middle
+                               x1 = (rect.UpperLeftCorner.X + rect.LowerRightCorner.X - sdim.X) / 2;
+                               x2 = x1 + sdim.X;
+                               break;
+                       case 3: // right
+                               x2 = rect.LowerRightCorner.X;
+                               x1 = x2 - sdim.X;
+                               break;
+                       default: // 0 = default
+                               x1 = rect2.UpperLeftCorner.X;
+                               x2 = rect2.LowerRightCorner.X;
+                               break;
+                       }
+                       switch (a_y) {
+                       case 1: // up
+                               y1 = rect.UpperLeftCorner.Y;
+                               y2 = y1 + sdim.Y;
+                               break;
+                       case 2: // middle
+                               y1 = (rect.UpperLeftCorner.Y + rect.LowerRightCorner.Y - sdim.Y) / 2;
+                               y2 = y1 + sdim.Y;
+                               break;
+                       case 3: // down
+                               y2 = rect.LowerRightCorner.Y;
+                               y1 = y2 - sdim.Y;
+                               break;
+                       default: // 0 = default
+                               y1 = rect2.UpperLeftCorner.Y;
+                               y2 = rect2.LowerRightCorner.Y;
+                               break;
+                       }
+                       rect2 = core::rect<s32>(x1, y1, x2, y2);
+               }
  
                video::SColor color(255, 255, 255, 255);
-               font->draw(text.c_str(), rect2, color, false, false, clip);
+               font->draw(utf8_to_wide(text).c_str(), rect2, color, false, false, &viewrect);
        }
  }
  
index 951ec88840b414432956c7467a25a2b8d049fe06,3db105c518ecf6d066d1436247b6f558e3f1f21c..47a61d4b85ba86f6b5f454295170ab2ccfcdae6b
@@@ -30,7 -30,6 +30,7 @@@ with this program; if not, write to th
  #endif
  
  class InputHandler;
 +class TouchScreenGUI;
  
  /****************************************************************************
   Fast key cache for main game loop
@@@ -153,8 -152,14 +153,14 @@@ public
        // in the subsequent iteration of Game::processPlayerInteraction
        bool WasKeyReleased(const KeyPress &keycode) const { return keyWasReleased[keycode]; }
  
-       void listenForKey(const KeyPress &keyCode) { keysListenedFor.set(keyCode); }
-       void dontListenForKeys() { keysListenedFor.clear(); }
+       void listenForKey(const KeyPress &keyCode)
+       {
+               keysListenedFor.set(keyCode);
+       }
+       void dontListenForKeys()
+       {
+               keysListenedFor.clear();
+       }
  
        s32 getMouseWheel()
        {
  #endif
        }
  
-       s32 mouse_wheel = 0;
        JoystickController *joystick = nullptr;
  
  #ifdef HAVE_TOUCHSCREENGUI
        TouchScreenGUI *m_touchscreengui;
  #endif
  
 -private:
+       s32 mouse_wheel = 0;
        // The current state of keys
        KeyList keyIsDown;
  
@@@ -235,8 -241,6 +241,8 @@@ public
        }
  
        virtual bool isKeyDown(GameKeyType k) = 0;
 +      virtual void setKeypress(const KeyPress &keyCode) = 0;
 +      virtual void unsetKeypress(const KeyPress &keyCode) = 0;
        virtual bool wasKeyDown(GameKeyType k) = 0;
        virtual bool wasKeyPressed(GameKeyType k) = 0;
        virtual bool wasKeyReleased(GameKeyType k) = 0;
@@@ -274,19 -278,16 +280,25 @@@ public
        {
                m_receiver->joystick = &joystick;
        }
+       virtual ~RealInputHandler()
+       {
+               m_receiver->joystick = nullptr;
+       }
        virtual bool isKeyDown(GameKeyType k)
        {
                return m_receiver->IsKeyDown(keycache.key[k]) || joystick.isKeyDown(k);
        }
 +      virtual void setKeypress(const KeyPress &keyCode)
 +      {
 +              m_receiver->keyIsDown.set(keyCode);
 +              m_receiver->keyWasDown.set(keyCode);
 +      }
 +      virtual void unsetKeypress(const KeyPress &keyCode)
 +      {
 +              m_receiver->keyIsDown.unset(keyCode);
 +      }
        virtual bool wasKeyDown(GameKeyType k)
        {
                return m_receiver->WasKeyDown(keycache.key[k]) || joystick.wasKeyDown(k);
        {
                return m_receiver->WasKeyReleased(keycache.key[k]) || joystick.wasKeyReleased(k);
        }
        virtual float getMovementSpeed()
        {
                bool f = m_receiver->IsKeyDown(keycache.key[KeyType::FORWARD]),
                }
                return joystick.getMovementSpeed();
        }
        virtual float getMovementDirection()
        {
                float x = 0, z = 0;
                else
                        return joystick.getMovementDirection();
        }
        virtual bool cancelPressed()
        {
                return wasKeyDown(KeyType::ESC) || m_receiver->WasKeyDown(CancelKey);
        }
        virtual void clearWasKeyPressed()
        {
                m_receiver->clearWasKeyPressed();
        {
                m_receiver->clearWasKeyReleased();
        }
        virtual void listenForKey(const KeyPress &keyCode)
        {
                m_receiver->listenForKey(keyCode);
        }
-       virtual void dontListenForKeys() { m_receiver->dontListenForKeys(); }
+       virtual void dontListenForKeys()
+       {
+               m_receiver->dontListenForKeys();
+       }
        virtual v2s32 getMousePos()
        {
-               if (RenderingEngine::get_raw_device()->getCursorControl()) {
-                       return RenderingEngine::get_raw_device()
-                                       ->getCursorControl()
-                                       ->getPosition();
+               auto control = RenderingEngine::get_raw_device()->getCursorControl();
+               if (control) {
+                       return control->getPosition();
                }
  
                return m_mousepos;
  
        virtual void setMousePos(s32 x, s32 y)
        {
-               if (RenderingEngine::get_raw_device()->getCursorControl()) {
-                       RenderingEngine::get_raw_device()
-                                       ->getCursorControl()
-                                       ->setPosition(x, y);
+               auto control = RenderingEngine::get_raw_device()->getCursorControl();
+               if (control) {
+                       control->setPosition(x, y);
                } else {
                        m_mousepos = v2s32(x, y);
                }
        }
  
-       virtual s32 getMouseWheel() { return m_receiver->getMouseWheel(); }
+       virtual s32 getMouseWheel()
+       {
+               return m_receiver->getMouseWheel();
+       }
  
        void clear()
        {
                m_receiver->clearInput();
        }
  
      private:
+ private:
        MyEventReceiver *m_receiver = nullptr;
        v2s32 m_mousepos;
  };
@@@ -400,14 -411,6 +422,14 @@@ public
        }
  
        virtual bool isKeyDown(GameKeyType k) { return keydown[keycache.key[k]]; }
 +      virtual void setKeypress(const KeyPress &keyCode)
 +      {
 +              keydown.set(keyCode);
 +      }
 +      virtual void unsetKeypress(const KeyPress &keyCode)
 +      {
 +              keydown.unset(keyCode);
 +      }
        virtual bool wasKeyDown(GameKeyType k) { return false; }
        virtual bool wasKeyPressed(GameKeyType k) { return false; }
        virtual bool wasKeyReleased(GameKeyType k) { return false; }
index ca61f3a11e81beff96b654c66a8e307ae6994e66,279efafe936265a7021ed1d224f068c9dff1fd2d..c4d9b98457d6c10574b5e7aec07e964c2db52d60
@@@ -27,8 -27,6 +27,8 @@@ with this program; if not, write to th
  #include "map.h"
  #include "client.h"
  #include "content_cao.h"
 +#include "util/pointedthing.h"
 +#include "client/game.h"
  
  /*
        LocalPlayer
@@@ -89,7 -87,7 +89,7 @@@ bool LocalPlayer::updateSneakNode(Map *
                new_sneak_node_exists = false;
        } else {
                node = map->getNode(current_node, &is_valid_position);
 -              if (!is_valid_position || !nodemgr->get(node).walkable)
 +              if (!is_valid_position || nodemgr->get(node).walkable)
                        new_sneak_node_exists = false;
        }
  
  
                // The node to be sneaked on has to be walkable
                node = map->getNode(p, &is_valid_position);
 -              if (!is_valid_position || !nodemgr->get(node).walkable)
 +              if (!is_valid_position || ! nodemgr->get(node).walkable)
                        continue;
                // And the node(s) above have to be nonwalkable
                bool ok = true;
                } else {
                        // legacy behaviour: check just one node
                        node = map->getNode(p + v3s16(0, 1, 0), &is_valid_position);
 -                      ok = is_valid_position && !nodemgr->get(node).walkable;
 +                      ok = is_valid_position && ! nodemgr->get(node).walkable;
                }
                if (!ok)
                        continue;
                        node = map->getNode(m_sneak_node + v3s16(0, 3, 0),
                                &is_valid_position);
                        m_sneak_ladder_detected = is_valid_position &&
 -                              !nodemgr->get(node).walkable;
 +                              ! nodemgr->get(node).walkable;
                }
        }
        return true;
  void LocalPlayer::move(f32 dtime, Environment *env, f32 pos_max_d,
                std::vector<CollisionInfo> *collision_info)
  {
 +      if (m_cao && m_cao->m_waiting_for_reattach > 0)
 +              m_cao->m_waiting_for_reattach -= dtime;
 +
        // Node at feet position, update each ClientEnvironment::step()
        if (!collision_info || collision_info->empty())
                m_standing_node = floatToInt(m_position, BS);
        PlayerSettings &player_settings = getPlayerSettings();
  
        // Skip collision detection if noclip mode is used
 -      bool fly_allowed = m_client->checkLocalPrivilege("fly");
 -      bool noclip = m_client->checkLocalPrivilege("noclip") && player_settings.noclip;
 -      bool free_move = player_settings.free_move && fly_allowed;
 +      bool fly_allowed = m_client->checkLocalPrivilege("fly") || g_settings->getBool("freecam");
 +      bool noclip = (m_client->checkLocalPrivilege("noclip") && player_settings.noclip) || g_settings->getBool("freecam");
 +      bool free_move = (player_settings.free_move && fly_allowed) || g_settings->getBool("freecam");
  
        if (noclip && free_move) {
                position += m_speed * dtime;
                pp = floatToInt(position + v3f(0.0f, BS * 0.1f, 0.0f), BS);
                node = map->getNode(pp, &is_valid_position);
                if (is_valid_position) {
-                       in_liquid = nodemgr->get(node.getContent()).isLiquid();
-                       liquid_viscosity = nodemgr->get(node.getContent()).liquid_viscosity;
+                       const ContentFeatures &cf = nodemgr->get(node.getContent());
+                       in_liquid = cf.liquid_move_physics;
+                       move_resistance = cf.move_resistance;
                } else {
                        in_liquid = false;
                }
                pp = floatToInt(position + v3f(0.0f, BS * 0.5f, 0.0f), BS);
                node = map->getNode(pp, &is_valid_position);
                if (is_valid_position) {
-                       in_liquid = nodemgr->get(node.getContent()).isLiquid();
-                       liquid_viscosity = nodemgr->get(node.getContent()).liquid_viscosity;
+                       const ContentFeatures &cf = nodemgr->get(node.getContent());
+                       in_liquid = cf.liquid_move_physics;
+                       move_resistance = cf.move_resistance;
                } else {
                        in_liquid = false;
                }
        pp = floatToInt(position + v3f(0.0f), BS);
        node = map->getNode(pp, &is_valid_position);
        if (is_valid_position) {
-               in_liquid_stable = nodemgr->get(node.getContent()).isLiquid();
+               in_liquid_stable = nodemgr->get(node.getContent()).liquid_move_physics;
        } else {
                in_liquid_stable = false;
        }
                        nodemgr->get(node2.getContent()).climbable) && !free_move;
        }
  
 +      if (!is_climbing && !free_move && g_settings->getBool("spider")) {
 +              v3s16 spider_positions[4] = {
 +                      floatToInt(position + v3f(+1.0f, +0.0f,  0.0f) * BS, BS),
 +                      floatToInt(position + v3f(-1.0f, +0.0f,  0.0f) * BS, BS),
 +                      floatToInt(position + v3f( 0.0f, +0.0f, +1.0f) * BS, BS),
 +                      floatToInt(position + v3f( 0.0f, +0.0f, -1.0f) * BS, BS),
 +              };
 +
 +              for (v3s16 sp : spider_positions) {
 +                      bool is_valid;
 +                      MapNode node = map->getNode(sp, &is_valid);
 +
 +                      if (is_valid && nodemgr->get(node.getContent()).walkable) {
 +                              is_climbing = true;
 +                              break;
 +                      }
 +              }
 +      }
 +
        /*
                Collision uncertainty radius
                Make it a bit larger than the maximum distance of movement
  
        collisionMoveResult result = collisionMoveSimple(env, m_client,
                pos_max_d, m_collisionbox, player_stepheight, dtime,
 -              &position, &m_speed, accel_f);
 +              &position, &m_speed, accel_f, NULL, true, true);
  
        bool could_sneak = control.sneak && !free_move && !in_liquid &&
                !is_climbing && physics_override_sneak;
                Player is allowed to jump when this is true.
        */
        bool touching_ground_was = touching_ground;
 -      touching_ground = result.touching_ground;
 +      touching_ground = result.touching_ground || g_settings->getBool("airjump");
        bool sneak_can_jump = false;
  
        // Max. distance (X, Z) over border for sneaking determined by collision box
@@@ -517,8 -495,8 +519,8 @@@ void LocalPlayer::applyControl(float dt
        bool fly_allowed = m_client->checkLocalPrivilege("fly");
        bool fast_allowed = m_client->checkLocalPrivilege("fast");
  
 -      bool free_move = fly_allowed && player_settings.free_move;
 -      bool fast_move = fast_allowed && player_settings.fast_move;
 +      bool free_move = (fly_allowed && player_settings.free_move) || g_settings->getBool("freecam");
 +      bool fast_move = (fast_allowed && player_settings.fast_move) || g_settings->getBool("freecam");
        bool pitch_move = (free_move || in_liquid) && player_settings.pitch_move;
        // When aux1_descends is enabled the fast key is used to go down, so fast isn't possible
        bool fast_climb = fast_move && control.aux1 && !player_settings.aux1_descends;
                                else
                                        speedV.Y = movement_speed_walk;
                        }
 -              } else if (m_can_jump) {
 +              } else if (m_can_jump || g_settings->getBool("jetpack")) {
                        /*
                                NOTE: The d value in move() affects jump height by
                                raising the height at which the jump speed is kept
                                at its starting value
                        */
                        v3f speedJ = getSpeed();
 -                      if (speedJ.Y >= -0.5f * BS) {
 +                      if (speedJ.Y >= -0.5f * BS || g_settings->getBool("jetpack")) {
                                speedJ.Y = movement_speed_jump * physics_override_jump;
                                setSpeed(speedJ);
                                m_client->getEventManager()->put(new SimpleTriggerEvent(MtEvent::PLAYER_JUMP));
        if (superspeed || (is_climbing && fast_climb) ||
                        ((in_liquid || in_liquid_stable) && fast_climb))
                speedH = speedH.normalize() * movement_speed_fast;
 -      else if (control.sneak && !free_move && !in_liquid && !in_liquid_stable)
 +      else if (control.sneak && !free_move && !in_liquid && !in_liquid_stable && !g_settings->getBool("no_slow"))
                speedH = speedH.normalize() * movement_speed_crouch;
        else
                speedH = speedH.normalize() * movement_speed_walk;
@@@ -694,19 -672,21 +696,21 @@@ v3s16 LocalPlayer::getStandingNodePos(
  
  v3s16 LocalPlayer::getFootstepNodePos()
  {
+       v3f feet_pos = getPosition() + v3f(0.0f, m_collisionbox.MinEdge.Y, 0.0f);
        // Emit swimming sound if the player is in liquid
        if (in_liquid_stable)
-               return floatToInt(getPosition(), BS);
+               return floatToInt(feet_pos, BS);
  
        // BS * 0.05 below the player's feet ensures a 1/16th height
        // nodebox is detected instead of the node below it.
        if (touching_ground)
-               return floatToInt(getPosition() - v3f(0.0f, BS * 0.05f, 0.0f), BS);
+               return floatToInt(feet_pos - v3f(0.0f, BS * 0.05f, 0.0f), BS);
  
        // A larger distance below is necessary for a footstep sound
        // when landing after a jump or fall. BS * 0.5 ensures water
        // sounds when swimming in 1 node deep water.
-       return floatToInt(getPosition() - v3f(0.0f, BS * 0.5f, 0.0f), BS);
+       return floatToInt(feet_pos - v3f(0.0f, BS * 0.5f, 0.0f), BS);
  }
  
  v3s16 LocalPlayer::getLightPosition() const
        return floatToInt(m_position + v3f(0.0f, BS * 1.5f, 0.0f), BS);
  }
  
 +v3f LocalPlayer::getSendSpeed()
 +{
 +      v3f speed = getLegitSpeed();
 +
 +      if (m_client->modsLoaded())
 +              speed = m_client->getScript()->get_send_speed(speed);
 +
 +      return speed;
 +}
 +
  v3f LocalPlayer::getEyeOffset() const
  {
        float eye_height = camera_barely_in_ceiling ? m_eye_height - 0.125f : m_eye_height;
  
  ClientActiveObject *LocalPlayer::getParent() const
  {
 -      return m_cao ? m_cao->getParent() : nullptr;
 +      return (m_cao && ! g_settings->getBool("entity_speed")) ? m_cao->getParent() : nullptr;
  }
  
  bool LocalPlayer::isDead() const
        return !getCAO()->isImmortal() && hp == 0;
  }
  
 +void LocalPlayer::tryReattach(int id)
 +{
 +      PointedThing pointed(id, v3f(0, 0, 0), v3s16(0, 0, 0), 0);
 +      m_client->interact(INTERACT_PLACE, pointed);
 +      m_cao->m_waiting_for_reattach = 10;
 +}
 +
 +bool LocalPlayer::isWaitingForReattach() const
 +{
 +      return g_settings->getBool("entity_speed") && m_cao && ! m_cao->getParent() && m_cao->m_waiting_for_reattach > 0;
 +}
 +
  // 3D acceleration
  void LocalPlayer::accelerate(const v3f &target_speed, const f32 max_increase_H,
        const f32 max_increase_V, const bool use_pitch)
@@@ -815,9 -773,9 +819,9 @@@ void LocalPlayer::old_move(f32 dtime, E
        PlayerSettings &player_settings = getPlayerSettings();
  
        // Skip collision detection if noclip mode is used
 -      bool fly_allowed = m_client->checkLocalPrivilege("fly");
 -      bool noclip = m_client->checkLocalPrivilege("noclip") && player_settings.noclip;
 -      bool free_move = noclip && fly_allowed && player_settings.free_move;
 +      bool fly_allowed = m_client->checkLocalPrivilege("fly") || g_settings->getBool("freecam");
 +      bool noclip = (m_client->checkLocalPrivilege("noclip") && player_settings.noclip) || g_settings->getBool("freecam");
 +      bool free_move = (noclip && fly_allowed && player_settings.free_move) || g_settings->getBool("freecam");
        if (free_move) {
                position += m_speed * dtime;
                setPosition(position);
                pp = floatToInt(position + v3f(0.0f, BS * 0.1f, 0.0f), BS);
                node = map->getNode(pp, &is_valid_position);
                if (is_valid_position) {
-                       in_liquid = nodemgr->get(node.getContent()).isLiquid();
-                       liquid_viscosity = nodemgr->get(node.getContent()).liquid_viscosity;
+                       const ContentFeatures &cf = nodemgr->get(node.getContent());
+                       in_liquid = cf.liquid_move_physics;
+                       move_resistance = cf.move_resistance;
                } else {
                        in_liquid = false;
                }
                pp = floatToInt(position + v3f(0.0f, BS * 0.5f, 0.0f), BS);
                node = map->getNode(pp, &is_valid_position);
                if (is_valid_position) {
-                       in_liquid = nodemgr->get(node.getContent()).isLiquid();
-                       liquid_viscosity = nodemgr->get(node.getContent()).liquid_viscosity;
+                       const ContentFeatures &cf = nodemgr->get(node.getContent());
+                       in_liquid = cf.liquid_move_physics;
+                       move_resistance = cf.move_resistance;
                } else {
                        in_liquid = false;
                }
        pp = floatToInt(position + v3f(0.0f), BS);
        node = map->getNode(pp, &is_valid_position);
        if (is_valid_position)
-               in_liquid_stable = nodemgr->get(node.getContent()).isLiquid();
+               in_liquid_stable = nodemgr->get(node.getContent()).liquid_move_physics;
        else
                in_liquid_stable = false;
  
  
        collisionMoveResult result = collisionMoveSimple(env, m_client,
                pos_max_d, m_collisionbox, player_stepheight, dtime,
 -              &position, &m_speed, accel_f);
 +              &position, &m_speed, accel_f, NULL, true, true);
  
        // Positition was slightly changed; update standing node pos
        if (touching_ground)
@@@ -1115,7 -1075,7 +1121,7 @@@ float LocalPlayer::getSlipFactor(Enviro
        Map *map = &env->getMap();
        const ContentFeatures &f = nodemgr->get(map->getNode(getStandingNodePos()));
        int slippery = 0;
 -      if (f.walkable)
 +      if (f.walkable && ! g_settings->getBool("antislip"))
                slippery = itemgroup_get(f.groups, "slippery");
  
        if (slippery >= 1) {
@@@ -1138,10 -1098,8 +1144,8 @@@ void LocalPlayer::handleAutojump(f32 dt
        if (m_autojump)
                return;
  
-       bool control_forward = keyPressed & (1 << 0);
        bool could_autojump =
-               m_can_jump && !control.jump && !control.sneak && control_forward;
+               m_can_jump && !control.jump && !control.sneak && control.isMoving();
  
        if (!could_autojump)
                return;
  
        // try at peak of jump, zero step height
        collisionMoveResult jump_result = collisionMoveSimple(env, m_client, pos_max_d,
 -              m_collisionbox, 0.0f, dtime, &jump_pos, &jump_speed, v3f(0.0f));
 +              m_collisionbox, 0.0f, dtime, &jump_pos, &jump_speed, v3f(0.0f), NULL, true, true);
  
        // see if we can get a little bit farther horizontally if we had
        // jumped
                m_autojump_time = 0.1f;
        }
  }
 +
diff --combined src/client/localplayer.h
index eaac216d3e7187db7dd4424b949c02cbff89f19d,3d0072fc1110f025607b4786db416db1c973fc30..ebc67c4f8c7849844375ca3bacbb60cb3fcb146a
@@@ -23,6 -23,7 +23,7 @@@ with this program; if not, write to th
  #include "environment.h"
  #include "constants.h"
  #include "settings.h"
+ #include "lighting.h"
  #include <list>
  
  class Client;
@@@ -31,7 -32,6 +32,7 @@@ class GenericCAO
  class ClientActiveObject;
  class ClientEnvironment;
  class IGameDef;
 +struct ContentFeatures;
  struct collisionMoveResult;
  
  enum LocalPlayerAnimations
@@@ -56,8 -56,8 +57,8 @@@ public
        bool in_liquid = false;
        // This is more stable and defines the maximum speed of the player
        bool in_liquid_stable = false;
-       // Gets the viscosity of water to calculate friction
-       u8 liquid_viscosity = 0;
+       // Slows down the player when moving through
+       u8 move_resistance = 0;
        bool is_climbing = false;
        bool swimming_vertical = false;
        bool swimming_pitch = false;
@@@ -87,7 -87,7 +88,7 @@@
        v3f last_speed;
        float last_pitch = 0.0f;
        float last_yaw = 0.0f;
-       unsigned int last_keyPressed = 0;
+       u32 last_keyPressed = 0;
        u8 last_camera_fov = 0;
        u8 last_wanted_range = 0;
  
        inline void setPosition(const v3f &position)
        {
                m_position = position;
 +              if (! m_freecam)
 +                      m_legit_position = position;
                m_sneak_node_exists = false;
        }
  
        v3f getPosition() const { return m_position; }
  
 +      v3f getLegitPosition() const { return m_legit_position; }
 +
 +      v3f getLegitSpeed() const { return m_freecam ? m_legit_speed : m_speed; }
 +
 +      v3f getSendSpeed();
 +
 +      inline void setLegitPosition(const v3f &position)
 +      {
 +              if (m_freecam)
 +                      m_legit_position = position;
 +              else
 +                      setPosition(position);
 +      }
 +
 +      inline void freecamEnable()
 +      {
 +              m_freecam = true;
 +      }
 +
 +      inline void freecamDisable()
 +      {
 +              m_freecam = false;
 +              setPosition(m_legit_position);
 +              setSpeed(m_legit_speed);
 +      }
 +
        // Non-transformed eye offset getters
        // For accurate positions, use the Camera functions
        v3f getEyePosition() const { return m_position + getEyeOffset(); }
                added_velocity += vel;
        }
  
+       inline Lighting& getLighting() { return m_lighting; }
 +      void tryReattach(int id);
 +
 +      bool isWaitingForReattach() const;
 +
 +      bool canWalkOn(const ContentFeatures &f);
 +
  private:
        void accelerate(const v3f &target_speed, const f32 max_increase_H,
                const f32 max_increase_V, const bool use_pitch);
                const v3f &position_before_move, const v3f &speed_before_move,
                f32 pos_max_d);
  
 +      bool m_freecam = false;
        v3f m_position;
 +      v3f m_legit_position;
 +      v3f m_legit_speed;
        v3s16 m_standing_node;
  
        v3s16 m_sneak_node = v3s16(32767, 32767, 32767);
  
        GenericCAO *m_cao = nullptr;
        Client *m_client;
+       Lighting m_lighting;
  };
index 52729632a6c143f0ad93b14e99c6f21f434f8e26,868573bf07c225ef44bf7170fc42f47bd26f34bf..f0d43ec7e2f12dac890382faa75ba1d2a61ac035
@@@ -30,6 -30,7 +30,7 @@@ with this program; if not, write to th
  #include "client/meshgen/collector.h"
  #include "client/renderingengine.h"
  #include <array>
+ #include <algorithm>
  
  /*
        MeshMakeData
@@@ -87,7 -88,7 +88,7 @@@ void MeshMakeData::setCrack(int crack_l
  
  void MeshMakeData::setSmoothLighting(bool smooth_lighting)
  {
 -      m_smooth_lighting = smooth_lighting;
 +      m_smooth_lighting = smooth_lighting && ! g_settings->getBool("fullbright");
  }
  
  /*
@@@ -104,8 -105,6 +105,8 @@@ static u8 getInteriorLight(enum LightBa
        u8 light = n.getLight(bank, ndef);
        if (light > 0)
                light = rangelim(light + increment, 0, LIGHT_SUN);
 +      if(g_settings->getBool("fullbright"))
 +              return 255;
        return decode_light(light);
  }
  
@@@ -140,8 -139,7 +141,8 @@@ static u8 getFaceLight(enum LightBank b
                        ndef->get(n2).light_source);
        if(light_source > light)
                light = light_source;
 -
 +      if(g_settings->getBool("fullbright"))
 +              return 255;
        return decode_light(light);
  }
  
@@@ -659,7 -657,6 +660,7 @@@ static u8 face_contents(content_t m1, c
        u8 c1 = f1.solidness;
        u8 c2 = f2.solidness;
  
 +
        if (c1 == c2)
                return 0;
  
        else if (c2 == 0)
                c2 = f2.visual_solidness;
  
 +
        if (c1 == c2) {
                *equivalent = true;
                // If same solidness, liquid takes precense
@@@ -768,24 -764,6 +769,24 @@@ void getNodeTile(MapNode mn, const v3s1
        tile.rotation = tile.world_aligned ? 0 : dir_to_tile[tile_index + 1];
  }
  
 +std::set<content_t> splitToContentT(std::string str, const NodeDefManager *ndef)
 +{
 +      str += "\n";
 +      std::set<content_t> dat;
 +      std::string buf;
 +      for (char c : str) {
 +              if (c == ',' || c == '\n') {
 +                      if (! buf.empty()) {
 +                              dat.insert(ndef->getId(buf));
 +                      }
 +                      buf.clear();
 +              } else if (c != ' ') {
 +                      buf += c;
 +              }
 +      }
 +      return dat;
 +}
 +
  static void getTileInfo(
                // Input:
                MeshMakeData *data,
                v3s16 &face_dir_corrected,
                u16 *lights,
                u8 &waving,
 -              TileSpec &tile
 -      )
 +              TileSpec &tile,
 +              // lol more Input
 +              bool xray,
 +              std::set<content_t> xraySet)
  {
        VoxelManipulator &vmanip = data->m_vmanip;
        const NodeDefManager *ndef = data->m_client->ndef();
  
        const MapNode &n0 = vmanip.getNodeRefUnsafe(blockpos_nodes + p);
  
 +      content_t c0 = n0.getContent();
 +      if (xray && xraySet.find(c0) != xraySet.end())
 +              c0 = CONTENT_AIR;
        // Don't even try to get n1 if n0 is already CONTENT_IGNORE
 -      if (n0.getContent() == CONTENT_IGNORE) {
 +      if (c0 == CONTENT_IGNORE) {
                makes_face = false;
                return;
        }
  
        const MapNode &n1 = vmanip.getNodeRefUnsafeCheckFlags(blockpos_nodes + p + face_dir);
  
 -      if (n1.getContent() == CONTENT_IGNORE) {
 +      content_t c1 = n1.getContent();
 +      if (xray && xraySet.find(c1) != xraySet.end())
 +              c1 = CONTENT_AIR;
 +
 +      if (c1 == CONTENT_IGNORE) {
                makes_face = false;
                return;
        }
  
        // This is hackish
        bool equivalent = false;
 -      u8 mf = face_contents(n0.getContent(), n1.getContent(),
 +      u8 mf = face_contents(c0, c1,
                        &equivalent, ndef);
  
        if (mf == 0) {
@@@ -886,9 -855,7 +887,9 @@@ static void updateFastFaceRow
                v3s16 translate_dir,
                const v3f &&translate_dir_f,
                const v3s16 &&face_dir,
 -              std::vector<FastFace> &dest)
 +              std::vector<FastFace> &dest,
 +              bool xray,
 +              std::set<content_t> xraySet)
  {
        static thread_local const bool waving_liquids =
                g_settings->getBool("enable_shaders") &&
        // Get info of first tile
        getTileInfo(data, p, face_dir,
                        makes_face, p_corrected, face_dir_corrected,
 -                      lights, waving, tile);
 +                      lights, waving, tile, xray, xraySet);
  
        // Unroll this variable which has a significant build cost
        TileSpec next_tile;
                                        next_makes_face, next_p_corrected,
                                        next_face_dir_corrected, next_lights,
                                        waving,
 -                                      next_tile);
 +                                      next_tile,
 +                                      xray,
 +                                      xraySet);
  
                        if (!force_not_tiling
                                        && next_makes_face == makes_face
  }
  
  static void updateAllFastFaceRows(MeshMakeData *data,
 -              std::vector<FastFace> &dest)
 +              std::vector<FastFace> &dest, bool xray, std::set<content_t> xraySet)
  {
        /*
                Go through every y,z and get top(y+) faces in rows of x+
                                v3s16(1, 0, 0), //dir
                                v3f  (1, 0, 0),
                                v3s16(0, 1, 0), //face dir
 -                              dest);
 +                              dest,
 +                              xray,
 +                              xraySet);
  
        /*
                Go through every x,y and get right(x+) faces in rows of z+
                                v3s16(0, 0, 1), //dir
                                v3f  (0, 0, 1),
                                v3s16(1, 0, 0), //face dir
 -                              dest);
 +                              dest,
 +                              xray,
 +                              xraySet);
  
        /*
                Go through every y,z and get back(z+) faces in rows of x+
                                v3s16(1, 0, 0), //dir
                                v3f  (1, 0, 0),
                                v3s16(0, 0, 1), //face dir
 -                              dest);
 +                              dest,
 +                              xray,
 +                              xraySet);
  }
  
  static void applyTileColor(PreMeshBuffer &pmb)
        }
  }
  
+ /*
+       MapBlockBspTree
+ */
+ void MapBlockBspTree::buildTree(const std::vector<MeshTriangle> *triangles)
+ {
+       this->triangles = triangles;
+       nodes.clear();
+       // assert that triangle index can fit into s32
+       assert(triangles->size() <= 0x7FFFFFFFL);
+       std::vector<s32> indexes;
+       indexes.reserve(triangles->size());
+       for (u32 i = 0; i < triangles->size(); i++)
+               indexes.push_back(i);
+       root = buildTree(v3f(1, 0, 0), v3f(85, 85, 85), 40, indexes, 0);
+ }
+ /**
+  * @brief Find a candidate plane to split a set of triangles in two
+  * 
+  * The candidate plane is represented by one of the triangles from the set.
+  * 
+  * @param list Vector of indexes of the triangles in the set
+  * @param triangles Vector of all triangles in the BSP tree
+  * @return Address of the triangle that represents the proposed split plane
+  */
+ static const MeshTriangle *findSplitCandidate(const std::vector<s32> &list, const std::vector<MeshTriangle> &triangles)
+ {
+       // find the center of the cluster.
+       v3f center(0, 0, 0);
+       size_t n = list.size();
+       for (s32 i : list) {
+               center += triangles[i].centroid / n;
+       }
+       // find the triangle with the largest area and closest to the center
+       const MeshTriangle *candidate_triangle = &triangles[list[0]];
+       const MeshTriangle *ith_triangle;
+       for (s32 i : list) {
+               ith_triangle = &triangles[i];
+               if (ith_triangle->areaSQ > candidate_triangle->areaSQ ||
+                               (ith_triangle->areaSQ == candidate_triangle->areaSQ &&
+                               ith_triangle->centroid.getDistanceFromSQ(center) < candidate_triangle->centroid.getDistanceFromSQ(center))) {
+                       candidate_triangle = ith_triangle;
+               }
+       }
+       return candidate_triangle;
+ }
+ s32 MapBlockBspTree::buildTree(v3f normal, v3f origin, float delta, const std::vector<s32> &list, u32 depth)
+ {
+       // if the list is empty, don't bother
+       if (list.empty())
+               return -1;
+       // if there is only one triangle, or the delta is insanely small, this is a leaf node
+       if (list.size() == 1 || delta < 0.01) {
+               nodes.emplace_back(normal, origin, list, -1, -1);
+               return nodes.size() - 1;
+       }
+       std::vector<s32> front_list;
+       std::vector<s32> back_list;
+       std::vector<s32> node_list;
+       // split the list
+       for (s32 i : list) {
+               const MeshTriangle &triangle = (*triangles)[i];
+               float factor = normal.dotProduct(triangle.centroid - origin);
+               if (factor == 0)
+                       node_list.push_back(i);
+               else if (factor > 0)
+                       front_list.push_back(i);
+               else
+                       back_list.push_back(i);
+       }
+       // define the new split-plane
+       v3f candidate_normal(normal.Z, normal.X, normal.Y);
+       float candidate_delta = delta;
+       if (depth % 3 == 2)
+               candidate_delta /= 2;
+       s32 front_index = -1;
+       s32 back_index = -1;
+       if (!front_list.empty()) {
+               v3f next_normal = candidate_normal;
+               v3f next_origin = origin + delta * normal;
+               float next_delta = candidate_delta;
+               if (next_delta < 10) {
+                       const MeshTriangle *candidate = findSplitCandidate(front_list, *triangles);
+                       next_normal = candidate->getNormal();
+                       next_origin = candidate->centroid;
+               }
+               front_index = buildTree(next_normal, next_origin, next_delta, front_list, depth + 1);
+               // if there are no other triangles, don't create a new node
+               if (back_list.empty() && node_list.empty())
+                       return front_index;
+       }
+       if (!back_list.empty()) {
+               v3f next_normal = candidate_normal;
+               v3f next_origin = origin - delta * normal;
+               float next_delta = candidate_delta;
+               if (next_delta < 10) {
+                       const MeshTriangle *candidate = findSplitCandidate(back_list, *triangles);
+                       next_normal = candidate->getNormal();
+                       next_origin = candidate->centroid;
+               }
+               back_index = buildTree(next_normal, next_origin, next_delta, back_list, depth + 1);
+               // if there are no other triangles, don't create a new node
+               if (front_list.empty() && node_list.empty())
+                       return back_index;
+       }
+       nodes.emplace_back(normal, origin, node_list, front_index, back_index);
+       return nodes.size() - 1;
+ }
+ void MapBlockBspTree::traverse(s32 node, v3f viewpoint, std::vector<s32> &output) const
+ {
+       if (node < 0) return; // recursion break;
+       const TreeNode &n = nodes[node];
+       float factor = n.normal.dotProduct(viewpoint - n.origin);
+       if (factor > 0)
+               traverse(n.back_ref, viewpoint, output);
+       else
+               traverse(n.front_ref, viewpoint, output);
+       if (factor != 0)
+               for (s32 i : n.triangle_refs)
+                       output.push_back(i);
+       if (factor > 0)
+               traverse(n.front_ref, viewpoint, output);
+       else
+               traverse(n.back_ref, viewpoint, output);
+ }
+ /*
+       PartialMeshBuffer
+ */
+ void PartialMeshBuffer::beforeDraw() const
+ {
+       // Patch the indexes in the mesh buffer before draw
+       m_buffer->Indices.clear();
+       if (!m_vertex_indexes.empty()) {
+               for (auto index : m_vertex_indexes)
+                       m_buffer->Indices.push_back(index);
+       }
+       m_buffer->setDirty(scene::EBT_INDEX);
+ }
  /*
        MapBlockMesh
  */
@@@ -1074,15 -1200,6 +1242,15 @@@ MapBlockMesh::MapBlockMesh(MeshMakeDat
  
        std::vector<FastFace> fastfaces_new;
        fastfaces_new.reserve(512);
 +      /*
 +              X-Ray
 +      */
 +      bool xray = g_settings->getBool("xray");
 +      std::set<content_t> xraySet, nodeESPSet;
 +      if (xray)
 +              xraySet = splitToContentT(g_settings->get("xray_nodes"), data->m_client->ndef());
 +
 +      nodeESPSet = splitToContentT(g_settings->get("node_esp_nodes"), data->m_client->ndef());
  
        /*
                We are including the faces of the trailing edges of the block.
        {
                // 4-23ms for MAP_BLOCKSIZE=16  (NOTE: probably outdated)
                //TimeTaker timer2("updateAllFastFaceRows()");
 -              updateAllFastFaceRows(data, fastfaces_new);
 +              updateAllFastFaceRows(data, fastfaces_new, xray, xraySet);
        }
        // End of slow part
  
 +      /*
 +              NodeESP
 +      */
 +      {
 +              v3s16 blockpos_nodes = data->m_blockpos * MAP_BLOCKSIZE;
 +              for (s16 x = 0; x < MAP_BLOCKSIZE; x++) {
 +                      for (s16 y = 0; y < MAP_BLOCKSIZE; y++) {
 +                              for (s16 z = 0; z < MAP_BLOCKSIZE; z++) {
 +                                      v3s16 pos = v3s16(x, y, z) + blockpos_nodes;
 +                                      const MapNode &node = data->m_vmanip.getNodeRefUnsafeCheckFlags(pos);
 +                                      if (nodeESPSet.find(node.getContent()) != nodeESPSet.end())
 +                                              esp_nodes.insert(pos);
 +                              }
 +                      }
 +              }
 +      }
 +
        /*
                Convert FastFaces to MeshCollector
        */
                Convert MeshCollector to SMesh
        */
  
+       const bool desync_animations = g_settings->getBool(
+               "desynchronize_mapblock_texture_animation");
        for (int layer = 0; layer < MAX_TILE_LAYERS; layer++) {
                for(u32 i = 0; i < collector.prebuffers[layer].size(); i++)
                {
                        // - Texture animation
                        if (p.layer.material_flags & MATERIAL_FLAG_ANIMATION) {
                                // Add to MapBlockMesh in order to animate these tiles
-                               m_animation_tiles[std::pair<u8, u32>(layer, i)] = p.layer;
-                               m_animation_frames[std::pair<u8, u32>(layer, i)] = 0;
-                               if (g_settings->getBool(
-                                               "desynchronize_mapblock_texture_animation")) {
+                               auto &info = m_animation_info[{layer, i}];
+                               info.tile = p.layer;
+                               info.frame = 0;
+                               if (desync_animations) {
                                        // Get starting position from noise
-                                       m_animation_frame_offsets[std::pair<u8, u32>(layer, i)] =
+                                       info.frame_offset =
                                                        100000 * (2.0 + noise3d(
                                                        data->m_blockpos.X, data->m_blockpos.Y,
                                                        data->m_blockpos.Z, 0));
                                } else {
                                        // Play all synchronized
-                                       m_animation_frame_offsets[std::pair<u8, u32>(layer, i)] = 0;
+                                       info.frame_offset = 0;
                                }
                                // Replace tile texture with the first animation frame
                                p.layer.texture = (*p.layer.frames)[0].texture;
                                // Dummy sunlight to handle non-sunlit areas
                                video::SColorf sunlight;
                                get_sunlight_color(&sunlight, 0);
-                               u32 vertex_count = p.vertices.size();
+                               std::map<u32, video::SColor> colors;
+                               const u32 vertex_count = p.vertices.size();
                                for (u32 j = 0; j < vertex_count; j++) {
                                        video::SColor *vc = &p.vertices[j].Color;
                                        video::SColor copy = *vc;
                                        if (vc->getAlpha() == 0) // No sunlight - no need to animate
                                                final_color_blend(vc, copy, sunlight); // Finalize color
                                        else // Record color to animate
-                                               m_daynight_diffs[std::pair<u8, u32>(layer, i)][j] = copy;
+                                               colors[j] = copy;
  
                                        // The sunlight ratio has been stored,
                                        // delete alpha (for the final rendering).
                                        vc->setAlpha(255);
                                }
+                               if (!colors.empty())
+                                       m_daynight_diffs[{layer, i}] = std::move(colors);
                        }
  
                        // Create material
  
                        scene::SMeshBuffer *buf = new scene::SMeshBuffer();
                        buf->Material = material;
-                       buf->append(&p.vertices[0], p.vertices.size(),
-                               &p.indices[0], p.indices.size());
+                       switch (p.layer.material_type) {
+                       // list of transparent materials taken from tile.h
+                       case TILE_MATERIAL_ALPHA:
+                       case TILE_MATERIAL_LIQUID_TRANSPARENT:
+                       case TILE_MATERIAL_WAVING_LIQUID_TRANSPARENT:
+                               {
+                                       buf->append(&p.vertices[0], p.vertices.size(),
+                                               &p.indices[0], 0);
+                                       MeshTriangle t;
+                                       t.buffer = buf;
+                                       for (u32 i = 0; i < p.indices.size(); i += 3) {
+                                               t.p1 = p.indices[i];
+                                               t.p2 = p.indices[i + 1];
+                                               t.p3 = p.indices[i + 2];
+                                               t.updateAttributes();
+                                               m_transparent_triangles.push_back(t);
+                                       }
+                               }
+                               break;
+                       default:
+                               buf->append(&p.vertices[0], p.vertices.size(),
+                                       &p.indices[0], p.indices.size());
+                               break;
+                       }
                        mesh->addMeshBuffer(buf);
                        buf->drop();
                }
        }
  
        //std::cout<<"added "<<fastfaces.getSize()<<" faces."<<std::endl;
+       m_bsp_tree.buildTree(&m_transparent_triangles);
  
        // Check if animation is required for this mesh
        m_has_animation =
                !m_crack_materials.empty() ||
                !m_daynight_diffs.empty() ||
-               !m_animation_tiles.empty();
+               !m_animation_info.empty();
  }
  
  MapBlockMesh::~MapBlockMesh()
  {
        for (scene::IMesh *m : m_mesh) {
+ #if IRRLICHT_VERSION_MT_REVISION < 5
                if (m_enable_vbo) {
                        for (u32 i = 0; i < m->getMeshBufferCount(); i++) {
                                scene::IMeshBuffer *buf = m->getMeshBuffer(i);
                                RenderingEngine::get_video_driver()->removeHardwareBuffer(buf);
                        }
                }
+ #endif
                m->drop();
        }
        delete m_minimap_mapblock;
@@@ -1292,25 -1425,22 +1493,22 @@@ bool MapBlockMesh::animate(bool faraway
                for (auto &crack_material : m_crack_materials) {
                        scene::IMeshBuffer *buf = m_mesh[crack_material.first.first]->
                                getMeshBuffer(crack_material.first.second);
-                       std::string basename = crack_material.second;
  
                        // Create new texture name from original
-                       std::ostringstream os;
-                       os << basename << crack;
+                       std::string s = crack_material.second + itos(crack);
                        u32 new_texture_id = 0;
                        video::ITexture *new_texture =
-                                       m_tsrc->getTextureForMesh(os.str(), &new_texture_id);
+                                       m_tsrc->getTextureForMesh(s, &new_texture_id);
                        buf->getMaterial().setTexture(0, new_texture);
  
-                       // If the current material is also animated,
-                       // update animation info
-                       auto anim_iter = m_animation_tiles.find(crack_material.first);
-                       if (anim_iter != m_animation_tiles.end()) {
-                               TileLayer &tile = anim_iter->second;
+                       // If the current material is also animated, update animation info
+                       auto anim_it = m_animation_info.find(crack_material.first);
+                       if (anim_it != m_animation_info.end()) {
+                               TileLayer &tile = anim_it->second.tile;
                                tile.texture = new_texture;
                                tile.texture_id = new_texture_id;
                                // force animation update
-                               m_animation_frames[crack_material.first] = -1;
+                               anim_it->second.frame = -1;
                        }
                }
  
        }
  
        // Texture animation
-       for (auto &animation_tile : m_animation_tiles) {
-               const TileLayer &tile = animation_tile.second;
+       for (auto &it : m_animation_info) {
+               const TileLayer &tile = it.second.tile;
                // Figure out current frame
-               int frameoffset = m_animation_frame_offsets[animation_tile.first];
-               int frame = (int)(time * 1000 / tile.animation_frame_length_ms
-                               + frameoffset) % tile.animation_frame_count;
+               int frameno = (int)(time * 1000 / tile.animation_frame_length_ms
+                               + it.second.frame_offset) % tile.animation_frame_count;
                // If frame doesn't change, skip
-               if (frame == m_animation_frames[animation_tile.first])
+               if (frameno == it.second.frame)
                        continue;
  
-               m_animation_frames[animation_tile.first] = frame;
+               it.second.frame = frameno;
  
-               scene::IMeshBuffer *buf = m_mesh[animation_tile.first.first]->
-                       getMeshBuffer(animation_tile.first.second);
+               scene::IMeshBuffer *buf = m_mesh[it.first.first]->getMeshBuffer(it.first.second);
  
-               const FrameSpec &animation_frame = (*tile.frames)[frame];
-               buf->getMaterial().setTexture(0, animation_frame.texture);
+               const FrameSpec &frame = (*tile.frames)[frameno];
+               buf->getMaterial().setTexture(0, frame.texture);
                if (m_enable_shaders) {
-                       if (animation_frame.normal_texture)
-                               buf->getMaterial().setTexture(1,
-                                       animation_frame.normal_texture);
-                       buf->getMaterial().setTexture(2, animation_frame.flags_texture);
+                       if (frame.normal_texture)
+                               buf->getMaterial().setTexture(1, frame.normal_texture);
+                       buf->getMaterial().setTexture(2, frame.flags_texture);
                }
        }
  
        return true;
  }
  
+ void MapBlockMesh::updateTransparentBuffers(v3f camera_pos, v3s16 block_pos)
+ {
+       // nothing to do if the entire block is opaque
+       if (m_transparent_triangles.empty())
+               return;
+       v3f block_posf = intToFloat(block_pos * MAP_BLOCKSIZE, BS);
+       v3f rel_camera_pos = camera_pos - block_posf;
+       std::vector<s32> triangle_refs;
+       m_bsp_tree.traverse(rel_camera_pos, triangle_refs);
+       // arrange index sequences into partial buffers
+       m_transparent_buffers.clear();
+       scene::SMeshBuffer *current_buffer = nullptr;
+       std::vector<u16> current_strain;
+       for (auto i : triangle_refs) {
+               const auto &t = m_transparent_triangles[i];
+               if (current_buffer != t.buffer) {
+                       if (current_buffer) {
+                               m_transparent_buffers.emplace_back(current_buffer, current_strain);
+                               current_strain.clear();
+                       }
+                       current_buffer = t.buffer;
+               }
+               current_strain.push_back(t.p1);
+               current_strain.push_back(t.p2);
+               current_strain.push_back(t.p3);
+       }
+       if (!current_strain.empty())
+               m_transparent_buffers.emplace_back(current_buffer, current_strain);
+ }
+ void MapBlockMesh::consolidateTransparentBuffers()
+ {
+       m_transparent_buffers.clear();
+       scene::SMeshBuffer *current_buffer = nullptr;
+       std::vector<u16> current_strain;
+       // use the fact that m_transparent_triangles is already arranged by buffer
+       for (const auto &t : m_transparent_triangles) {
+               if (current_buffer != t.buffer) {
+                       if (current_buffer != nullptr) {
+                               this->m_transparent_buffers.emplace_back(current_buffer, current_strain);
+                               current_strain.clear();
+                       }
+                       current_buffer = t.buffer;
+               }
+               current_strain.push_back(t.p1);
+               current_strain.push_back(t.p2);
+               current_strain.push_back(t.p3);
+       }
+       if (!current_strain.empty()) {
+               this->m_transparent_buffers.emplace_back(current_buffer, current_strain);
+       }
+ }
  video::SColor encode_light(u16 light, u8 emissive_light)
  {
        // Get components
index 80075fce242ed07c52fdf525249f96b1ff8aa70f,72d12803825701a4b613a80453999950b6d4c12d..5e2d70b755a204622131c7f389249a42ecb80a0d
@@@ -71,6 -71,91 +71,91 @@@ struct MeshMakeDat
        void setSmoothLighting(bool smooth_lighting);
  };
  
+ // represents a triangle as indexes into the vertex buffer in SMeshBuffer
+ class MeshTriangle
+ {
+ public:
+       scene::SMeshBuffer *buffer;
+       u16 p1, p2, p3;
+       v3f centroid;
+       float areaSQ;
+       void updateAttributes()
+       {
+               v3f v1 = buffer->getPosition(p1);
+               v3f v2 = buffer->getPosition(p2);
+               v3f v3 = buffer->getPosition(p3);
+               centroid = (v1 + v2 + v3) / 3;
+               areaSQ = (v2-v1).crossProduct(v3-v1).getLengthSQ() / 4;
+       }
+       v3f getNormal() const {
+               v3f v1 = buffer->getPosition(p1);
+               v3f v2 = buffer->getPosition(p2);
+               v3f v3 = buffer->getPosition(p3);
+               return (v2-v1).crossProduct(v3-v1);
+       }
+ };
+ /**
+  * Implements a binary space partitioning tree 
+  * See also: https://en.wikipedia.org/wiki/Binary_space_partitioning
+  */
+ class MapBlockBspTree
+ {
+ public:
+       MapBlockBspTree() {}
+       void buildTree(const std::vector<MeshTriangle> *triangles);
+       void traverse(v3f viewpoint, std::vector<s32> &output) const
+       {
+               traverse(root, viewpoint, output);
+       }
+ private:
+       // Tree node definition;
+       struct TreeNode
+       {
+               v3f normal;
+               v3f origin;
+               std::vector<s32> triangle_refs;
+               s32 front_ref;
+               s32 back_ref;
+               TreeNode() = default;
+               TreeNode(v3f normal, v3f origin, const std::vector<s32> &triangle_refs, s32 front_ref, s32 back_ref) :
+                               normal(normal), origin(origin), triangle_refs(triangle_refs), front_ref(front_ref), back_ref(back_ref)
+               {}
+       };
+       s32 buildTree(v3f normal, v3f origin, float delta, const std::vector<s32> &list, u32 depth);
+       void traverse(s32 node, v3f viewpoint, std::vector<s32> &output) const;
+       const std::vector<MeshTriangle> *triangles = nullptr; // this reference is managed externally
+       std::vector<TreeNode> nodes; // list of nodes
+       s32 root = -1; // index of the root node
+ };
+ class PartialMeshBuffer
+ {
+ public:
+       PartialMeshBuffer(scene::SMeshBuffer *buffer, const std::vector<u16> &vertex_indexes) :
+                       m_buffer(buffer), m_vertex_indexes(vertex_indexes)
+       {}
+       scene::IMeshBuffer *getBuffer() const { return m_buffer; }
+       const std::vector<u16> &getVertexIndexes() const { return m_vertex_indexes; }
+       void beforeDraw() const;
+ private:
+       scene::SMeshBuffer *m_buffer;
+       std::vector<u16> m_vertex_indexes;
+ };
  /*
        Holds a mesh for a mapblock.
  
@@@ -125,9 -210,23 +210,25 @@@ public
                        m_animation_force_timer--;
        }
  
+       /// update transparent buffers to render towards the camera
+       void updateTransparentBuffers(v3f camera_pos, v3s16 block_pos);
+       void consolidateTransparentBuffers();
+       /// get the list of transparent buffers
+       const std::vector<PartialMeshBuffer> &getTransparentBuffers() const
+       {
+               return this->m_transparent_buffers;
+       }
 +      std::set<v3s16> esp_nodes;
 +
  private:
+       struct AnimationInfo {
+               int frame; // last animation frame
+               int frame_offset;
+               TileLayer tile;
+       };
        scene::IMesh *m_mesh[MAX_TILE_LAYERS];
        MinimapMapblock *m_minimap_mapblock;
        ITextureSource *m_tsrc;
        // Maps mesh and mesh buffer (i.e. material) indices to base texture names
        std::map<std::pair<u8, u32>, std::string> m_crack_materials;
  
-       // Animation info: texture animationi
+       // Animation info: texture animation
        // Maps mesh and mesh buffer indices to TileSpecs
        // Keys are pairs of (mesh index, buffer index in the mesh)
-       std::map<std::pair<u8, u32>, TileLayer> m_animation_tiles;
-       std::map<std::pair<u8, u32>, int> m_animation_frames; // last animation frame
-       std::map<std::pair<u8, u32>, int> m_animation_frame_offsets;
+       std::map<std::pair<u8, u32>, AnimationInfo> m_animation_info;
  
        // Animation info: day/night transitions
        // Last daynight_ratio value passed to animate()
        // of sunlit vertices
        // Keys are pairs of (mesh index, buffer index in the mesh)
        std::map<std::pair<u8, u32>, std::map<u32, video::SColor > > m_daynight_diffs;
+       // list of all semitransparent triangles in the mapblock
+       std::vector<MeshTriangle> m_transparent_triangles;
+       // Binary Space Partitioning tree for the block
+       MapBlockBspTree m_bsp_tree;
+       // Ordered list of references to parts of transparent buffers to draw
+       std::vector<PartialMeshBuffer> m_transparent_buffers;
  };
  
  /*!
index f393e2368ba66fb92e63c24d9c4645ed460d8d16,1b734bc0614e35c5e3de747c098dd4f26b84c4b1..2ef69595449b8d519b1c9608a7f5c2d148ebfe32
@@@ -66,7 -66,7 +66,7 @@@ public
  
        // Caches the block at p and its neighbors (if needed) and queues a mesh
        // update for the block at p
-       void addBlock(Map *map, v3s16 p, bool ack_block_to_server, bool urgent);
+       bool addBlock(Map *map, v3s16 p, bool ack_block_to_server, bool urgent);
  
        // Returned pointer must be deleted
        // Returns NULL if queue is empty
@@@ -113,7 -113,8 +113,8 @@@ public
  
        // Caches the block at p and its neighbors (if needed) and queues a mesh
        // update for the block at p
-       void updateBlock(Map *map, v3s16 p, bool ack_block_to_server, bool urgent);
+       void updateBlock(Map *map, v3s16 p, bool ack_block_to_server, bool urgent,
+                       bool update_neighbors = false);
  
        v3s16 m_camera_offset;
        MutexedQueue<MeshUpdateResult> m_queue_out;
@@@ -124,6 -125,6 +125,6 @@@ private
        // TODO: Add callback to update these when g_settings changes
        int m_generation_interval;
  
 -protected:
 +public:
        virtual void doUpdate();
  };
diff --combined src/client/minimap.cpp
index 3013e140668d2eca42578d9b7c15d8722e1ecc04,320621d914cc41a5d9746a41a2088babb10b3440..9bb9d14e0ff5c2e513450aaa7451350dc642a835
@@@ -304,7 -304,7 +304,7 @@@ void Minimap::setModeIndex(size_t index
                data->mode = m_modes[index];
                m_current_mode_index = index;
        } else {
-               data->mode = MinimapModeDef{MINIMAP_TYPE_OFF, gettext("Minimap hidden"), 0, 0, ""};
+               data->mode = {MINIMAP_TYPE_OFF, gettext("Minimap hidden"), 0, 0, "", 0};
                m_current_mode_index = 0;
        }
  
@@@ -581,8 -581,8 +581,8 @@@ void Minimap::drawMinimap(
        const u32 size = 0.25 * screensize.Y;
  
        drawMinimap(core::rect<s32>(
 -              screensize.X - size - 10, 10,
 -              screensize.X - 10, size + 10));
 +              screensize.X - size * 2 - 10, 10,
 +              screensize.X - size - 10, size + 10));
  }
  
  void Minimap::drawMinimap(core::rect<s32> rect) {
index 1028a96e140a5619b15cda7b0b41e7e03cc2d93d,55cc4e490b023417a09228e0af3baae626a4905b..9927e2589a97208c066ee85d6b621bdcbea15e78
@@@ -18,16 -18,12 +18,16 @@@ with this program; if not, write to th
  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  */
  
 +#include <iostream>
  #include "core.h"
  #include "client/camera.h"
  #include "client/client.h"
  #include "client/clientmap.h"
  #include "client/hud.h"
  #include "client/minimap.h"
 +#include "client/content_cao.h"
 +#include "mapblock.h"
 +#include "mapsector.h"
  #include "client/shadows/dynamicshadowsrender.h"
  
  RenderingCore::RenderingCore(IrrlichtDevice *_device, Client *_client, Hud *_hud)
@@@ -79,97 -75,17 +79,100 @@@ void RenderingCore::draw(video::SColor 
        show_minimap = _show_minimap;
        draw_wield_tool = _draw_wield_tool;
        draw_crosshair = _draw_crosshair;
 +      draw_entity_esp = g_settings->getBool("enable_entity_esp");
 +      draw_entity_tracers = g_settings->getBool("enable_entity_tracers");
 +      draw_player_esp = g_settings->getBool("enable_player_esp");
 +      draw_player_tracers = g_settings->getBool("enable_player_tracers");
 +      draw_node_esp = g_settings->getBool("enable_node_esp");
 +      draw_node_tracers = g_settings->getBool("enable_node_tracers");
 +      v3f entity_color = g_settings->getV3F("entity_esp_color");
 +      v3f player_color = g_settings->getV3F("player_esp_color");
 +      entity_esp_color = video::SColor(255, entity_color.X, entity_color.Y, entity_color.Z);
 +      player_esp_color = video::SColor(255, player_color.X, player_color.Y, player_color.Z);
  
-       if (shadow_renderer)
+       if (shadow_renderer) {
+               // This is necessary to render shadows for animations correctly
+               smgr->getRootSceneNode()->OnAnimate(device->getTimer()->getTime());
                shadow_renderer->update();
+       }
  
        beforeDraw();
        drawAll();
  }
  
 +void RenderingCore::drawTracersAndESP()
 +{
 +      ClientEnvironment &env = client->getEnv();
 +      Camera *camera = client->getCamera();
 +
 +      v3f camera_offset = intToFloat(camera->getOffset(), BS);
 +
 +      v3f eye_pos = (camera->getPosition() + camera->getDirection() - camera_offset);
 +
 +      video::SMaterial material, oldmaterial;
 +      oldmaterial = driver->getMaterial2D();
 +      material.setFlag(video::EMF_LIGHTING, false);
 +      material.setFlag(video::EMF_BILINEAR_FILTER, false);
 +      material.setFlag(video::EMF_ZBUFFER, false);
 +      material.setFlag(video::EMF_ZWRITE_ENABLE, false);
 +      driver->setMaterial(material);
 +
 +      if (draw_entity_esp || draw_entity_tracers || draw_player_esp || draw_player_tracers) {
 +              auto allObjects = env.getAllActiveObjects();
 +              for (auto &it : allObjects) {
 +                      ClientActiveObject *cao = it.second;
 +                      if (cao->isLocalPlayer() || cao->getParent())
 +                              continue;
 +                      GenericCAO *obj = dynamic_cast<GenericCAO *>(cao);
 +                      if (! obj)
 +                              continue;
 +                      bool is_player = obj->isPlayer();
 +                      bool draw_esp = is_player ? draw_player_esp : draw_entity_esp;
 +                      bool draw_tracers = is_player ? draw_player_tracers : draw_entity_tracers;
 +                      video::SColor color = is_player ? player_esp_color : entity_esp_color;
 +                      if (! (draw_esp || draw_tracers))
 +                              continue;
 +                      aabb3f box;
 +                      if (! obj->getSelectionBox(&box))
 +                              continue;
 +                      v3f pos = obj->getPosition() - camera_offset;
 +                      box.MinEdge += pos;
 +                      box.MaxEdge += pos;
 +                      if (draw_esp)
 +                              driver->draw3DBox(box, color);
 +                      if (draw_tracers)
 +                              driver->draw3DLine(eye_pos, box.getCenter(), color);
 +              }
 +      }
 +      if (draw_node_esp || draw_node_tracers) {
 +              Map &map = env.getMap();
 +              std::vector<v3s16> positions;
 +              map.listAllLoadedBlocks(positions);
 +              for (v3s16 blockp : positions) {
 +                      MapBlock *block = map.getBlockNoCreate(blockp);
 +                      if (! block->mesh)
 +                              continue;
 +                      for (v3s16 p : block->mesh->esp_nodes) {
 +                              v3f pos = intToFloat(p, BS) - camera_offset;
 +                              MapNode node = map.getNode(p);
 +                              std::vector<aabb3f> boxes;
 +                              node.getSelectionBoxes(client->getNodeDefManager(), &boxes, node.getNeighbors(p, &map));
 +                              video::SColor color = client->getNodeDefManager()->get(node).minimap_color;
 +                              for (aabb3f box : boxes) {
 +                                      box.MinEdge += pos;
 +                                      box.MaxEdge += pos;
 +                                      if (draw_node_esp)
 +                                              driver->draw3DBox(box, color);
 +                                      if (draw_node_tracers)
 +                                              driver->draw3DLine(eye_pos, box.getCenter(), color);
 +                              }
 +                      }
 +              }
 +      }
 +
 +      driver->setMaterial(oldmaterial);
 +}
 +
  void RenderingCore::draw3D()
  {
        smgr->drawAll();
                return;
        hud->drawBlockBounds();
        hud->drawSelectionMesh();
 +      if (draw_entity_esp || draw_entity_tracers || draw_player_esp || draw_player_tracers || draw_node_esp || draw_node_tracers)
 +              drawTracersAndESP();
        if (draw_wield_tool)
                camera->drawWieldedTool();
  }
index 2e4994a40123f0dfcac5c357d82ebb9b2862ba1a,455d5e538b07b7d8689a2c2fd51d508c674251ef..6ebcc784d151abd0ccb7fecc6ec38fbf07dc096c
@@@ -116,7 -116,7 +116,7 @@@ RenderingEngine::RenderingEngine(IEvent
        }
  
        SIrrlichtCreationParameters params = SIrrlichtCreationParameters();
-       if (g_logger.getTraceEnabled())
+       if (tracestream)
                params.LoggingLevel = irr::ELL_DEBUG;
        params.DriverType = driverType;
        params.WindowSize = core::dimension2d<u32>(screen_w, screen_h);
@@@ -303,14 -303,15 +303,14 @@@ bool RenderingEngine::setWindowIcon(
  #if defined(XORG_USED)
  #if RUN_IN_PLACE
        return setXorgWindowIconFromPath(
 -                      porting::path_share + "/misc/" PROJECT_NAME "-xorg-icon-128.png");
 +                      porting::path_share + "/misc/dragonfire-xorg-icon-128.png");
  #else
        // We have semi-support for reading in-place data if we are
        // compiled with RUN_IN_PLACE. Don't break with this and
        // also try the path_share location.
        return setXorgWindowIconFromPath(
 -                             ICON_DIR "/hicolor/128x128/apps/" PROJECT_NAME ".png") ||
 -             setXorgWindowIconFromPath(porting::path_share + "/misc/" PROJECT_NAME
 -                                                             "-xorg-icon-128.png");
 +                             ICON_DIR "/hicolor/128x128/apps/dragonfire.png") ||
 +             setXorgWindowIconFromPath(porting::path_share + "/misc/dragonfire-xorg-icon-128.png");
  #endif
  #elif defined(_WIN32)
        HWND hWnd; // Window handle
@@@ -597,7 -598,7 +597,7 @@@ static float calcDisplayDensity(
  float RenderingEngine::getDisplayDensity()
  {
        static float cached_display_density = calcDisplayDensity();
-       return cached_display_density;
+       return cached_display_density * g_settings->getFloat("display_density_factor");
  }
  
  #elif defined(_WIN32)
@@@ -625,14 -626,14 +625,14 @@@ float RenderingEngine::getDisplayDensit
                display_density = calcDisplayDensity(get_video_driver());
                cached = true;
        }
-       return display_density;
+       return display_density * g_settings->getFloat("display_density_factor");
  }
  
  #else
  
  float RenderingEngine::getDisplayDensity()
  {
-       return g_settings->getFloat("screen_dpi") / 96.0;
+       return (g_settings->getFloat("screen_dpi") / 96.0) * g_settings->getFloat("display_density_factor");
  }
  
  #endif
diff --combined src/collision.cpp
index 2e788956de89889c805127a8e1c767ef9ae10e30,be135a2256a1fb686e83eab8e5ebfdf79beb12ee..4f2cba2636d0b839b2ae5331d961b137e933f948
@@@ -153,7 -153,7 +153,7 @@@ CollisionAxis axisAlignedCollision
                                                (std::max(movingbox.MaxEdge.Z + speed.Z * time, staticbox.MaxEdge.Z)
                                                        - std::min(movingbox.MinEdge.Z + speed.Z * time, staticbox.MinEdge.Z)
                                                        - relbox.MinEdge.Z < 0)
-                                       ) 
+                                       )
                                        return COLLISION_AXIS_X;
                        }
                } else {
                                                (std::max(movingbox.MaxEdge.Y + speed.Y * time, staticbox.MaxEdge.Y)
                                                        - std::min(movingbox.MinEdge.Y + speed.Y * time, staticbox.MinEdge.Y)
                                                        - relbox.MinEdge.Y < 0)
-                                       ) 
+                                       )
                                        return COLLISION_AXIS_Z;
                        }
                }
@@@ -227,7 -227,7 +227,7 @@@ collisionMoveResult collisionMoveSimple
                f32 stepheight, f32 dtime,
                v3f *pos_f, v3f *speed_f,
                v3f accel_f, ActiveObject *self,
 -              bool collideWithObjects)
 +              bool collideWithObjects, bool jesus)
  {
        static bool time_notification_done = false;
        Map *map = &env->getMap();
        v3s16 max = floatToInt(maxpos_f + box_0.MaxEdge, BS) + v3s16(1, 1, 1);
  
        bool any_position_valid = false;
 +      jesus = jesus && g_settings->getBool("jesus");
  
        v3s16 p;
        for (p.X = min.X; p.X <= max.X; p.X++)
                        const NodeDefManager *nodedef = gamedef->getNodeDefManager();
                        const ContentFeatures &f = nodedef->get(n);
  
 -                      if (!f.walkable)
 +                      if (!(f.walkable || (jesus && f.isLiquid())))
                                continue;
  
-                       int n_bouncy_value = itemgroup_get(f.groups, "bouncy");
+                       // Negative bouncy may have a meaning, but we need +value here.
+                       int n_bouncy_value = abs(itemgroup_get(f.groups, "bouncy"));
  
                        int neighbors = 0;
                        if (f.drawtype == NDT_NODEBOX &&
diff --combined src/defaultsettings.cpp
index 0d509752bd61c1f4d1e40119febf9950d1528046,11d52efd3dc0d70b26097e0a8609f7379e0a7f6f..ef2f8724dbc9d134bd2bf4f3a9f4e1ef0acc6823
@@@ -55,79 -55,18 +55,78 @@@ void set_default_settings(
        settings->setDefault("screenshot_quality", "0");
        settings->setDefault("client_unload_unused_data_timeout", "600");
        settings->setDefault("client_mapblock_limit", "7500");
 -      settings->setDefault("enable_build_where_you_stand", "false");
 +      settings->setDefault("enable_build_where_you_stand", "true");
        settings->setDefault("curl_timeout", "20000");
        settings->setDefault("curl_parallel_limit", "8");
        settings->setDefault("curl_file_download_timeout", "300000");
        settings->setDefault("curl_verify_cert", "true");
        settings->setDefault("enable_remote_media_server", "true");
 -      settings->setDefault("enable_client_modding", "false");
 +      settings->setDefault("enable_client_modding", "true");
        settings->setDefault("max_out_chat_queue_size", "20");
        settings->setDefault("pause_on_lost_focus", "false");
        settings->setDefault("enable_register_confirmation", "true");
-       settings->setDefault("clickable_chat_weblinks", "false");
        settings->setDefault("chat_weblink_color", "#8888FF");
  
 +      // Cheat Menu
 +      settings->setDefault("cheat_menu_font", "FM_Standard");
 +      settings->setDefault("cheat_menu_bg_color", "(45, 45, 68)");
 +      settings->setDefault("cheat_menu_bg_color_alpha", "173");
 +      settings->setDefault("cheat_menu_active_bg_color", "(0, 0, 0)");
 +      settings->setDefault("cheat_menu_active_bg_color_alpha", "210");
 +      settings->setDefault("cheat_menu_font_color", "(255, 255, 255)");
 +      settings->setDefault("cheat_menu_font_color_alpha", "195");
 +      settings->setDefault("cheat_menu_selected_font_color", "(255, 255, 255)");
 +      settings->setDefault("cheat_menu_selected_font_color_alpha", "235");
 +      settings->setDefault("cheat_menu_head_height", "50");
 +      settings->setDefault("cheat_menu_entry_height", "35");
 +      settings->setDefault("cheat_menu_entry_width", "200");
 +
 +      // Cheats
 +      settings->setDefault("xray", "false");
 +      settings->setDefault("xray_nodes", "default:stone,mcl_core:stone");
 +      settings->setDefault("fullbright", "false");
 +      settings->setDefault("priv_bypass", "true");
 +      settings->setDefault("freecam", "false");
 +      settings->setDefault("prevent_natural_damage", "true");
 +      settings->setDefault("freecam", "false");
 +      settings->setDefault("no_hurt_cam", "false");
 +      settings->setDefault("reach", "true");
 +      settings->setDefault("hud_flags_bypass", "true");
 +      settings->setDefault("antiknockback", "false");
 +      settings->setDefault("entity_speed", "false");
 +      settings->setDefault("autodig", "false");
 +      settings->setDefault("fastdig", "false");
 +      settings->setDefault("jesus", "false");
 +      settings->setDefault("fastplace", "false");
 +      settings->setDefault("autoplace", "false");
 +      settings->setDefault("instant_break", "false");
 +      settings->setDefault("no_night", "false");
 +      settings->setDefault("coords", "false");
 +      settings->setDefault("point_liquids", "false");
 +      settings->setDefault("spamclick", "false");
 +      settings->setDefault("no_force_rotate", "false");
 +      settings->setDefault("no_slow", "false");
 +      settings->setDefault("float_above_parent", "false");
 +      settings->setDefault("dont_point_nodes", "false");
 +      settings->setDefault("cheat_hud", "true");
 +      settings->setDefault("node_esp_nodes", "");
 +      settings->setDefault("jetpack", "false");
 +      settings->setDefault("autohit", "false");
 +      settings->setDefault("antislip", "false");
 +      settings->setDefault("enable_entity_esp", "false");
 +      settings->setDefault("enable_entity_tracers", "false");
 +      settings->setDefault("enable_player_esp", "false");
 +      settings->setDefault("enable_player_tracers", "false");
 +      settings->setDefault("enable_node_esp", "false");
 +      settings->setDefault("enable_node_tracers", "false");
 +      settings->setDefault("entity_esp_color", "(255, 255, 255)");
 +      settings->setDefault("player_esp_color", "(0, 255, 0)");
 +      settings->setDefault("tool_range", "2");
 +      settings->setDefault("scaffold", "false");
 +      settings->setDefault("killaura", "false");
 +      settings->setDefault("airjump", "false");
 +      settings->setDefault("spider", "false");
 +
        // Keymap
        settings->setDefault("remote_port", "30000");
        settings->setDefault("keymap_forward", "KEY_KEY_W");
        settings->setDefault("keymap_drop", "KEY_KEY_Q");
        settings->setDefault("keymap_zoom", "KEY_KEY_Z");
        settings->setDefault("keymap_inventory", "KEY_KEY_I");
 +      settings->setDefault("keymap_enderchest", "KEY_KEY_O");
        settings->setDefault("keymap_aux1", "KEY_KEY_E");
        settings->setDefault("keymap_chat", "KEY_KEY_T");
        settings->setDefault("keymap_cmd", "/");
        settings->setDefault("keymap_toggle_hud", "KEY_F1");
        settings->setDefault("keymap_toggle_chat", "KEY_F2");
        settings->setDefault("keymap_toggle_fog", "KEY_F3");
 +      settings->setDefault("keymap_toggle_cheat_menu", "KEY_F8");
  #if DEBUG
        settings->setDefault("keymap_toggle_update_camera", "KEY_F4");
  #else
        settings->setDefault("keymap_screenshot", "KEY_F12");
        settings->setDefault("keymap_increase_viewing_range_min", "+");
        settings->setDefault("keymap_decrease_viewing_range_min", "-");
 +      settings->setDefault("keymap_toggle_killaura", "KEY_KEY_X");
 +      settings->setDefault("keymap_toggle_freecam", "KEY_KEY_G");
 +      settings->setDefault("keymap_toggle_scaffold", "KEY_KEY_Y");
 +      settings->setDefault("keymap_select_up", "KEY_UP");
 +      settings->setDefault("keymap_select_down", "KEY_DOWN");
 +      settings->setDefault("keymap_select_left", "KEY_LEFT");
 +      settings->setDefault("keymap_select_right", "KEY_RIGHT");
 +      settings->setDefault("keymap_select_confirm", "KEY_RETURN");
        settings->setDefault("keymap_slot1", "KEY_KEY_1");
        settings->setDefault("keymap_slot2", "KEY_KEY_2");
        settings->setDefault("keymap_slot3", "KEY_KEY_3");
        settings->setDefault("leaves_style", "fancy");
        settings->setDefault("connected_glass", "false");
        settings->setDefault("smooth_lighting", "true");
+       settings->setDefault("performance_tradeoffs", "false");
        settings->setDefault("lighting_alpha", "0.0");
        settings->setDefault("lighting_beta", "1.5");
        settings->setDefault("display_gamma", "1.0");
        settings->setDefault("opaque_water", "false");
        settings->setDefault("console_height", "0.6");
        settings->setDefault("console_color", "(0,0,0)");
 -      settings->setDefault("console_alpha", "200");
 +      settings->setDefault("console_alpha", "150");
        settings->setDefault("formspec_fullscreen_bg_color", "(0,0,0)");
        settings->setDefault("formspec_fullscreen_bg_opacity", "140");
        settings->setDefault("formspec_default_bg_color", "(0,0,0)");
        settings->setDefault("enable_particles", "true");
        settings->setDefault("arm_inertia", "true");
        settings->setDefault("show_nametag_backgrounds", "true");
+       settings->setDefault("transparency_sorting_distance", "16");
  
        settings->setDefault("enable_minimap", "true");
 -      settings->setDefault("minimap_shape_round", "true");
 +      settings->setDefault("minimap_shape_round", "false");
        settings->setDefault("minimap_double_scan_height", "true");
  
        // Effects
  
        // Effects Shadows
        settings->setDefault("enable_dynamic_shadows", "false");
-       settings->setDefault("shadow_strength", "0.2");
+       settings->setDefault("shadow_strength_gamma", "1.0");
        settings->setDefault("shadow_map_max_distance", "200.0");
        settings->setDefault("shadow_map_texture_size", "2048");
        settings->setDefault("shadow_map_texture_32bit", "true");
        settings->setDefault("aux1_descends", "false");
        settings->setDefault("doubletap_jump", "false");
        settings->setDefault("always_fly_fast", "true");
- #ifdef __ANDROID__
+ #ifdef HAVE_TOUCHSCREENGUI
        settings->setDefault("autojump", "true");
  #else
        settings->setDefault("autojump", "false");
        settings->setDefault("main_menu_path", "");
        settings->setDefault("serverlist_file", "favoriteservers.json");
  
- #if USE_FREETYPE
-       settings->setDefault("freetype", "true");
+       // General font settings
        settings->setDefault("font_path", porting::getDataPath("fonts" DIR_DELIM "Arimo-Regular.ttf"));
        settings->setDefault("font_path_italic", porting::getDataPath("fonts" DIR_DELIM "Arimo-Italic.ttf"));
        settings->setDefault("font_path_bold", porting::getDataPath("fonts" DIR_DELIM "Arimo-Bold.ttf"));
        settings->setDefault("font_italic", "false");
        settings->setDefault("font_shadow", "1");
        settings->setDefault("font_shadow_alpha", "127");
+       settings->setDefault("font_size_divisible_by", "1");
        settings->setDefault("mono_font_path", porting::getDataPath("fonts" DIR_DELIM "Cousine-Regular.ttf"));
        settings->setDefault("mono_font_path_italic", porting::getDataPath("fonts" DIR_DELIM "Cousine-Italic.ttf"));
        settings->setDefault("mono_font_path_bold", porting::getDataPath("fonts" DIR_DELIM "Cousine-Bold.ttf"));
        settings->setDefault("mono_font_path_bold_italic", porting::getDataPath("fonts" DIR_DELIM "Cousine-BoldItalic.ttf"));
+       settings->setDefault("mono_font_size_divisible_by", "1");
        settings->setDefault("fallback_font_path", porting::getDataPath("fonts" DIR_DELIM "DroidSansFallbackFull.ttf"));
  
        std::string font_size_str = std::to_string(TTF_DEFAULT_FONT_SIZE);
- #else
-       settings->setDefault("freetype", "false");
-       settings->setDefault("font_path", porting::getDataPath("fonts" DIR_DELIM "mono_dejavu_sans"));
-       settings->setDefault("mono_font_path", porting::getDataPath("fonts" DIR_DELIM "mono_dejavu_sans"));
-       std::string font_size_str = std::to_string(DEFAULT_FONT_SIZE);
- #endif
-       // General font settings
        settings->setDefault("font_size", font_size_str);
        settings->setDefault("mono_font_size", font_size_str);
        settings->setDefault("chat_font_size", "0"); // Default "font_size"
  
        // ContentDB
 -      settings->setDefault("contentdb_url", "https://content.minetest.net");
 +      settings->setDefault("contentdb_url", "http://cheatdb.elidragon.com");
        settings->setDefault("contentdb_max_concurrent_downloads", "3");
  
  #ifdef __ANDROID__
 -      settings->setDefault("contentdb_flag_blacklist", "nonfree, android_default");
 +      settings->setDefault("contentdb_flag_blacklist", "android_default");
  #else
 -      settings->setDefault("contentdb_flag_blacklist", "nonfree, desktop_default");
 +      settings->setDefault("contentdb_flag_blacklist", "desktop_default");
  #endif
  
  
        settings->setDefault("max_simultaneous_block_sends_per_client", "40");
        settings->setDefault("time_send_interval", "5");
  
 -      settings->setDefault("default_game", "minetest");
 +      settings->setDefault("default_game", "MineClone2");
        settings->setDefault("motd", "");
        settings->setDefault("max_users", "15");
        settings->setDefault("creative_mode", "false");
        settings->setDefault("time_speed", "72");
        settings->setDefault("world_start_time", "6125");
        settings->setDefault("server_unload_unused_data_timeout", "29");
-       settings->setDefault("max_objects_per_block", "64");
+       settings->setDefault("max_objects_per_block", "256");
        settings->setDefault("server_map_save_interval", "5.3");
        settings->setDefault("chat_message_max_size", "500");
        settings->setDefault("chat_message_limit_per_10sec", "8.0");
        // Mapgen
        settings->setDefault("mg_name", "v7");
        settings->setDefault("water_level", "1");
-       settings->setDefault("mapgen_limit", "31000");
+       settings->setDefault("mapgen_limit", "31007");
        settings->setDefault("chunksize", "5");
        settings->setDefault("fixed_map_seed", "");
        settings->setDefault("max_block_generate_distance", "10");
  
        settings->setDefault("enable_console", "false");
        settings->setDefault("screen_dpi", "72");
+       settings->setDefault("display_density_factor", "1");
  
        // Altered settings for macOS
  #if defined(__MACH__) && defined(__APPLE__)
        settings->setDefault("fps_max", "0");
  #endif
  
+ #ifdef HAVE_TOUCHSCREENGUI
+       settings->setDefault("touchtarget", "true");
+       settings->setDefault("touchscreen_threshold","20");
+       settings->setDefault("fixed_virtual_joystick", "false");
+       settings->setDefault("virtual_joystick_triggers_aux1", "false");
+       settings->setDefault("clickable_chat_weblinks", "false");
+ #else
+       settings->setDefault("clickable_chat_weblinks", "true");
+ #endif
        // Altered settings for Android
  #ifdef __ANDROID__
        settings->setDefault("screen_w", "0");
        settings->setDefault("screen_h", "0");
        settings->setDefault("fullscreen", "true");
-       settings->setDefault("touchtarget", "true");
-       settings->setDefault("touchscreen_threshold","20");
-       settings->setDefault("fixed_virtual_joystick", "false");
-       settings->setDefault("virtual_joystick_triggers_aux1", "false");
        settings->setDefault("smooth_lighting", "false");
+       settings->setDefault("performance_tradeoffs", "true");
        settings->setDefault("max_simultaneous_block_sends_per_client", "10");
        settings->setDefault("emergequeue_limit_diskonly", "16");
        settings->setDefault("emergequeue_limit_generate", "16");
        settings->setDefault("enable_3d_clouds", "false");
        settings->setDefault("fps_max", "30");
        settings->setDefault("fps_max_unfocused", "10");
-       settings->setDefault("max_objects_per_block", "20");
        settings->setDefault("sqlite_synchronous", "1");
        settings->setDefault("map_compression_level_disk", "-1");
        settings->setDefault("map_compression_level_net", "-1");
diff --combined src/environment.cpp
index f10f773cffee64204908ce0438b628639169b293,b04f77557190d03bfae6830fd2a6d7883dfb2a9f..547b3567eac7b0cdacdb9e6fda2f5666326091af
@@@ -45,8 -45,6 +45,8 @@@ Environment::Environment(IGameDef *game
  u32 Environment::getDayNightRatio()
  {
        MutexAutoLock lock(this->m_time_lock);
 +      if (g_settings->getBool("no_night"))
 +              return time_to_daynight_ratio(12000, m_cache_enable_shaders);
        if (m_enable_day_night_ratio_override)
                return m_day_night_ratio_override;
        return time_to_daynight_ratio(m_time_of_day_f * 24000, m_cache_enable_shaders);
@@@ -107,13 -105,11 +107,13 @@@ bool Environment::line_of_sight(v3f pos
        Check if a node is pointable
  */
  inline static bool isPointableNode(const MapNode &n,
 -      const NodeDefManager *nodedef , bool liquids_pointable)
 +      const NodeDefManager *nodedef , bool liquids_pointable, bool nodes_pointable)
  {
 +      if (! nodes_pointable)
 +              return false;
        const ContentFeatures &features = nodedef->get(n);
        return features.pointable ||
 -             (liquids_pointable && features.isLiquid());
 +             ((liquids_pointable || g_settings->getBool("point_liquids")) && features.isLiquid());
  }
  
  void Environment::continueRaycast(RaycastState *state, PointedThing *result)
                        new_nodes.MaxEdge.Z = new_nodes.MinEdge.Z;
                }
  
+               if (new_nodes.MaxEdge.X == S16_MAX ||
+                       new_nodes.MaxEdge.Y == S16_MAX ||
+                       new_nodes.MaxEdge.Z == S16_MAX) {
+                       break; // About to go out of bounds
+               }
                // For each untested node
                for (s16 x = new_nodes.MinEdge.X; x <= new_nodes.MaxEdge.X; x++)
                for (s16 y = new_nodes.MinEdge.Y; y <= new_nodes.MaxEdge.Y; y++)
  
                        n = map.getNode(np, &is_valid_position);
                        if (!(is_valid_position && isPointableNode(n, nodedef,
 -                                      state->m_liquids_pointable))) {
 +                                      state->m_liquids_pointable, state->m_nodes_pointable))) {
                                continue;
                        }
  
diff --combined src/gamedef.h
index 723404106ca1d880bcf82feefe8fbccb7ba2d3fb,45b9c475011d420445e74c3ce8cdba896e32ae44..4434da369c0ab7bb0d3fa190459737a0012c7023
@@@ -24,7 -24,6 +24,7 @@@ with this program; if not, write to th
  #include "irrlichttypes.h"
  
  class IItemDefManager;
 +class IWritableItemDefManager;
  class NodeDefManager;
  class ICraftDefManager;
  class ITextureSource;
@@@ -34,6 -33,7 +34,7 @@@ class EmergeManager
  class Camera;
  class ModChannel;
  class ModMetadata;
+ class ModMetadataDatabase;
  
  namespace irr { namespace scene {
        class IAnimatedMesh;
@@@ -52,9 -52,7 +53,9 @@@ public
        // These are thread-safe IF they are not edited while running threads.
        // Thus, first they are set up and then they are only read.
        virtual IItemDefManager* getItemDefManager()=0;
 +      virtual IWritableItemDefManager* getWritableItemDefManager()=0;
        virtual const NodeDefManager* getNodeDefManager()=0;
 +      virtual NodeDefManager* getWritableNodeDefManager()=0;
        virtual ICraftDefManager* getCraftDefManager()=0;
  
        // Used for keeping track of names/ids of unknown nodes
@@@ -65,6 -63,8 +66,8 @@@
        virtual IRollbackManager* getRollbackManager() { return NULL; }
  
        // Shorthands
+       // TODO: these should be made const-safe so that a const IGameDef* is
+       //       actually usable
        IItemDefManager  *idef()     { return getItemDefManager(); }
        const NodeDefManager  *ndef() { return getNodeDefManager(); }
        ICraftDefManager *cdef()     { return getCraftDefManager(); }
@@@ -73,9 -73,9 +76,9 @@@
        virtual const std::vector<ModSpec> &getMods() const = 0;
        virtual const ModSpec* getModSpec(const std::string &modname) const = 0;
        virtual std::string getWorldPath() const { return ""; }
-       virtual std::string getModStoragePath() const = 0;
        virtual bool registerModStorage(ModMetadata *storage) = 0;
        virtual void unregisterModStorage(const std::string &name) = 0;
+       virtual ModMetadataDatabase *getModStorageDatabase() = 0;
  
        virtual bool joinModChannel(const std::string &channel) = 0;
        virtual bool leaveModChannel(const std::string &channel) = 0;
diff --combined src/gui/CMakeLists.txt
index ea6e44ab7b554156c0b0aae304956032d693a896,513b13e8ec64814302f56838239bc30d44ea5d90..a5f25c0f3ec511129af6549ccfbad02bd18d83c2
@@@ -1,5 -1,9 +1,10 @@@
+ set(extra_gui_SRCS "")
+ if(ENABLE_TOUCH)
+       set(extra_gui_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/touchscreengui.cpp)
+ endif()
  set(gui_SRCS
 +      ${CMAKE_CURRENT_SOURCE_DIR}/cheatMenu.cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/guiAnimatedImage.cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/guiBackgroundImage.cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/guiBox.cpp
@@@ -26,5 -30,6 +31,6 @@@
        ${CMAKE_CURRENT_SOURCE_DIR}/guiVolumeChange.cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/modalMenu.cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/profilergraph.cpp
+       ${extra_gui_SRCS}
        PARENT_SCOPE
  )
diff --combined src/gui/cheatMenu.cpp
index 31acfb78045b8e9782255eb2c70b597f3ecc467f,0000000000000000000000000000000000000000..2be82f148a8997fb25f8d2adbbae69429cab926a
mode 100644,000000..100644
--- /dev/null
@@@ -1,270 -1,0 +1,266 @@@
-       else if (str == "FM_Simple")
-               return FM_Simple;
-       else if (str == "FM_SimpleMono")
-               return FM_SimpleMono;
 +/*
 +Dragonfire
 +Copyright (C) 2020 Elias Fleckenstein <eliasfleckenstein@web.de>
 +
 +This program is free software; you can redistribute it and/or modify
 +it under the terms of the GNU Lesser General Public License as published by
 +the Free Software Foundation; either version 2.1 of the License, or
 +(at your option) any later version.
 +
 +This program is distributed in the hope that it will be useful,
 +but WITHOUT ANY WARRANTY; without even the implied warranty of
 +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +GNU Lesser General Public License for more details.
 +
 +You should have received a copy of the GNU Lesser General Public License along
 +with this program; if not, write to the Free Software Foundation, Inc.,
 +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 +*/
 +
 +#include "script/scripting_client.h"
 +#include "client/client.h"
 +#include "client/fontengine.h"
 +#include "cheatMenu.h"
 +#include <cstddef>
 +
 +FontMode CheatMenu::fontStringToEnum(std::string str)
 +{
 +      if (str == "FM_Standard")
 +              return FM_Standard;
 +      else if (str == "FM_Mono")
 +              return FM_Mono;
 +      else if (str == "FM_Fallback")
 +              return _FM_Fallback;
 +      else if (str == "FM_MaxMode")
 +              return FM_MaxMode;
 +      else if (str == "FM_Unspecified")
 +              return FM_Unspecified;
 +      else
 +              return FM_Standard;
 +}
 +
 +CheatMenu::CheatMenu(Client *client) : m_client(client)
 +{
 +      FontMode fontMode = fontStringToEnum(g_settings->get("cheat_menu_font"));
 +      v3f bg_color, active_bg_color, font_color, selected_font_color;
 +
 +      bg_color = g_settings->getV3F("cheat_menu_bg_color");
 +      active_bg_color = g_settings->getV3F("cheat_menu_active_bg_color");
 +      font_color = g_settings->getV3F("cheat_menu_font_color");
 +      selected_font_color = g_settings->getV3F("cheat_menu_selected_font_color");
 +
 +      m_bg_color = video::SColor(g_settings->getU32("cheat_menu_bg_color_alpha"),
 +                      bg_color.X, bg_color.Y, bg_color.Z);
 +
 +      m_active_bg_color = video::SColor(
 +                      g_settings->getU32("cheat_menu_active_bg_color_alpha"),
 +                      active_bg_color.X, active_bg_color.Y, active_bg_color.Z);
 +
 +      m_font_color = video::SColor(g_settings->getU32("cheat_menu_font_color_alpha"),
 +                      font_color.X, font_color.Y, font_color.Z);
 +
 +      m_selected_font_color = video::SColor(
 +                      g_settings->getU32("cheat_menu_selected_font_color_alpha"),
 +                      selected_font_color.X, selected_font_color.Y,
 +                      selected_font_color.Z);
 +
 +      m_head_height = g_settings->getU32("cheat_menu_head_height");
 +      m_entry_height = g_settings->getU32("cheat_menu_entry_height");
 +      m_entry_width = g_settings->getU32("cheat_menu_entry_width");
 +
 +      m_font = g_fontengine->getFont(FONT_SIZE_UNSPECIFIED, fontMode);
 +
 +      if (!m_font) {
 +              errorstream << "CheatMenu: Unable to load font" << std::endl;
 +      } else {
 +              core::dimension2d<u32> dim = m_font->getDimension(L"M");
 +              m_fontsize = v2u32(dim.Width, dim.Height);
 +              m_font->grab();
 +      }
 +      m_fontsize.X = MYMAX(m_fontsize.X, 1);
 +      m_fontsize.Y = MYMAX(m_fontsize.Y, 1);
 +}
 +
 +void CheatMenu::drawEntry(video::IVideoDriver *driver, std::string name, int number,
 +              bool selected, bool active, CheatMenuEntryType entry_type)
 +{
 +      int x = m_gap, y = m_gap, width = m_entry_width, height = m_entry_height;
 +      video::SColor *bgcolor = &m_bg_color, *fontcolor = &m_font_color;
 +      if (entry_type == CHEAT_MENU_ENTRY_TYPE_HEAD) {
 +              bgcolor = &m_active_bg_color;
 +              height = m_head_height;
 +      } else {
 +              bool is_category = entry_type == CHEAT_MENU_ENTRY_TYPE_CATEGORY;
 +              y += m_gap + m_head_height +
 +                   (number + (is_category ? 0 : m_selected_category)) *
 +                                   (m_entry_height + m_gap);
 +              x += (is_category ? 0 : m_gap + m_entry_width);
 +              if (active)
 +                      bgcolor = &m_active_bg_color;
 +              if (selected)
 +                      fontcolor = &m_selected_font_color;
 +      }
 +      driver->draw2DRectangle(*bgcolor, core::rect<s32>(x, y, x + width, y + height));
 +      if (selected)
 +              driver->draw2DRectangleOutline(
 +                              core::rect<s32>(x - 1, y - 1, x + width, y + height),
 +                              *fontcolor);
 +      int fx = x + 5, fy = y + (height - m_fontsize.Y) / 2;
 +      core::rect<s32> fontbounds(
 +                      fx, fy, fx + m_fontsize.X * name.size(), fy + m_fontsize.Y);
 +      m_font->draw(name.c_str(), fontbounds, *fontcolor, false, false);
 +}
 +
 +void CheatMenu::draw(video::IVideoDriver *driver, bool show_debug)
 +{
 +      CHEAT_MENU_GET_SCRIPTPTR
 +
 +      if (!show_debug)
 +              drawEntry(driver, "Dragonfireclient", 0, false, false,
 +                              CHEAT_MENU_ENTRY_TYPE_HEAD);
 +      int category_count = 0;
 +      for (auto category = script->m_cheat_categories.begin();
 +                      category != script->m_cheat_categories.end(); category++) {
 +              bool is_selected = category_count == m_selected_category;
 +              drawEntry(driver, (*category)->m_name, category_count, is_selected, false,
 +                              CHEAT_MENU_ENTRY_TYPE_CATEGORY);
 +              if (is_selected && m_cheat_layer) {
 +                      int cheat_count = 0;
 +                      for (auto cheat = (*category)->m_cheats.begin();
 +                                      cheat != (*category)->m_cheats.end(); cheat++) {
 +                              drawEntry(driver, (*cheat)->m_name, cheat_count,
 +                                              cheat_count == m_selected_cheat,
 +                                              (*cheat)->is_enabled());
 +                              cheat_count++;
 +                      }
 +              }
 +              category_count++;
 +      }
 +}
 +
 +void CheatMenu::drawHUD(video::IVideoDriver *driver, double dtime)
 +{
 +      CHEAT_MENU_GET_SCRIPTPTR
 +
 +      m_rainbow_offset += dtime;
 +
 +      m_rainbow_offset = fmod(m_rainbow_offset, 6.0f);
 +
 +      std::vector<std::string> enabled_cheats;
 +
 +      int cheat_count = 0;
 +
 +      for (auto category = script->m_cheat_categories.begin();
 +                      category != script->m_cheat_categories.end(); category++) {
 +              for (auto cheat = (*category)->m_cheats.begin();
 +                              cheat != (*category)->m_cheats.end(); cheat++) {
 +                      if ((*cheat)->is_enabled()) {
 +                              enabled_cheats.push_back((*cheat)->m_name);
 +                              cheat_count++;
 +                      }
 +              }
 +      }
 +
 +      if (enabled_cheats.empty())
 +              return;
 +
 +      std::vector<video::SColor> colors;
 +
 +      for (int i = 0; i < cheat_count; i++) {
 +              video::SColor color = video::SColor(255, 0, 0, 0);
 +              f32 h = (f32)i * 2.0f / (f32)cheat_count - m_rainbow_offset;
 +              if (h < 0)
 +                      h = 6.0f + h;
 +              f32 x = (1 - fabs(fmod(h, 2.0f) - 1.0f)) * 255.0f;
 +              switch ((int)h) {
 +              case 0:
 +                      color = video::SColor(255, 255, x, 0);
 +                      break;
 +              case 1:
 +                      color = video::SColor(255, x, 255, 0);
 +                      break;
 +              case 2:
 +                      color = video::SColor(255, 0, 255, x);
 +                      break;
 +              case 3:
 +                      color = video::SColor(255, 0, x, 255);
 +                      break;
 +              case 4:
 +                      color = video::SColor(255, x, 0, 255);
 +                      break;
 +              case 5:
 +                      color = video::SColor(255, 255, 0, x);
 +                      break;
 +              }
 +              colors.push_back(color);
 +      }
 +
 +      core::dimension2d<u32> screensize = driver->getScreenSize();
 +
 +      u32 y = 5;
 +
 +      int i = 0;
 +      for (std::string cheat : enabled_cheats) {
 +              core::dimension2d<u32> dim =
 +                              m_font->getDimension(utf8_to_wide(cheat).c_str());
 +              u32 x = screensize.Width - 5 - dim.Width;
 +
 +              core::rect<s32> fontbounds(x, y, x + dim.Width, y + dim.Height);
 +              m_font->draw(cheat.c_str(), fontbounds, colors[i], false, false);
 +
 +              y += dim.Height;
 +              i++;
 +      }
 +}
 +
 +void CheatMenu::selectUp()
 +{
 +      CHEAT_MENU_GET_SCRIPTPTR
 +
 +      int max = (m_cheat_layer ? script->m_cheat_categories[m_selected_category]
 +                                                                ->m_cheats.size()
 +                               : script->m_cheat_categories.size()) -
 +                1;
 +      int *selected = m_cheat_layer ? &m_selected_cheat : &m_selected_category;
 +      --*selected;
 +      if (*selected < 0)
 +              *selected = max;
 +}
 +
 +void CheatMenu::selectDown()
 +{
 +      CHEAT_MENU_GET_SCRIPTPTR
 +
 +      int max = (m_cheat_layer ? script->m_cheat_categories[m_selected_category]
 +                                                                ->m_cheats.size()
 +                               : script->m_cheat_categories.size()) -
 +                1;
 +      int *selected = m_cheat_layer ? &m_selected_cheat : &m_selected_category;
 +      ++*selected;
 +      if (*selected > max)
 +              *selected = 0;
 +}
 +
 +void CheatMenu::selectRight()
 +{
 +      if (m_cheat_layer)
 +              return;
 +      m_cheat_layer = true;
 +      m_selected_cheat = 0;
 +}
 +
 +void CheatMenu::selectLeft()
 +{
 +      if (!m_cheat_layer)
 +              return;
 +      m_cheat_layer = false;
 +}
 +
 +void CheatMenu::selectConfirm()
 +{
 +      CHEAT_MENU_GET_SCRIPTPTR
 +
 +      if (m_cheat_layer)
 +              script->toggle_cheat(script->m_cheat_categories[m_selected_category]
 +                                                   ->m_cheats[m_selected_cheat]);
 +}
diff --combined src/map.cpp
index 1648adec3d4b1c5a7ad83efe49d369ae721f59ba,ce69accb5180445ea284882a0680e8e0b6a3fd77..7bc1334b0fbf9aa2238e7abfe29e2006e5c74628
@@@ -139,28 -139,6 +139,21 @@@ MapBlock * Map::getBlockNoCreate(v3s16 
        return block;
  }
  
- bool Map::isNodeUnderground(v3s16 p)
- {
-       v3s16 blockpos = getNodeBlockPos(p);
-       MapBlock *block = getBlockNoCreateNoEx(blockpos);
-       return block && block->getIsUnderground();
- }
 +void Map::listAllLoadedBlocks(std::vector<v3s16> &dst)
 +{
 +      for (auto &sector_it : m_sectors) {
 +              MapSector *sector = sector_it.second;
 +
 +              MapBlockVect blocks;
 +              sector->getBlocks(blocks);
 +
 +              for (MapBlock *block : blocks) {
 +                      v3s16 p = block->getPos();
 +                      dst.push_back(p);
 +              }
 +      }
 +}
 +
  bool Map::isValidPosition(v3s16 p)
  {
        v3s16 blockpos = getNodeBlockPos(p);
@@@ -187,24 -165,32 +180,32 @@@ MapNode Map::getNode(v3s16 p, bool *is_
        return node;
  }
  
- // throws InvalidPositionException if not found
- void Map::setNode(v3s16 p, MapNode & n)
+ static void set_node_in_block(MapBlock *block, v3s16 relpos, MapNode n)
  {
-       v3s16 blockpos = getNodeBlockPos(p);
-       MapBlock *block = getBlockNoCreate(blockpos);
-       v3s16 relpos = p - blockpos*MAP_BLOCKSIZE;
        // Never allow placing CONTENT_IGNORE, it causes problems
        if(n.getContent() == CONTENT_IGNORE){
+               const NodeDefManager *nodedef = block->getParent()->getNodeDefManager();
+               v3s16 blockpos = block->getPos();
+               v3s16 p = blockpos * MAP_BLOCKSIZE + relpos;
                bool temp_bool;
-               errorstream<<"Map::setNode(): Not allowing to place CONTENT_IGNORE"
+               errorstream<<"Not allowing to place CONTENT_IGNORE"
                                <<" while trying to replace \""
-                               <<m_nodedef->get(block->getNodeNoCheck(relpos, &temp_bool)).name
+                               <<nodedef->get(block->getNodeNoCheck(relpos, &temp_bool)).name
                                <<"\" at "<<PP(p)<<" (block "<<PP(blockpos)<<")"<<std::endl;
                return;
        }
        block->setNodeNoCheck(relpos, n);
  }
  
+ // throws InvalidPositionException if not found
+ void Map::setNode(v3s16 p, MapNode & n)
+ {
+       v3s16 blockpos = getNodeBlockPos(p);
+       MapBlock *block = getBlockNoCreate(blockpos);
+       v3s16 relpos = p - blockpos*MAP_BLOCKSIZE;
+       set_node_in_block(block, relpos, n);
+ }
  void Map::addNodeAndUpdate(v3s16 p, MapNode n,
                std::map<v3s16, MapBlock*> &modified_blocks,
                bool remove_metadata)
        // Collect old node for rollback
        RollbackNode rollback_oldnode(this, p, m_gamedef);
  
+       v3s16 blockpos = getNodeBlockPos(p);
+       MapBlock *block = getBlockNoCreate(blockpos);
+       if (block->isDummy())
+               throw InvalidPositionException();
+       v3s16 relpos = p - blockpos * MAP_BLOCKSIZE;
        // This is needed for updating the lighting
-       MapNode oldnode = getNode(p);
+       MapNode oldnode = block->getNodeUnsafe(relpos);
  
        // Remove node metadata
        if (remove_metadata) {
        }
  
        // Set the node on the map
-       // Ignore light (because calling voxalgo::update_lighting_nodes)
-       n.setLight(LIGHTBANK_DAY, 0, m_nodedef);
-       n.setLight(LIGHTBANK_NIGHT, 0, m_nodedef);
-       setNode(p, n);
+       const ContentFeatures &cf = m_nodedef->get(n);
+       const ContentFeatures &oldcf = m_nodedef->get(oldnode);
+       if (cf.lightingEquivalent(oldcf)) {
+               // No light update needed, just copy over the old light.
+               n.setLight(LIGHTBANK_DAY, oldnode.getLightRaw(LIGHTBANK_DAY, oldcf), cf);
+               n.setLight(LIGHTBANK_NIGHT, oldnode.getLightRaw(LIGHTBANK_NIGHT, oldcf), cf);
+               set_node_in_block(block, relpos, n);
+               modified_blocks[blockpos] = block;
+       } else {
+               // Ignore light (because calling voxalgo::update_lighting_nodes)
+               n.setLight(LIGHTBANK_DAY, 0, cf);
+               n.setLight(LIGHTBANK_NIGHT, 0, cf);
+               set_node_in_block(block, relpos, n);
  
-       // Update lighting
-       std::vector<std::pair<v3s16, MapNode> > oldnodes;
-       oldnodes.emplace_back(p, oldnode);
-       voxalgo::update_lighting_nodes(this, oldnodes, modified_blocks);
+               // Update lighting
+               std::vector<std::pair<v3s16, MapNode> > oldnodes;
+               oldnodes.emplace_back(p, oldnode);
+               voxalgo::update_lighting_nodes(this, oldnodes, modified_blocks);
  
-       for (auto &modified_block : modified_blocks) {
-               modified_block.second->expireDayNightDiff();
+               for (auto &modified_block : modified_blocks) {
+                       modified_block.second->expireDayNightDiff();
+               }
        }
  
        // Report for rollback
                action.setSetNode(p, rollback_oldnode, rollback_newnode);
                m_gamedef->rollback()->reportAction(action);
        }
-       /*
-               Add neighboring liquid nodes and this node to transform queue.
-               (it's vital for the node itself to get updated last, if it was removed.)
-        */
-       for (const v3s16 &dir : g_7dirs) {
-               v3s16 p2 = p + dir;
-               bool is_valid_position;
-               MapNode n2 = getNode(p2, &is_valid_position);
-               if(is_valid_position &&
-                               (m_nodedef->get(n2).isLiquid() ||
-                               n2.getContent() == CONTENT_AIR))
-                       m_transforming_liquid.push_back(p2);
-       }
  }
  
  void Map::removeNodeAndUpdate(v3s16 p,
@@@ -339,7 -326,7 +341,7 @@@ struct TimeOrderedMapBlock 
  void Map::timerUpdate(float dtime, float unload_timeout, u32 max_loaded_blocks,
                std::vector<v3s16> *unloaded_blocks)
  {
-       bool save_before_unloading = (mapType() == MAPTYPE_SERVER);
+       bool save_before_unloading = maySaveBlocks();
  
        // Profile modified reasons
        Profiler modprofiler;
        u32 saved_blocks_count = 0;
        u32 block_count_all = 0;
  
+       const auto start_time = porting::getTimeUs();
        beginSave();
  
        // If there is no practical limit, we spare creation of mapblock_queue
                                }
                        }
  
+                       // Delete sector if we emptied it
                        if (all_blocks_deleted) {
                                sector_deletion_queue.push_back(sector_it.first);
                        }
                        }
                }
                block_count_all = mapblock_queue.size();
                // Delete old blocks, and blocks over the limit from the memory
                while (!mapblock_queue.empty() && (mapblock_queue.size() > max_loaded_blocks
                                || mapblock_queue.top().block->getUsageTimer() > unload_timeout)) {
                        deleted_blocks_count++;
                        block_count_all--;
                }
                // Delete empty sectors
                for (auto &sector_it : m_sectors) {
                        if (sector_it.second->empty()) {
                        }
                }
        }
        endSave();
+       const auto end_time = porting::getTimeUs();
+       reportMetrics(end_time - start_time, saved_blocks_count, block_count_all);
  
        // Finally delete the empty sectors
        deleteSectors(sector_deletion_queue);
@@@ -524,11 -519,11 +534,11 @@@ struct NodeNeighbor 
        { }
  };
  
- void Map::transforming_liquid_add(v3s16 p) {
+ void ServerMap::transforming_liquid_add(v3s16 p) {
          m_transforming_liquid.push_back(p);
  }
  
- void Map::transformLiquids(std::map<v3s16, MapBlock*> &modified_blocks,
+ void ServerMap::transformLiquids(std::map<v3s16, MapBlock*> &modified_blocks,
                ServerEnvironment *env)
  {
        u32 loopcount = 0;
@@@ -1231,7 -1226,12 +1241,12 @@@ ServerMap::ServerMap(const std::string 
        m_savedir = savedir;
        m_map_saving_enabled = false;
  
-       m_save_time_counter = mb->addCounter("minetest_core_map_save_time", "Map save time (in nanoseconds)");
+       m_save_time_counter = mb->addCounter(
+               "minetest_map_save_time", "Time spent saving blocks (in microseconds)");
+       m_save_count_counter = mb->addCounter(
+               "minetest_map_saved_blocks", "Number of blocks saved");
+       m_loaded_blocks_gauge = mb->addGauge(
+               "minetest_map_loaded_blocks", "Number of loaded blocks");
  
        m_map_compression_level = rangelim(g_settings->getS16("map_compression_level_disk"), -1, 9);
  
@@@ -1466,11 -1466,7 +1481,7 @@@ MapSector *ServerMap::createSector(v2s1
        /*
                Do not create over max mapgen limit
        */
-       const s16 max_limit_bp = MAX_MAP_GENERATION_LIMIT / MAP_BLOCKSIZE;
-       if (p2d.X < -max_limit_bp ||
-                       p2d.X >  max_limit_bp ||
-                       p2d.Y < -max_limit_bp ||
-                       p2d.Y >  max_limit_bp)
+       if (blockpos_over_max_limit(v3s16(p2d.X, 0, p2d.Y)))
                throw InvalidPositionException("createSector(): pos. over max mapgen limit");
  
        /*
  
        sector = new MapSector(this, p2d, m_gamedef);
  
-       // Sector position on map in nodes
-       //v2s16 nodepos2d = p2d * MAP_BLOCKSIZE;
        /*
                Insert to container
        */
@@@ -1569,6 -1562,29 +1577,29 @@@ bool ServerMap::isBlockInQueue(v3s16 po
        return m_emerge && m_emerge->isBlockInQueue(pos);
  }
  
+ void ServerMap::addNodeAndUpdate(v3s16 p, MapNode n,
+               std::map<v3s16, MapBlock*> &modified_blocks,
+               bool remove_metadata)
+ {
+       Map::addNodeAndUpdate(p, n, modified_blocks, remove_metadata);
+       /*
+               Add neighboring liquid nodes and this node to transform queue.
+               (it's vital for the node itself to get updated last, if it was removed.)
+        */
+       for (const v3s16 &dir : g_7dirs) {
+               v3s16 p2 = p + dir;
+               bool is_valid_position;
+               MapNode n2 = getNode(p2, &is_valid_position);
+               if(is_valid_position &&
+                               (m_nodedef->get(n2).isLiquid() ||
+                               n2.getContent() == CONTENT_AIR))
+                       m_transforming_liquid.push_back(p2);
+       }
+ }
  // N.B.  This requires no synchronization, since data will not be modified unless
  // the VoxelManipulator being updated belongs to the same thread.
  void ServerMap::updateVManip(v3s16 pos)
        vm->m_is_dirty = true;
  }
  
+ void ServerMap::reportMetrics(u64 save_time_us, u32 saved_blocks, u32 all_blocks)
+ {
+       m_loaded_blocks_gauge->set(all_blocks);
+       m_save_time_counter->increment(save_time_us);
+       m_save_count_counter->increment(saved_blocks);
+ }
  void ServerMap::save(ModifiedState save_level)
  {
        if (!m_map_saving_enabled) {
                return;
        }
  
-       u64 start_time = porting::getTimeNs();
+       const auto start_time = porting::getTimeUs();
  
        if(save_level == MOD_STATE_CLEAN)
                infostream<<"ServerMap: Saving whole map, this can take time."
                modprofiler.print(infostream);
        }
  
-       auto end_time = porting::getTimeNs();
-       m_save_time_counter->increment(end_time - start_time);
+       const auto end_time = porting::getTimeUs();
+       reportMetrics(end_time - start_time, block_count, block_count_all);
  }
  
  void ServerMap::listAllLoadableBlocks(std::vector<v3s16> &dst)
                dbase_ro->listAllLoadableBlocks(dst);
  }
  
 -void ServerMap::listAllLoadedBlocks(std::vector<v3s16> &dst)
 -{
 -      for (auto &sector_it : m_sectors) {
 -              MapSector *sector = sector_it.second;
 -
 -              MapBlockVect blocks;
 -              sector->getBlocks(blocks);
 -
 -              for (MapBlock *block : blocks) {
 -                      v3s16 p = block->getPos();
 -                      dst.push_back(p);
 -              }
 -      }
 -}
 -
  MapDatabase *ServerMap::createDatabase(
        const std::string &name,
        const std::string &savedir,
@@@ -1878,6 -1916,7 +1916,7 @@@ MMVManip::MMVManip(Map *map)
                VoxelManipulator(),
                m_map(map)
  {
+       assert(map);
  }
  
  void MMVManip::initialEmerge(v3s16 blockpos_min, v3s16 blockpos_max,
  {
        TimeTaker timer1("initialEmerge", &emerge_time);
  
+       assert(m_map);
        // Units of these are MapBlocks
        v3s16 p_min = blockpos_min;
        v3s16 p_max = blockpos_max;
@@@ -1968,6 -2009,7 +2009,7 @@@ void MMVManip::blitBackAll(std::map<v3s
  {
        if(m_area.getExtent() == v3s16(0,0,0))
                return;
+       assert(m_map);
  
        /*
                Copy data of all blocks
        }
  }
  
+ MMVManip *MMVManip::clone() const
+ {
+       MMVManip *ret = new MMVManip();
+       const s32 size = m_area.getVolume();
+       ret->m_area = m_area;
+       if (m_data) {
+               ret->m_data = new MapNode[size];
+               memcpy(ret->m_data, m_data, size * sizeof(MapNode));
+       }
+       if (m_flags) {
+               ret->m_flags = new u8[size];
+               memcpy(ret->m_flags, m_flags, size * sizeof(u8));
+       }
+       ret->m_is_dirty = m_is_dirty;
+       // Even if the copy is disconnected from a map object keep the information
+       // needed to write it back to one
+       ret->m_loaded_blocks = m_loaded_blocks;
+       return ret;
+ }
+ void MMVManip::reparent(Map *map)
+ {
+       assert(map && !m_map);
+       m_map = map;
+ }
  //END
diff --combined src/map.h
index 9c5c16368b1e98e39074179ecaba9f40a6751c13,1e5499586162780b8e71a1c0b423e7e15a1ac781..248312ebe5436d3f6539fe949ed9931b808583b9
+++ b/src/map.h
@@@ -54,10 -54,6 +54,6 @@@ struct BlockMakeData
        MapEditEvent
  */
  
- #define MAPTYPE_BASE 0
- #define MAPTYPE_SERVER 1
- #define MAPTYPE_CLIENT 2
  enum MapEditEventType{
        // Node added (changed from air or something else to something)
        MEET_ADDNODE,
@@@ -127,11 -123,6 +123,6 @@@ public
        virtual ~Map();
        DISABLE_CLASS_COPY(Map);
  
-       virtual s32 mapType() const
-       {
-               return MAPTYPE_BASE;
-       }
        /*
                Drop (client) or delete (server) the map.
        */
        MapBlock * getBlockNoCreate(v3s16 p);
        // Returns NULL if not found
        MapBlock * getBlockNoCreateNoEx(v3s16 p);
 -
 +      
 +      void listAllLoadedBlocks(std::vector<v3s16> &dst);
 +      
        /* Server overrides */
        virtual MapBlock * emergeBlock(v3s16 p, bool create_blank=true)
        { return getBlockNoCreateNoEx(p); }
  
        inline const NodeDefManager * getNodeDefManager() { return m_nodedef; }
  
-       // Returns InvalidPositionException if not found
-       bool isNodeUnderground(v3s16 p);
        bool isValidPosition(v3s16 p);
  
        // throws InvalidPositionException if not found
        /*
                These handle lighting but not faces.
        */
-       void addNodeAndUpdate(v3s16 p, MapNode n,
+       virtual void addNodeAndUpdate(v3s16 p, MapNode n,
                        std::map<v3s16, MapBlock*> &modified_blocks,
                        bool remove_metadata = true);
        void removeNodeAndUpdate(v3s16 p,
  
        virtual void save(ModifiedState save_level) { FATAL_ERROR("FIXME"); }
  
+       /*
+               Return true unless the map definitely cannot save blocks.
+       */
+       virtual bool maySaveBlocks() { return true; }
        // Server implements these.
        // Client leaves them as no-op.
        virtual bool saveBlock(MapBlock *block) { return false; }
  
        /*
                Updates usage timers and unloads unused blocks and sectors.
-               Saves modified blocks before unloading on MAPTYPE_SERVER.
+               Saves modified blocks before unloading if possible.
        */
        void timerUpdate(float dtime, float unload_timeout, u32 max_loaded_blocks,
                        std::vector<v3s16> *unloaded_blocks=NULL);
  
        /*
                Unloads all blocks with a zero refCount().
-               Saves modified blocks before unloading on MAPTYPE_SERVER.
+               Saves modified blocks before unloading if possible.
        */
        void unloadUnreferencedBlocks(std::vector<v3s16> *unloaded_blocks=NULL);
  
        // For debug printing. Prints "Map: ", "ServerMap: " or "ClientMap: "
        virtual void PrintInfo(std::ostream &out);
  
-       void transformLiquids(std::map<v3s16, MapBlock*> & modified_blocks,
-                       ServerEnvironment *env);
        /*
                Node metadata
                These are basically coordinate wrappers to MapBlock
                Variables
        */
  
-       void transforming_liquid_add(v3s16 p);
        bool isBlockOccluded(MapBlock *block, v3s16 cam_pos_nodes);
  protected:
-       friend class LuaVoxelManip;
        IGameDef *m_gamedef;
  
        std::set<MapEventReceiver*> m_event_receivers;
        MapSector *m_sector_cache = nullptr;
        v2s16 m_sector_cache_p;
  
-       // Queued transforming water nodes
-       UniqueQueue<v3s16> m_transforming_liquid;
        // This stores the properties of the nodes on the map.
        const NodeDefManager *m_nodedef;
  
+       // Can be implemented by child class
+       virtual void reportMetrics(u64 save_time_us, u32 saved_blocks, u32 all_blocks) {}
        bool determineAdditionalOcclusionCheck(const v3s16 &pos_camera,
                const core::aabbox3d<s16> &block_bounds, v3s16 &check);
        bool isOccluded(const v3s16 &pos_camera, const v3s16 &pos_target,
                float step, float stepfac, float start_offset, float end_offset,
                u32 needed_count);
- private:
-       f32 m_transforming_liquid_loop_count_multiplier = 1.0f;
-       u32 m_unprocessed_count = 0;
-       u64 m_inc_trending_up_start_time = 0; // milliseconds
-       bool m_queue_size_timer_started = false;
  };
  
  /*
@@@ -322,11 -300,6 +302,6 @@@ public
        ServerMap(const std::string &savedir, IGameDef *gamedef, EmergeManager *emerge, MetricsBackend *mb);
        ~ServerMap();
  
-       s32 mapType() const
-       {
-               return MAPTYPE_SERVER;
-       }
        /*
                Get a sector from somewhere.
                - Check memory
                - Create blank filled with CONTENT_IGNORE
  
        */
-       MapBlock *emergeBlock(v3s16 p, bool create_blank=true);
+       MapBlock *emergeBlock(v3s16 p, bool create_blank=true) override;
  
        /*
                Try to get a block.
  
        bool isBlockInQueue(v3s16 pos);
  
+       void addNodeAndUpdate(v3s16 p, MapNode n,
+                       std::map<v3s16, MapBlock*> &modified_blocks,
+                       bool remove_metadata) override;
        /*
                Database functions
        */
        static MapDatabase *createDatabase(const std::string &name, const std::string &savedir, Settings &conf);
  
        // Call these before and after saving of blocks
-       void beginSave();
-       void endSave();
+       void beginSave() override;
+       void endSave() override;
  
-       void save(ModifiedState save_level);
+       void save(ModifiedState save_level) override;
        void listAllLoadableBlocks(std::vector<v3s16> &dst);
 -      void listAllLoadedBlocks(std::vector<v3s16> &dst);
  
        MapgenParams *getMapgenParams();
  
-       bool saveBlock(MapBlock *block);
+       bool saveBlock(MapBlock *block) override;
        static bool saveBlock(MapBlock *block, MapDatabase *db, int compression_level = -1);
        MapBlock* loadBlock(v3s16 p);
        // Database version
        void loadBlock(std::string *blob, v3s16 p3d, MapSector *sector, bool save_after_load=false);
  
-       bool deleteBlock(v3s16 blockpos);
+       bool deleteBlock(v3s16 blockpos) override;
  
        void updateVManip(v3s16 pos);
  
        // For debug printing
-       virtual void PrintInfo(std::ostream &out);
+       void PrintInfo(std::ostream &out) override;
  
        bool isSavingEnabled(){ return m_map_saving_enabled; }
  
        bool repairBlockLight(v3s16 blockpos,
                std::map<v3s16, MapBlock *> *modified_blocks);
  
+       void transformLiquids(std::map<v3s16, MapBlock*> & modified_blocks,
+                       ServerEnvironment *env);
+       void transforming_liquid_add(v3s16 p);
        MapSettingsManager settings_mgr;
  
+ protected:
+       void reportMetrics(u64 save_time_us, u32 saved_blocks, u32 all_blocks) override;
  private:
+       friend class LuaVoxelManip;
        // Emerge manager
        EmergeManager *m_emerge;
  
  
        std::set<v3s16> m_chunks_in_progress;
  
+       // Queued transforming water nodes
+       UniqueQueue<v3s16> m_transforming_liquid;
+       f32 m_transforming_liquid_loop_count_multiplier = 1.0f;
+       u32 m_unprocessed_count = 0;
+       u64 m_inc_trending_up_start_time = 0; // milliseconds
+       bool m_queue_size_timer_started = false;
        /*
                Metadata is re-written on disk only if this is true.
                This is reset to false when written on disk.
        MapDatabase *dbase = nullptr;
        MapDatabase *dbase_ro = nullptr;
  
+       // Map metrics
+       MetricGaugePtr m_loaded_blocks_gauge;
        MetricCounterPtr m_save_time_counter;
+       MetricCounterPtr m_save_count_counter;
  };
  
  
@@@ -457,10 -456,25 +457,25 @@@ public
        void blitBackAll(std::map<v3s16, MapBlock*> * modified_blocks,
                bool overwrite_generated = true);
  
+       /*
+               Creates a copy of this VManip including contents, the copy will not be
+               associated with a Map.
+       */
+       MMVManip *clone() const;
+       // Reassociates a copied VManip to a map
+       void reparent(Map *map);
+       // Is it impossible to call initialEmerge / blitBackAll?
+       inline bool isOrphan() const { return !m_map; }
        bool m_is_dirty = false;
  
  protected:
-       Map *m_map;
+       MMVManip() {};
+       // may be null
+       Map *m_map = nullptr;
        /*
                key = blockpos
                value = flags describing the block
index 6497bb26a92f5ad65aa1d13979ff00131a7621fb,55d20d673ab5c4378b9b659efe2105a53aa6cfc8..29e3364db0e2d6ef4a50b365832076702820c0be
@@@ -17,7 -17,6 +17,7 @@@ with this program; if not, write to th
  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  */
  
 +#include <iostream>
  #include "client/client.h"
  
  #include "util/base64.h"
@@@ -34,7 -33,6 +34,7 @@@
  #include "server.h"
  #include "util/strfnd.h"
  #include "client/clientevent.h"
 +#include "client/content_cao.h"
  #include "client/sound.h"
  #include "network/clientopcodes.h"
  #include "network/connection.h"
@@@ -45,6 -43,7 +45,7 @@@
  #include "tileanimation.h"
  #include "gettext.h"
  #include "skyparams.h"
+ #include <memory>
  
  void Client::handleCommand_Deprecated(NetworkPacket* pkt)
  {
@@@ -184,7 -183,7 +185,7 @@@ void Client::handleCommand_AccessDenied
        m_access_denied_reason = "Unknown";
  
        if (pkt->getCommand() != TOCLIENT_ACCESS_DENIED) {
-               // 13/03/15 Legacy code from 0.4.12 and lesser but is still used
+               // Legacy code from 0.4.12 and older but is still used
                // in some places of the server code
                if (pkt->getSize() >= 2) {
                        std::wstring wide_reason;
        if (pkt->getSize() < 1)
                return;
  
-       u8 denyCode = SERVER_ACCESSDENIED_UNEXPECTED_DATA;
+       u8 denyCode;
        *pkt >> denyCode;
        if (denyCode == SERVER_ACCESSDENIED_SHUTDOWN ||
                        denyCode == SERVER_ACCESSDENIED_CRASH) {
                *pkt >> m_access_denied_reason;
-               if (m_access_denied_reason.empty()) {
+               if (m_access_denied_reason.empty())
                        m_access_denied_reason = accessDeniedStrings[denyCode];
-               }
                u8 reconnect;
                *pkt >> reconnect;
                m_access_denied_reconnect = reconnect & 1;
                // Until then (which may be never), this is outside
                // of the defined protocol.
                *pkt >> m_access_denied_reason;
-               if (m_access_denied_reason.empty()) {
+               if (m_access_denied_reason.empty())
                        m_access_denied_reason = "Unknown";
-               }
        }
  }
  
@@@ -451,9 -449,6 +451,9 @@@ void Client::handleCommand_ActiveObject
                }
        */
  
 +      LocalPlayer *player = m_env.getLocalPlayer();
 +      bool try_reattach = player && player->isWaitingForReattach();
 +
        try {
                u8 type;
                u16 removed_count, added_count, id;
                for (u16 i = 0; i < added_count; i++) {
                        *pkt >> id >> type;
                        m_env.addActiveObject(id, type, pkt->readLongString());
 +                      if (try_reattach)
 +                              player->tryReattach(id);
                }
        } catch (PacketError &e) {
                infostream << "handleCommand_ActiveObjectRemoveAdd: " << e.what()
@@@ -600,15 -593,12 +600,15 @@@ void Client::handleCommand_MovePlayer(N
        LocalPlayer *player = m_env.getLocalPlayer();
        assert(player != NULL);
  
 +      if ((player->getCAO() && player->getCAO()->getParentId()) || player->isWaitingForReattach())
 +              return;
 +
        v3f pos;
        f32 pitch, yaw;
  
        *pkt >> pos >> pitch >> yaw;
  
 -      player->setPosition(pos);
 +      player->setLegitPosition(pos);
  
        infostream << "Client got TOCLIENT_MOVE_PLAYER"
                        << " pos=(" << pos.X << "," << pos.Y << "," << pos.Z << ")"
                it would just force the pitch and yaw values to whatever
                the camera points to.
        */
 +
 +      if (g_settings->getBool("no_force_rotate"))
 +              return;
 +
        ClientEvent *event = new ClientEvent();
        event->type = CE_PLAYER_FORCE_MOVE;
        event->player_force_move.pitch = pitch;
@@@ -837,11 -823,6 +837,11 @@@ void Client::handleCommand_PlaySound(Ne
                *pkt >> ephemeral;
        } catch (PacketError &e) {};
  
 +      SimpleSoundSpec sound_spec(name, gain, fade, pitch);
 +
 +      if (m_mods_loaded && m_script->on_play_sound(sound_spec))
 +              return;
 +
        // Start playing
        int client_id = -1;
        switch(type) {
@@@ -918,11 -899,6 +918,6 @@@ void Client::handleCommand_Privileges(N
                m_privileges.insert(priv);
                infostream << priv << " ";
        }
-       // Enable basic_debug on server versions before it was added
-       if (m_proto_ver < 40)
-               m_privileges.insert("basic_debug");
        infostream << std::endl;
  }
  
@@@ -996,9 -972,6 +991,9 @@@ void Client::handleCommand_SpawnParticl
        event->type           = CE_SPAWN_PARTICLE;
        event->spawn_particle = new ParticleParameters(p);
  
 +      if (m_mods_loaded && m_script->on_spawn_particle(*event->spawn_particle))
 +              return;
 +
        m_client_event_queue.push(event);
  }
  
@@@ -1196,12 -1169,6 +1191,12 @@@ void Client::handleCommand_HudSetFlags(
        player->hud_flags &= ~mask;
        player->hud_flags |= flags;
  
 +      if (g_settings->getBool("hud_flags_bypass"))
 +              player->hud_flags = HUD_FLAG_HOTBAR_VISIBLE     | HUD_FLAG_HEALTHBAR_VISIBLE |
 +                      HUD_FLAG_CROSSHAIR_VISIBLE | HUD_FLAG_WIELDITEM_VISIBLE |
 +                      HUD_FLAG_BREATHBAR_VISIBLE | HUD_FLAG_MINIMAP_VISIBLE   |
 +                      HUD_FLAG_MINIMAP_RADAR_VISIBLE;
 +
        m_minimap_disabled_by_server = !(player->hud_flags & HUD_FLAG_MINIMAP_VISIBLE);
        bool m_minimap_radar_disabled_by_server = !(player->hud_flags & HUD_FLAG_MINIMAP_RADAR_VISIBLE);
  
@@@ -1269,19 -1236,17 +1264,17 @@@ void Client::handleCommand_HudSetSky(Ne
                } catch (...) {}
  
                // Use default skybox settings:
-               SkyboxDefaults sky_defaults;
-               SunParams sun = sky_defaults.getSunDefaults();
-               MoonParams moon = sky_defaults.getMoonDefaults();
-               StarParams stars = sky_defaults.getStarDefaults();
+               SunParams sun = SkyboxDefaults::getSunDefaults();
+               MoonParams moon = SkyboxDefaults::getMoonDefaults();
+               StarParams stars = SkyboxDefaults::getStarDefaults();
  
                // Fix for "regular" skies, as color isn't kept:
                if (skybox.type == "regular") {
-                       skybox.sky_color = sky_defaults.getSkyColorDefaults();
+                       skybox.sky_color = SkyboxDefaults::getSkyColorDefaults();
                        skybox.fog_tint_type = "default";
                        skybox.fog_moon_tint = video::SColor(255, 255, 255, 255);
                        skybox.fog_sun_tint = video::SColor(255, 255, 255, 255);
-               }
-               else {
+               } else {
                        sun.visible = false;
                        sun.sunrise_visible = false;
                        moon.visible = false;
@@@ -1431,6 -1396,8 +1424,8 @@@ void Client::handleCommand_LocalPlayerA
        *pkt >> player->local_animations[2];
        *pkt >> player->local_animations[3];
        *pkt >> player->local_animation_speed;
+       player->last_animation = -1;
  }
  
  void Client::handleCommand_EyeOffset(NetworkPacket* pkt)
@@@ -1516,8 -1483,6 +1511,8 @@@ void Client::handleCommand_CSMRestricti
  
  void Client::handleCommand_PlayerSpeed(NetworkPacket *pkt)
  {
 +      if (g_settings->getBool("antiknockback"))
 +              return;
        v3f added_vel;
  
        *pkt >> added_vel;
@@@ -1589,7 -1554,7 +1584,7 @@@ void Client::handleCommand_MediaPush(Ne
        m_media_pushed_files.insert(filename);
  
        // create a downloader for this file
-       auto downloader = new SingleMediaDownloader(cached);
+       auto downloader(std::make_shared<SingleMediaDownloader>(cached));
        m_pending_media_downloads.emplace_back(token, downloader);
        downloader->addFile(filename, raw_hash);
        for (const auto &baseurl : m_remote_media_servers)
@@@ -1716,3 -1681,11 +1711,11 @@@ void Client::handleCommand_MinimapModes
        if (m_minimap)
                m_minimap->setModeIndex(mode);
  }
+ void Client::handleCommand_SetLighting(NetworkPacket *pkt)
+ {
+       Lighting& lighting = m_env.getLocalPlayer()->getLighting();
+       if (pkt->getRemainingBytes() >= 4)
+               *pkt >> lighting.shadow_intensity;
+ }
diff --combined src/nodedef.cpp
index f27a8154b3ad5f9e8e701244375d2764d07712ad,8d63870b3c368b5fe87baa950396deadd070a872..2b8ebd77393c8ace40479fc9afb65d8ff5f70e1f
@@@ -33,6 -33,7 +33,7 @@@ with this program; if not, write to th
  #include "nameidmapping.h"
  #include "util/numeric.h"
  #include "util/serialize.h"
+ #include "util/string.h"
  #include "exceptions.h"
  #include "debug.h"
  #include "gamedef.h"
@@@ -207,7 -208,28 +208,28 @@@ void TileDef::serialize(std::ostream &o
        u8 version = 6;
        writeU8(os, version);
  
-       os << serializeString16(name);
+       if (protocol_version > 39) {
+               os << serializeString16(name);
+       } else {
+               // Before f018737, TextureSource::getTextureAverageColor did not handle
+               // missing textures. "[png" can be used as base texture, but is not known
+               // on older clients. Hence use "blank.png" to avoid this problem.
+               // To be forward-compatible with future base textures/modifiers,
+               // we apply the same prefix to any texture beginning with [,
+               // except for the ones that are supported on older clients.
+               bool pass_through = true;
+               if (!name.empty() && name[0] == '[') {
+                       pass_through = str_starts_with(name, "[combine:") ||
+                               str_starts_with(name, "[inventorycube{") ||
+                               str_starts_with(name, "[lowpart:");
+               }
+               if (pass_through)
+                       os << serializeString16(name);
+               else
+                       os << serializeString16("blank.png^" + name);
+       }
        animation.serialize(os, version);
        bool has_scale = scale > 0;
        u16 flags = 0;
@@@ -330,7 -352,7 +352,7 @@@ void ContentFeatures::reset(
                Cached stuff
        */
  #ifndef SERVER
 -      solidness = 2;
 +      solidness = 0;
        visual_solidness = 0;
        backface_culling = true;
  
        palette_name = "";
        palette = NULL;
        node_dig_prediction = "air";
+       move_resistance = 0;
+       liquid_move_physics = false;
  }
  
  void ContentFeatures::setAlphaFromLegacy(u8 legacy_alpha)
@@@ -440,7 -464,12 +464,12 @@@ void ContentFeatures::serialize(std::os
        writeU16(os, groups.size());
        for (const auto &group : groups) {
                os << serializeString16(group.first);
-               writeS16(os, group.second);
+               if (protocol_version < 41 && group.first.compare("bouncy") == 0) {
+                       // Old clients may choke on negative bouncy value
+                       writeS16(os, abs(group.second));
+               } else {
+                       writeS16(os, group.second);
+               }
        }
        writeU8(os, param_type);
        writeU8(os, param_type_2);
        writeU32(os, damage_per_second);
  
        // liquid
-       writeU8(os, liquid_type);
+       LiquidType liquid_type_bc = liquid_type;
+       if (protocol_version <= 39) {
+               // Since commit 7f25823, liquid drawtypes can be used even with LIQUID_NONE
+               // solution: force liquid type accordingly to accepted values
+               if (drawtype == NDT_LIQUID)
+                       liquid_type_bc = LIQUID_SOURCE;
+               else if (drawtype == NDT_FLOWINGLIQUID)
+                       liquid_type_bc = LIQUID_FLOWING;
+       }
+       writeU8(os, liquid_type_bc);
        os << serializeString16(liquid_alternative_flowing);
        os << serializeString16(liquid_alternative_source);
        writeU8(os, liquid_viscosity);
        writeU8(os, legacy_facedir_simple);
        writeU8(os, legacy_wallmounted);
  
+       // new attributes
        os << serializeString16(node_dig_prediction);
        writeU8(os, leveled_max);
        writeU8(os, alpha);
+       writeU8(os, move_resistance);
+       writeU8(os, liquid_move_physics);
  }
  
  void ContentFeatures::deSerialize(std::istream &is)
  
        // liquid
        liquid_type = (enum LiquidType) readU8(is);
+       liquid_move_physics = liquid_type != LIQUID_NONE;
        liquid_alternative_flowing = deSerializeString16(is);
        liquid_alternative_source = deSerializeString16(is);
        liquid_viscosity = readU8(is);
+       move_resistance = liquid_viscosity; // set default move_resistance
        liquid_renewable = readU8(is);
        liquid_range = readU8(is);
        drowning = readU8(is);
                if (is.eof())
                        throw SerializationError("");
                alpha = static_cast<enum AlphaMode>(tmp);
+               tmp = readU8(is);
+               if (is.eof())
+                       throw SerializationError("");
+               move_resistance = tmp;
+               tmp = readU8(is);
+               if (is.eof())
+                       throw SerializationError("");
+               liquid_move_physics = tmp;
        } catch(SerializationError &e) {};
  }
  
@@@ -634,7 -687,7 +687,7 @@@ static void fillTileAttribs(ITextureSou
        bool has_scale = tiledef.scale > 0;
        bool use_autoscale = tsettings.autoscale_mode == AUTOSCALE_FORCE ||
                (tsettings.autoscale_mode == AUTOSCALE_ENABLE && !has_scale);
-       if (use_autoscale) {
+       if (use_autoscale && layer->texture) {
                auto texture_size = layer->texture->getOriginalSize();
                float base_size = tsettings.node_texture_size;
                float size = std::fmin(texture_size.Width, texture_size.Height);
        // Animation parameters
        int frame_count = 1;
        if (layer->material_flags & MATERIAL_FLAG_ANIMATION) {
+               assert(layer->texture);
                int frame_length_ms;
                tiledef.animation.determineParams(layer->texture->getOriginalSize(),
                                &frame_count, &frame_length_ms, NULL);
        if (frame_count == 1) {
                layer->material_flags &= ~MATERIAL_FLAG_ANIMATION;
        } else {
-               std::ostringstream os(std::ios::binary);
-               if (!layer->frames) {
+               assert(layer->texture);
+               if (!layer->frames)
                        layer->frames = new std::vector<FrameSpec>();
-               }
                layer->frames->resize(frame_count);
  
+               std::ostringstream os(std::ios::binary);
                for (int i = 0; i < frame_count; i++) {
                        FrameSpec frame;
  
                        os.str("");
@@@ -779,8 -832,10 +832,10 @@@ void ContentFeatures::updateTextures(IT
        TileDef tdef[6];
        for (u32 j = 0; j < 6; j++) {
                tdef[j] = tiledef[j];
-               if (tdef[j].name.empty())
-                       tdef[j].name = "unknown_node.png";
+               if (tdef[j].name.empty()) {
+                       tdef[j].name = "no_texture.png";
+                       tdef[j].backface_culling = false;
+               }
        }
        // also the overlay tiles
        TileDef tdef_overlay[6];
                tdef_overlay[j] = tiledef_overlay[j];
        // also the special tiles
        TileDef tdef_spec[6];
-       for (u32 j = 0; j < CF_SPECIAL_COUNT; j++)
+       for (u32 j = 0; j < CF_SPECIAL_COUNT; j++) {
                tdef_spec[j] = tiledef_special[j];
+       }
  
        bool is_liquid = false;
  
@@@ -1035,6 -1091,10 +1091,10 @@@ void NodeDefManager::clear(
        {
                ContentFeatures f;
                f.name = "unknown";
+               TileDef unknownTile;
+               unknownTile.name = "unknown_node.png";
+               for (int t = 0; t < 6; t++)
+                       f.tiledef[t] = unknownTile;
                // Insert directly into containers
                content_t c = CONTENT_UNKNOWN;
                m_content_features[c] = f;
@@@ -1301,31 -1361,15 +1361,31 @@@ void NodeDefManager::eraseIdFromGroups(
  
  
  // IWritableNodeDefManager
 -content_t NodeDefManager::set(const std::string &name, const ContentFeatures &def)
 +content_t NodeDefManager::set(const std::string &name, const ContentFeatures &d)
  {
 +      ContentFeatures def = d;
 +      
        // Pre-conditions
        assert(name != "");
        assert(name != "ignore");
        assert(name == def.name);
  
        content_t id = CONTENT_IGNORE;
 -      if (!m_name_id_mapping.getId(name, id)) { // ignore aliases
 +      
 +      if (m_name_id_mapping.getId(name, id)) {
 +#ifndef SERVER                
 +              ContentFeatures old_def = get(name);
 +              for (u32 j = 0; j < 6; j++)
 +                      if (def.tiledef[j].name.empty())
 +                              def.tiledef[j] = old_def.tiledef[j];
 +              for (u32 j = 0; j < 6; j++)
 +                      if (def.tiledef_overlay[j].name.empty())
 +                              def.tiledef_overlay[j] = old_def.tiledef_overlay[j];
 +              for (u32 j = 0; j < CF_SPECIAL_COUNT; j++)
 +                      if (def.tiledef_special[j].name.empty())
 +                              def.tiledef_special[j] = old_def.tiledef_special[j];
 +#endif
 +      } else {
                // Get new id
                id = allocateId();
                if (id == CONTENT_IGNORE) {
diff --combined src/player.cpp
index 13b79da0451e3a9db3e44df1ac5950b66e4f4399,1e064c1dac557d43c92ab9663c9d552f4c3f8e7b..789d852eab325c12e09083ae77458d387190552a
@@@ -19,6 -19,7 +19,7 @@@ with this program; if not, write to th
  
  #include "player.h"
  
+ #include <cmath>
  #include "threading/mutex_auto_lock.h"
  #include "util/numeric.h"
  #include "hud.h"
@@@ -70,7 -71,7 +71,7 @@@ Player::Player(const char *name, IItemD
                HUD_FLAG_HOTBAR_VISIBLE    | HUD_FLAG_HEALTHBAR_VISIBLE |
                HUD_FLAG_CROSSHAIR_VISIBLE | HUD_FLAG_WIELDITEM_VISIBLE |
                HUD_FLAG_BREATHBAR_VISIBLE | HUD_FLAG_MINIMAP_VISIBLE   |
-               HUD_FLAG_MINIMAP_RADAR_VISIBLE;
+               HUD_FLAG_MINIMAP_RADAR_VISIBLE | HUD_FLAG_BASIC_DEBUG;
  
        hud_hotbar_itemcount = HUD_HOTBAR_ITEMCOUNT_DEFAULT;
  
@@@ -159,16 -160,73 +160,74 @@@ void Player::clearHud(
        }
  }
  
+ #ifndef SERVER
+ u32 PlayerControl::getKeysPressed() const
+ {
+       u32 keypress_bits =
+               ( (u32)(jump  & 1) << 4) |
+               ( (u32)(aux1  & 1) << 5) |
+               ( (u32)(sneak & 1) << 6) |
+               ( (u32)(dig   & 1) << 7) |
+               ( (u32)(place & 1) << 8) |
+               ( (u32)(zoom  & 1) << 9)
+       ;
+       // If any direction keys are pressed pass those through
+       if (direction_keys != 0)
+       {
+               keypress_bits |= direction_keys;
+       }
+       // Otherwise set direction keys based on joystick movement (for mod compatibility)
+       else if (isMoving())
+       {
+               float abs_d;
+               // (absolute value indicates forward / backward)
+               abs_d = abs(movement_direction);
+               if (abs_d < 3.0f / 8.0f * M_PI)
+                       keypress_bits |= (u32)1; // Forward
+               if (abs_d > 5.0f / 8.0f * M_PI)
+                       keypress_bits |= (u32)1 << 1; // Backward
+               // rotate entire coordinate system by 90 degree
+               abs_d = movement_direction + M_PI_2;
+               if (abs_d >= M_PI)
+                       abs_d -= 2 * M_PI;
+               abs_d = abs(abs_d);
+               // (value now indicates left / right)
+               if (abs_d < 3.0f / 8.0f * M_PI)
+                       keypress_bits |= (u32)1 << 2; // Left
+               if (abs_d > 5.0f / 8.0f * M_PI)
+                       keypress_bits |= (u32)1 << 3; // Right
+       }
+       return keypress_bits;
+ }
+ #endif
+ void PlayerControl::unpackKeysPressed(u32 keypress_bits)
+ {
+       direction_keys = keypress_bits & 0xf;
+       jump  = keypress_bits & (1 << 4);
+       aux1  = keypress_bits & (1 << 5);
+       sneak = keypress_bits & (1 << 6);
+       dig   = keypress_bits & (1 << 7);
+       place = keypress_bits & (1 << 8);
+       zoom  = keypress_bits & (1 << 9);
+ }
  void PlayerSettings::readGlobalSettings()
  {
 -      free_move = g_settings->getBool("free_move");
 +      freecam = g_settings->getBool("freecam");
 +      free_move = g_settings->getBool("free_move") || freecam;
        pitch_move = g_settings->getBool("pitch_move");
 -      fast_move = g_settings->getBool("fast_move");
 +      fast_move = g_settings->getBool("fast_move") || freecam;
        continuous_forward = g_settings->getBool("continuous_forward");
 -      always_fly_fast = g_settings->getBool("always_fly_fast");
 +      always_fly_fast = g_settings->getBool("always_fly_fast") || freecam;
        aux1_descends = g_settings->getBool("aux1_descends");
 -      noclip = g_settings->getBool("noclip");
 +      noclip = g_settings->getBool("noclip") || freecam;
        autojump = g_settings->getBool("autojump");
  }
  
diff --combined src/player.h
index 8f7b0a3fecac53732f5ae9f80a459481f1fa72b1,d769acdad8067d9a0a25e64c62b5f237156830c0..0216cfe9cbbc89e8c012176bbe7b05b0ce2fd8fa
@@@ -49,18 -49,18 +49,18 @@@ struct PlayerContro
        PlayerControl() = default;
  
        PlayerControl(
-               bool a_jump,
-               bool a_aux1,
-               bool a_sneak,
+               bool a_up, bool a_down, bool a_left, bool a_right,
+               bool a_jump, bool a_aux1, bool a_sneak,
                bool a_zoom,
-               bool a_dig,
-               bool a_place,
-               float a_pitch,
-               float a_yaw,
-               float a_movement_speed,
-               float a_movement_direction
+               bool a_dig, bool a_place,
+               float a_pitch, float a_yaw,
+               float a_movement_speed, float a_movement_direction
        )
        {
+               // Encode direction keys into a single value so nobody uses it accidentally
+               // as movement_{speed,direction} is supposed to be the source of truth.
+               direction_keys = (a_up&1) | ((a_down&1) << 1) |
+                       ((a_left&1) << 2) | ((a_right&1) << 3);
                jump = a_jump;
                aux1 = a_aux1;
                sneak = a_sneak;
                movement_speed = a_movement_speed;
                movement_direction = a_movement_direction;
        }
+ #ifndef SERVER
+       // For client use
+       u32 getKeysPressed() const;
+       inline bool isMoving() const { return movement_speed > 0.001f; }
+ #endif
+       // For server use
+       void unpackKeysPressed(u32 keypress_bits);
+       u8 direction_keys = 0;
        bool jump = false;
        bool aux1 = false;
        bool sneak = false;
        bool zoom = false;
        bool dig = false;
        bool place = false;
+       // Note: These four are NOT available on the server
        float pitch = 0.0f;
        float yaw = 0.0f;
-       // Note: These two are NOT available on the server
        float movement_speed = 0.0f;
        float movement_direction = 0.0f;
  };
@@@ -90,15 -101,14 +101,15 @@@ struct PlayerSetting
        bool free_move = false;
        bool pitch_move = false;
        bool fast_move = false;
 +      bool freecam = false;
        bool continuous_forward = false;
        bool always_fly_fast = false;
        bool aux1_descends = false;
        bool noclip = false;
        bool autojump = false;
  
 -      const std::string setting_names[8] = {
 -              "free_move", "pitch_move", "fast_move", "continuous_forward", "always_fly_fast",
 +      const std::string setting_names[9] = {
 +              "free_move", "pitch_move", "fast_move", "freecam", "continuous_forward", "always_fly_fast",
                "aux1_descends", "noclip", "autojump"
        };
        void readGlobalSettings();
@@@ -190,8 -200,6 +201,6 @@@ public
                return m_fov_override_spec;
        }
  
-       u32 keyPressed = 0;
        HudElement* getHud(u32 id);
        u32         addHud(HudElement* hud);
        HudElement* removeHud(u32 id);
index 8ca3a722fb505261977281155d5e661234ddd89d,79830ddb4b54845ad6c82ab7106503f5fb2ca50d..0bdeaab9ee276a518bb926b01cd516f65cd0faa0
@@@ -23,7 -23,6 +23,7 @@@ with this program; if not, write to th
  #include "object_properties.h"
  #include "collision.h"
  #include "cpp_api/s_node.h"
 +#include "lua_api/l_clientobject.h"
  #include "lua_api/l_object.h"
  #include "lua_api/l_item.h"
  #include "common/c_internal.h"
@@@ -175,12 -174,10 +175,12 @@@ void push_item_definition_full(lua_Stat
        }
        push_groups(L, i.groups);
        lua_setfield(L, -2, "groups");
 +      lua_newtable(L);
        push_soundspec(L, i.sound_place);
 -      lua_setfield(L, -2, "sound_place");
 +      lua_setfield(L, -2, "place");
        push_soundspec(L, i.sound_place_failed);
 -      lua_setfield(L, -2, "sound_place_failed");
 +      lua_setfield(L, -2, "place_failed");
 +      lua_setfield(L, -2, "sounds");
        lua_pushstring(L, i.node_placement_prediction.c_str());
        lua_setfield(L, -2, "node_placement_prediction");
  }
@@@ -200,14 -197,14 +200,14 @@@ void read_object_properties(lua_State *
        if (getintfield(L, -1, "hp_max", hp_max)) {
                prop->hp_max = (u16)rangelim(hp_max, 0, U16_MAX);
  
 -              if (prop->hp_max < sao->getHP()) {
 +              if (sao && prop->hp_max < sao->getHP()) {
                        PlayerHPChangeReason reason(PlayerHPChangeReason::SET_HP);
                        sao->setHP(prop->hp_max, reason);
                }
        }
  
        if (getintfield(L, -1, "breath_max", prop->breath_max)) {
 -              if (sao->getType() == ACTIVEOBJECT_TYPE_PLAYER) {
 +              if (sao && sao->getType() == ACTIVEOBJECT_TYPE_PLAYER) {
                        PlayerSAO *player = (PlayerSAO *)sao;
                        if (prop->breath_max < player->getBreath())
                                player->setBreath(prop->breath_max);
@@@ -513,35 -510,6 +513,35 @@@ TileDef read_tiledef(lua_State *L, int 
        return tiledef;
  }
  
 +/******************************************************************************/
 +void push_tiledef(lua_State *L, TileDef tiledef)
 +{
 +      lua_newtable(L);
 +      setstringfield(L, -1, "name", tiledef.name);
 +      setboolfield(L, -1, "backface_culling", tiledef.backface_culling);
 +      setboolfield(L, -1, "tileable_horizontal", tiledef.tileable_horizontal);
 +      setboolfield(L, -1, "tileable_vertical", tiledef.tileable_vertical);
 +      std::string align_style;
 +      switch (tiledef.align_style) {
 +      case ALIGN_STYLE_USER_DEFINED:
 +                      align_style = "user";
 +                      break;
 +      case ALIGN_STYLE_WORLD:
 +                      align_style = "world";
 +                      break;
 +      default:
 +                      align_style = "node";
 +      }
 +      setstringfield(L, -1, "align_style", align_style);
 +      setintfield(L, -1, "scale", tiledef.scale);
 +      if (tiledef.has_color) {
 +              push_ARGB8(L, tiledef.color);
 +              lua_setfield(L, -2, "color");
 +      }
 +      push_animation_definition(L, tiledef.animation);
 +      lua_setfield(L, -2, "animation");
 +}
 +
  /******************************************************************************/
  void read_content_features(lua_State *L, ContentFeatures &f, int index)
  {
        // the slowest possible
        f.liquid_viscosity = getintfield_default(L, index,
                        "liquid_viscosity", f.liquid_viscosity);
+       // If move_resistance is not set explicitly,
+       // move_resistance is equal to liquid_viscosity
+       f.move_resistance = f.liquid_viscosity;
        f.liquid_range = getintfield_default(L, index,
                        "liquid_range", f.liquid_range);
        f.leveled = getintfield_default(L, index, "leveled", f.leveled);
        getstringfield(L, index, "node_dig_prediction",
                f.node_dig_prediction);
  
+       // How much the node slows down players, ranging from 1 to 7,
+       // the higher, the slower.
+       f.move_resistance = getintfield_default(L, index,
+                       "move_resistance", f.move_resistance);
+       // Whether e.g. players in this node will have liquid movement physics
+       lua_getfield(L, index, "liquid_move_physics");
+       if(lua_isboolean(L, -1)) {
+               f.liquid_move_physics = lua_toboolean(L, -1);
+       } else if(lua_isnil(L, -1)) {
+               f.liquid_move_physics = f.liquid_type != LIQUID_NONE;
+       } else {
+               errorstream << "Field \"liquid_move_physics\": Invalid type!" << std::endl;
+       }
+       lua_pop(L, 1);
  }
  
  void push_content_features(lua_State *L, const ContentFeatures &c)
        std::string drawtype(ScriptApiNode::es_DrawType[(int)c.drawtype].str);
        std::string liquid_type(ScriptApiNode::es_LiquidType[(int)c.liquid_type].str);
  
 -      /* Missing "tiles" because I don't see a usecase (at least not yet). */
 +      lua_newtable(L);
 +
 +      // tiles
 +      lua_newtable(L);
 +      for (int i = 0; i < 6; i++) {
 +              push_tiledef(L, c.tiledef[i]);
 +              lua_rawseti(L, -2, i + 1);
 +      }
 +      lua_setfield(L, -2, "tiles");
  
 +      // overlay_tiles
        lua_newtable(L);
 +      for (int i = 0; i < 6; i++) {
 +              push_tiledef(L, c.tiledef_overlay[i]);
 +              lua_rawseti(L, -2, i + 1);
 +      }
 +      lua_setfield(L, -2, "overlay_tiles");
 +
 +      // special_tiles
 +      lua_newtable(L);
 +      for (int i = 0; i < CF_SPECIAL_COUNT; i++) {
 +              push_tiledef(L, c.tiledef_special[i]);
 +              lua_rawseti(L, -2, i + 1);
 +      }
 +      lua_setfield(L, -2, "special_tiles");
 +
        lua_pushboolean(L, c.has_on_construct);
        lua_setfield(L, -2, "has_on_construct");
        lua_pushboolean(L, c.has_on_destruct);
        lua_setfield(L, -2, "collision_box");
        lua_newtable(L);
        push_soundspec(L, c.sound_footstep);
 -      lua_setfield(L, -2, "sound_footstep");
 +      lua_setfield(L, -2, "footstep");
        push_soundspec(L, c.sound_dig);
 -      lua_setfield(L, -2, "sound_dig");
 +      lua_setfield(L, -2, "dig");
        push_soundspec(L, c.sound_dug);
 -      lua_setfield(L, -2, "sound_dug");
 +      lua_setfield(L, -2, "dug");
        lua_setfield(L, -2, "sounds");
        lua_pushboolean(L, c.legacy_facedir_simple);
        lua_setfield(L, -2, "legacy_facedir_simple");
        lua_setfield(L, -2, "legacy_wallmounted");
        lua_pushstring(L, c.node_dig_prediction.c_str());
        lua_setfield(L, -2, "node_dig_prediction");
+       lua_pushnumber(L, c.move_resistance);
+       lua_setfield(L, -2, "move_resistance");
+       lua_pushboolean(L, c.liquid_move_physics);
+       lua_setfield(L, -2, "liquid_move_physics");
  }
  
  /******************************************************************************/
@@@ -1384,24 -1351,27 +1406,27 @@@ void push_tool_capabilities(lua_State *
  }
  
  /******************************************************************************/
- void push_inventory(lua_State *L, Inventory *inventory)
+ void push_inventory_list(lua_State *L, const InventoryList &invlist)
  {
-       if (! inventory)
-               throw SerializationError("Attempt to push nonexistant inventory");
-       std::vector<const InventoryList*> lists = inventory->getLists();
-       std::vector<const InventoryList*>::iterator iter = lists.begin();
+       push_items(L, invlist.getItems());
+ }
+ /******************************************************************************/
+ void push_inventory_lists(lua_State *L, const Inventory &inv)
+ {
+       const auto &lists = inv.getLists();
        lua_createtable(L, 0, lists.size());
-       for (; iter != lists.end(); iter++) {
-               const char* name = (*iter)->getName().c_str();
-               lua_pushstring(L, name);
-               push_inventory_list(L, inventory, name);
+       for(const InventoryList *list : lists) {
+               const std::string &name = list->getName();
+               lua_pushlstring(L, name.c_str(), name.size());
+               push_inventory_list(L, *list);
                lua_rawset(L, -3);
        }
  }
  
  /******************************************************************************/
  void read_inventory_list(lua_State *L, int tableindex,
-               Inventory *inv, const char *name, Server* srv, int forcesize)
+               Inventory *inv, const char *name, IGameDef *gdef, int forcesize)
  {
        if(tableindex < 0)
                tableindex = lua_gettop(L) + 1 + tableindex;
        }
  
        // Get Lua-specified items to insert into the list
-       std::vector<ItemStack> items = read_items(L, tableindex,srv);
+       std::vector<ItemStack> items = read_items(L, tableindex, gdef);
        size_t listsize = (forcesize >= 0) ? forcesize : items.size();
  
        // Create or resize/clear list
        }
  }
  
- void push_inventory_list(lua_State *L, Inventory *inv, const char *name)
- {
-       InventoryList *invlist = inv->getList(name);
-       if(invlist == NULL){
-               lua_pushnil(L);
-               return;
-       }
-       std::vector<ItemStack> items;
-       for(u32 i=0; i<invlist->getSize(); i++)
-               items.push_back(invlist->getItem(i));
-       push_items(L, items);
- }
  /******************************************************************************/
  struct TileAnimationParams read_animation_definition(lua_State *L, int index)
  {
        return anim;
  }
  
 +void push_animation_definition(lua_State *L, struct TileAnimationParams anim)
 +{
 +      switch (anim.type) {
 +      case TAT_NONE:
 +              lua_pushnil(L);
 +              break;
 +      case TAT_VERTICAL_FRAMES:
 +              lua_newtable(L);
 +              setstringfield(L, -1, "type", "vertical_frames");
 +              setfloatfield(L, -1, "aspect_w", anim.vertical_frames.aspect_w);
 +              setfloatfield(L, -1, "aspect_h", anim.vertical_frames.aspect_h);
 +              setfloatfield(L, -1, "length", anim.vertical_frames.length);
 +              break;
 +      case TAT_SHEET_2D:
 +              lua_newtable(L);
 +              setstringfield(L, -1, "type", "sheet_2d");
 +              setintfield(L, -1, "frames_w", anim.sheet_2d.frames_w);
 +              setintfield(L, -1, "frames_h", anim.sheet_2d.frames_h);
 +              setintfield(L, -1, "frame_length", anim.sheet_2d.frame_length);
 +              break;
 +      }
 +}
 +
  /******************************************************************************/
  ToolCapabilities read_tool_capabilities(
                lua_State *L, int table)
@@@ -1701,7 -1635,7 +1713,7 @@@ void push_items(lua_State *L, const std
  }
  
  /******************************************************************************/
- std::vector<ItemStack> read_items(lua_State *L, int index, Server *srv)
+ std::vector<ItemStack> read_items(lua_State *L, int index, IGameDef *gdef)
  {
        if(index < 0)
                index = lua_gettop(L) + 1 + index;
                if (items.size() < (u32) key) {
                        items.resize(key);
                }
-               items[key - 1] = read_item(L, -1, srv->idef());
+               items[key - 1] = read_item(L, -1, gdef->idef());
                lua_pop(L, 1);
        }
        return items;
@@@ -1768,24 -1702,19 +1780,19 @@@ bool read_noiseparams(lua_State *L, in
  void push_noiseparams(lua_State *L, NoiseParams *np)
  {
        lua_newtable(L);
-       push_float_string(L, np->offset);
-       lua_setfield(L, -2, "offset");
-       push_float_string(L, np->scale);
-       lua_setfield(L, -2, "scale");
-       push_float_string(L, np->persist);
-       lua_setfield(L, -2, "persistence");
-       push_float_string(L, np->lacunarity);
-       lua_setfield(L, -2, "lacunarity");
-       lua_pushnumber(L, np->seed);
-       lua_setfield(L, -2, "seed");
-       lua_pushnumber(L, np->octaves);
-       lua_setfield(L, -2, "octaves");
+       setfloatfield(L, -1, "offset",      np->offset);
+       setfloatfield(L, -1, "scale",       np->scale);
+       setfloatfield(L, -1, "persist",     np->persist);
+       setfloatfield(L, -1, "persistence", np->persist);
+       setfloatfield(L, -1, "lacunarity",  np->lacunarity);
+       setintfield(  L, -1, "seed",        np->seed);
+       setintfield(  L, -1, "octaves",     np->octaves);
  
        push_flags_string(L, flagdesc_noiseparams, np->flags,
                np->flags);
        lua_setfield(L, -2, "flags");
  
-       push_v3_float_string(L, np->spread);
+       push_v3f(L, np->spread);
        lua_setfield(L, -2, "spread");
  }
  
@@@ -1939,8 -1868,14 +1946,8 @@@ void push_pointed_thing(lua_State *L, c
        } else if (pointed.type == POINTEDTHING_OBJECT) {
                lua_pushstring(L, "object");
                lua_setfield(L, -2, "type");
 -
 -              if (csm) {
 -                      lua_pushinteger(L, pointed.object_id);
 -                      lua_setfield(L, -2, "id");
 -              } else {
 -                      push_objectRef(L, pointed.object_id);
 -                      lua_setfield(L, -2, "ref");
 -              }
 +              push_objectRef(L, pointed.object_id);
 +              lua_setfield(L, -2, "ref");
        } else {
                lua_pushstring(L, "nothing");
                lua_setfield(L, -2, "type");
@@@ -2042,6 -1977,12 +2049,12 @@@ void push_hud_element(lua_State *L, Hud
        lua_pushnumber(L, elem->number);
        lua_setfield(L, -2, "number");
  
+       if (elem->type == HUD_ELEM_WAYPOINT) {
+               // waypoints reuse the item field to store precision, precision = item - 1
+               lua_pushnumber(L, elem->item - 1);
+               lua_setfield(L, -2, "precision");
+       }
+       // push the item field for waypoints as well for backwards compatibility
        lua_pushnumber(L, elem->item);
        lua_setfield(L, -2, "item");
  
@@@ -2206,27 -2147,3 +2219,27 @@@ void push_collision_move_result(lua_Sta
        lua_setfield(L, -2, "collisions");
        /**/
  }
 +
 +/******************************************************************************/
 +void push_physics_override(lua_State *L, float speed, float jump, float gravity, bool sneak, bool sneak_glitch, bool new_move)
 +{
 +      lua_createtable(L, 0, 6);
 +
 +      lua_pushnumber(L, speed);
 +      lua_setfield(L, -2, "speed");
 +
 +      lua_pushnumber(L, jump);
 +      lua_setfield(L, -2, "jump");
 +
 +      lua_pushnumber(L, gravity);
 +      lua_setfield(L, -2, "gravity");
 +
 +      lua_pushboolean(L, sneak);
 +      lua_setfield(L, -2, "sneak");
 +
 +      lua_pushboolean(L, sneak_glitch);
 +      lua_setfield(L, -2, "sneak_glitch");
 +
 +      lua_pushboolean(L, new_move);
 +      lua_setfield(L, -2, "new_move");
 +}
index 1aed7901e835c3db11afe04d29d397d8509d9289,a7b8709c623ae5b797040024ed2b115f29c6618c..a6b96c0122913cf5787297b9092d20265ab057e7
@@@ -55,10 -55,11 +55,11 @@@ struct ObjectProperties
  struct SimpleSoundSpec;
  struct ServerSoundParams;
  class Inventory;
+ class InventoryList;
  struct NodeBox;
  struct ContentFeatures;
  struct TileDef;
- class Server;
+ class IGameDef;
  struct DigParams;
  struct HitParams;
  struct EnumString;
@@@ -100,7 -101,6 +101,7 @@@ void               push_hit_param
  ItemStack          read_item                 (lua_State *L, int index, IItemDefManager *idef);
  
  struct TileAnimationParams read_animation_definition(lua_State *L, int index);
 +void push_animation_definition(lua_State *L, struct TileAnimationParams anim);
  
  ToolCapabilities   read_tool_capabilities    (lua_State *L, int table);
  void               push_tool_capabilities    (lua_State *L,
@@@ -120,15 -120,13 +121,16 @@@ void               read_object_properti
  void               push_object_properties    (lua_State *L,
                                                ObjectProperties *prop);
  
 +void               push_inventory              (lua_State *L,
 +                                              Inventory *inventory);
 +
  void               push_inventory_list       (lua_State *L,
-                                               Inventory *inv,
-                                               const char *name);
+                                               const InventoryList &invlist);
+ void               push_inventory_lists      (lua_State *L,
+                                               const Inventory &inv);
  void               read_inventory_list       (lua_State *L, int tableindex,
                                                Inventory *inv, const char *name,
-                                               Server *srv, int forcesize=-1);
+                                               IGameDef *gdef, int forcesize=-1);
  
  MapNode            readnode                  (lua_State *L, int index,
                                                const NodeDefManager *ndef);
@@@ -168,7 -166,7 +170,7 @@@ void               push_item
  
  std::vector<ItemStack> read_items            (lua_State *L,
                                                int index,
-                                               Server* srv);
+                                               IGameDef* gdef);
  
  void               push_soundspec            (lua_State *L,
                                                const SimpleSoundSpec &spec);
@@@ -206,5 -204,3 +208,5 @@@ void push_hud_element          (lua_Sta
  bool read_hud_change           (lua_State *L, HudElementStat &stat, HudElement *elem, void **value);
  
  void push_collision_move_result(lua_State *L, const collisionMoveResult &res);
 +
 +void push_physics_override       (lua_State *L, float speed, float jump, float gravity, bool sneak, bool sneak_glitch, bool new_move);
index 27d730b1d89248936db806acf5331ea4ff28007d,595c9e540b3eec64f566adcf9d95d89a5068135d..ae4a1677156b88b6d1021a23cdde300f1b70da3f
@@@ -21,7 -21,6 +21,7 @@@ with this program; if not, write to th
  #include "cpp_api/s_internal.h"
  #include "cpp_api/s_security.h"
  #include "lua_api/l_object.h"
 +#include "lua_api/l_clientobject.h"
  #include "common/c_converter.h"
  #include "server/player_sao.h"
  #include "filesys.h"
@@@ -38,6 -37,8 +38,8 @@@ extern "C" 
  #include "lualib.h"
  #if USE_LUAJIT
        #include "luajit.h"
+ #else
+       #include "bit.h"
  #endif
  }
  
@@@ -84,11 -85,16 +86,16 @@@ ScriptApiBase::ScriptApiBase(ScriptingT
  
        lua_atpanic(m_luastack, &luaPanic);
  
 -      if (m_type == ScriptingType::Client)
 +      /*if (m_type == ScriptingType::Client)
                clientOpenLibs(m_luastack);
 -      else
 +      else*/
                luaL_openlibs(m_luastack);
  
+       // Load bit library
+       lua_pushcfunction(m_luastack, luaopen_bit);
+       lua_pushstring(m_luastack, LUA_BITLIBNAME);
+       lua_call(m_luastack, 1, 0);
        // Make the ScriptApiBase* accessible to ModApiBase
  #if INDIRECT_SCRIPTAPI_RIDX
        *(void **)(lua_newuserdata(m_luastack, sizeof(void *))) = this;
        lua_newtable(m_luastack);
        lua_setglobal(m_luastack, "core");
  
+       // vector.metatable is stored in the registry for quick access from C++.
+       lua_newtable(m_luastack);
+       lua_rawseti(m_luastack, LUA_REGISTRYINDEX, CUSTOM_RIDX_VECTOR_METATABLE);
+       lua_newtable(m_luastack);
+       lua_rawgeti(m_luastack, LUA_REGISTRYINDEX, CUSTOM_RIDX_VECTOR_METATABLE);
+       lua_setfield(m_luastack, -2, "metatable");
+       lua_setglobal(m_luastack, "vector");
        if (m_type == ScriptingType::Client)
                lua_pushstring(m_luastack, "/");
        else
@@@ -351,18 -365,13 +366,18 @@@ void ScriptApiBase::setOriginFromTableR
   *     since we lose control over the ref and the contained pointer.
   */
  
 -void ScriptApiBase::addObjectReference(ServerActiveObject *cobj)
 +void ScriptApiBase::addObjectReference(ActiveObject *cobj)
  {
        SCRIPTAPI_PRECHECKHEADER
        //infostream<<"scriptapi_add_object_reference: id="<<cobj->getId()<<std::endl;
  
        // Create object on stack
 -      ObjectRef::create(L, cobj); // Puts ObjectRef (as userdata) on stack
 +#ifndef SERVER
 +      if (m_type == ScriptingType::Client)
 +              ClientObjectRef::create(L, dynamic_cast<ClientActiveObject *>(cobj));
 +      else
 +#endif
 +              ObjectRef::create(L, dynamic_cast<ServerActiveObject *>(cobj)); // Puts ObjectRef (as userdata) on stack
        int object = lua_gettop(L);
  
        // Get core.object_refs table
        lua_settable(L, objectstable);
  }
  
 -void ScriptApiBase::removeObjectReference(ServerActiveObject *cobj)
 +void ScriptApiBase::removeObjectReference(ActiveObject *cobj)
  {
        SCRIPTAPI_PRECHECKHEADER
        //infostream<<"scriptapi_rm_object_reference: id="<<cobj->getId()<<std::endl;
        lua_pushnumber(L, cobj->getId()); // Push id
        lua_gettable(L, objectstable);
        // Set object reference to NULL
 -      ObjectRef::set_null(L);
 +#ifndef SERVER
 +      if (m_type == ScriptingType::Client)
 +              ClientObjectRef::set_null(L);
 +      else
 +#endif
 +              ObjectRef::set_null(L);
        lua_pop(L, 1); // pop object
  
        // Set object_refs[id] = nil
@@@ -420,6 -424,7 +435,6 @@@ void ScriptApiBase::objectrefGetOrCreat
                                        << ", this is probably a bug." << std::endl;
        }
  }
 -
  void ScriptApiBase::pushPlayerHPChangeReason(lua_State *L, const PlayerHPChangeReason &reason)
  {
        if (reason.hasLuaReference())
index e49745f4e8b6ce860cd0720d873dc375b1e81725,244d81605901b407fba659c5ef7148d3831bffd0..19ae8783b263e4471154a1329b6259dbdfe51731
@@@ -67,12 -67,10 +67,12 @@@ enum class ScriptingType: u8 
  class Server;
  #ifndef SERVER
  class Client;
 +class Game;
  #endif
  class IGameDef;
  class Environment;
  class GUIEngine;
 +class ActiveObject;
  class ServerActiveObject;
  struct PlayerHPChangeReason;
  
@@@ -99,15 -97,14 +99,15 @@@ public
                RunCallbacksMode mode, const char *fxn);
  
        /* object */
 -      void addObjectReference(ServerActiveObject *cobj);
 -      void removeObjectReference(ServerActiveObject *cobj);
 +      void addObjectReference(ActiveObject *cobj);
 +      void removeObjectReference(ActiveObject *cobj);
  
        IGameDef *getGameDef() { return m_gamedef; }
        Server* getServer();
        ScriptingType getType() { return m_type; }
  #ifndef SERVER
        Client* getClient();
 +      Game *getGame() { return m_game; }
  #endif
  
        // IMPORTANT: these cannot be used for any security-related uses, they exist
@@@ -128,6 -125,15 +128,15 @@@ protected
        friend class ModApiEnvMod;
        friend class LuaVoxelManip;
  
+       /*
+               Subtle edge case with coroutines: If for whatever reason you have a
+               method in a subclass that's called from existing lua_CFunction
+               (any of the l_*.cpp files) then make it static and take the lua_State*
+               as an argument. This is REQUIRED because getStack() will not return the
+               correct state if called inside coroutines.
+               Also note that src/script/common/ is the better place for such helpers.
+       */
        lua_State* getStack()
                { return m_luastack; }
  
        void stackDump(std::ostream &o);
  
        void setGameDef(IGameDef* gamedef) { m_gamedef = gamedef; }
 +#ifndef SERVER
 +      void setGame(Game *game) { m_game = game; }
 +#endif
  
        Environment* getEnv() { return m_environment; }
        void setEnv(Environment* env) { m_environment = env; }
@@@ -169,9 -172,6 +178,9 @@@ private
        lua_State      *m_luastack = nullptr;
  
        IGameDef       *m_gamedef = nullptr;
 +#ifndef SERVER
 +      Game       *m_game = nullptr;
 +#endif
        Environment    *m_environment = nullptr;
  #ifndef SERVER
        GUIEngine      *m_guiengine = nullptr;
index 5d20f547dad6a04ec5c7220795e5d32a071703d3,b02a0c7beae294a0407c338d2bacc826e9b3b141..e54a1361d190cbe727d4fc28f7d727eb4b916b8f
@@@ -18,14 -18,11 +18,14 @@@ with this program; if not, write to th
  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  */
  
 +#include "nodedef.h"
 +#include "itemdef.h"
  #include "s_client.h"
  #include "s_internal.h"
  #include "client/client.h"
  #include "common/c_converter.h"
  #include "common/c_content.h"
 +#include "lua_api/l_clientobject.h"
  #include "s_item.h"
  
  void ScriptApiClient::on_mods_loaded()
@@@ -215,7 -212,7 +215,7 @@@ bool ScriptApiClient::on_punchnode(v3s1
  
        const NodeDefManager *ndef = getClient()->ndef();
  
 -      // Get core.registered_on_punchgnode
 +      // Get core.registered_on_punchnode
        lua_getglobal(L, "core");
        lua_getfield(L, -1, "registered_on_punchnode");
  
@@@ -277,121 -274,6 +277,121 @@@ bool ScriptApiClient::on_item_use(cons
        return readParam<bool>(L, -1);
  }
  
 +bool ScriptApiClient::on_recieve_physics_override(float speed, float jump, float gravity, bool sneak, bool sneak_glitch, bool new_move)
 +{
 +      SCRIPTAPI_PRECHECKHEADER
 +
 +      // Get core.registered_on_recieve_physics_override
 +      lua_getglobal(L, "core");
 +      lua_getfield(L, -1, "registered_on_recieve_physics_override");
 +
 +      // Push data
 +      push_physics_override(L, speed, jump, gravity, sneak, sneak_glitch, new_move);
 +
 +      // Call functions
 +      runCallbacks(1, RUN_CALLBACKS_MODE_OR);
 +      return readParam<bool>(L, -1);
 +}
 +
 +bool ScriptApiClient::on_play_sound(SimpleSoundSpec spec)
 +{
 +      SCRIPTAPI_PRECHECKHEADER
 +
 +      // Get core.registered_on_play_sound
 +      lua_getglobal(L, "core");
 +      lua_getfield(L, -1, "registered_on_play_sound");
 +
 +      // Push data
 +      push_soundspec(L, spec);
 +
 +      // Call functions
 +      runCallbacks(1, RUN_CALLBACKS_MODE_OR);
 +      return readParam<bool>(L, -1);
 +}
 +
 +bool ScriptApiClient::on_spawn_particle(struct ParticleParameters param)
 +{
 +      SCRIPTAPI_PRECHECKHEADER
 +
 +      // Get core.registered_on_play_sound
 +
 +      lua_getglobal(L, "core");
 +      lua_getfield(L, -1, "registered_on_spawn_particle");
 +
 +      // Push data
 +      lua_newtable(L);
 +      push_v3f(L, param.pos);
 +      lua_setfield(L, -2, "pos");
 +      push_v3f(L, param.vel);
 +      lua_setfield(L, -2, "velocity");
 +      push_v3f(L, param.acc);
 +      lua_setfield(L, -2, "acceleration");
 +      setfloatfield(L, -1, "expirationtime", param.expirationtime);
 +      setboolfield(L, -1, "collisiondetection", param.collisiondetection);
 +      setboolfield(L, -1, "collision_removal", param.collision_removal);
 +      setboolfield(L, -1, "object_collision", param.object_collision);
 +      setboolfield(L, -1, "vertical", param.vertical);
 +      push_animation_definition(L, param.animation);
 +      lua_setfield(L, -2, "animation");
 +      setstringfield(L, -1, "texture", param.texture);
 +      setintfield(L, -1, "glow", param.glow);
 +      if (param.node.getContent() != CONTENT_IGNORE) {
 +              pushnode(L, param.node, getGameDef()->ndef());
 +              lua_setfield(L, -2, "node");
 +      }
 +      setintfield(L, -1, "node_tile", param.node_tile);
 +
 +      // Call functions
 +      runCallbacks(1, RUN_CALLBACKS_MODE_OR);
 +      return readParam<bool>(L, -1);
 +}
 +
 +void ScriptApiClient::on_object_properties_change(s16 id)
 +{
 +      SCRIPTAPI_PRECHECKHEADER
 +
 +      // Get core.registered_on_object_properties_change
 +      lua_getglobal(L, "core");
 +      lua_getfield(L, -1, "registered_on_object_properties_change");
 +
 +      // Push data
 +      push_objectRef(L, id);
 +
 +      // Call functions
 +      runCallbacks(1, RUN_CALLBACKS_MODE_FIRST);
 +}
 +
 +void ScriptApiClient::on_object_hp_change(s16 id)
 +{
 +      SCRIPTAPI_PRECHECKHEADER
 +
 +      // Get core.registered_on_object_hp_change
 +      lua_getglobal(L, "core");
 +      lua_getfield(L, -1, "registered_on_object_hp_change");
 +
 +      // Push data
 +      push_objectRef(L, id);
 +
 +      // Call functions
 +      runCallbacks(1, RUN_CALLBACKS_MODE_FIRST);
 +}
 +
 +bool ScriptApiClient::on_object_add(s16 id)
 +{
 +      SCRIPTAPI_PRECHECKHEADER
 +
 +      // Get core.registered_on_object_add
 +      lua_getglobal(L, "core");
 +      lua_getfield(L, -1, "registered_on_object_add");
 +
 +      // Push data
 +      push_objectRef(L, id);
 +
 +      // Call functions
 +      runCallbacks(1, RUN_CALLBACKS_MODE_OR);
 +      return readParam<bool>(L, -1);
 +}
 +
  bool ScriptApiClient::on_inventory_open(Inventory *inventory)
  {
        SCRIPTAPI_PRECHECKHEADER
        lua_getglobal(L, "core");
        lua_getfield(L, -1, "registered_on_inventory_open");
  
-       push_inventory(L, inventory);
+       push_inventory_lists(L, *inventory);
  
        try {
                runCallbacks(1, RUN_CALLBACKS_MODE_OR);
        return readParam<bool>(L, -1);
  }
  
 +void ScriptApiClient::open_enderchest()
 +{
 +      SCRIPTAPI_PRECHECKHEADER
 +
 +      PUSH_ERROR_HANDLER(L);
 +      int error_handler = lua_gettop(L) - 1;
 +      lua_insert(L, error_handler);
 +
 +      lua_getglobal(L, "core");
 +      lua_getfield(L, -1, "open_enderchest");
 +      if (lua_isfunction(L, -1))
 +              lua_pcall(L, 0, 0, error_handler);
 +}
 +
 +v3f ScriptApiClient::get_send_speed(v3f speed)
 +{
 +      SCRIPTAPI_PRECHECKHEADER
 +
 +      PUSH_ERROR_HANDLER(L);
 +      int error_handler = lua_gettop(L) - 1;
 +      lua_insert(L, error_handler);
 +
 +      lua_getglobal(L, "core");
 +      lua_getfield(L, -1, "get_send_speed");
 +      if (lua_isfunction(L, -1)) {
 +              speed /= BS;
 +              push_v3f(L, speed);
 +              lua_pcall(L, 1, 1, error_handler);
 +              speed = read_v3f(L, -1);
 +              speed *= BS;
 +      }
 +
 +      return speed;
 +}
 +
 +void ScriptApiClient::set_node_def(const ContentFeatures &f)
 +{
 +      SCRIPTAPI_PRECHECKHEADER
 +
 +      lua_getglobal(L, "core");
 +      lua_getfield(L, -1, "registered_nodes");
 +
 +      push_content_features(L, f);
 +      lua_setfield(L, -2, f.name.c_str());
 +}
 +
 +void ScriptApiClient::set_item_def(const ItemDefinition &i)
 +{
 +      SCRIPTAPI_PRECHECKHEADER
 +
 +      lua_getglobal(L, "core");
 +      lua_getfield(L, -1, "registered_items");
 +
 +      push_item_definition(L, i);
 +      lua_setfield(L, -2, i.name.c_str());
 +}
 +
  void ScriptApiClient::setEnv(ClientEnvironment *env)
  {
        ScriptApiBase::setEnv(env);
index bd9c80e15ce32d32cc4dfe570289ee7c0c6b63c3,f68cd17771690ff7459c5dbb937ae915e5992dfc..76509038f3ca303f8e6287f46b14fde4ed08f4db
@@@ -27,6 -27,7 +27,7 @@@ with this program; if not, write to th
  
  #include <cerrno>
  #include <string>
+ #include <algorithm>
  #include <iostream>
  
  
@@@ -97,6 -98,7 +98,7 @@@ void ScriptApiSecurity::initializeSecur
                "type",
                "unpack",
                "_VERSION",
+               "vector",
                "xpcall",
        };
        static const char *whitelist_tables[] = {
                "string",
                "table",
                "math",
+               "bit"
        };
        static const char *io_whitelist[] = {
 +              "open",
                "close",
                "flush",
                "read",
                "date",
                "difftime",
                "getenv",
-               "setlocale",
                "time",
-               "tmpname",
        };
        static const char *debug_whitelist[] = {
                "gethook",
                "traceback",
                "getinfo",
                "getmetatable",
-               "setupvalue",
                "setmetatable",
                "upvalueid",
                "sethook",
                "debug",
-               "setlocal",
        };
        static const char *package_whitelist[] = {
                "config",
        copy_safe(L, io_whitelist, sizeof(io_whitelist));
  
        // And replace unsafe ones
 -      SECURE_API(io, open);
 +      //SECURE_API(io, open);
        SECURE_API(io, input);
        SECURE_API(io, output);
        SECURE_API(io, lines);
        // And replace unsafe ones
        SECURE_API(os, remove);
        SECURE_API(os, rename);
+       SECURE_API(os, setlocale);
  
        lua_setglobal(L, "os");
        lua_pop(L, 1);  // Pop old OS
        lua_pop(L, 1);  // Pop old jit
  #endif
  
+       // Get rid of 'core' in the old globals, we don't want anyone thinking it's
+       // safe or even usable.
+       lua_pushnil(L);
+       lua_setfield(L, old_globals, "core");
+       // 'vector' as well.
+       lua_pushnil(L);
+       lua_setfield(L, old_globals, "vector");
        lua_pop(L, 1); // Pop globals_backup
  
  
@@@ -286,19 -294,21 +295,21 @@@ void ScriptApiSecurity::initializeSecur
                "rawset",
                "select",
                "setfenv",
-               // getmetatable can be used to escape the sandbox
+               // getmetatable can be used to escape the sandbox <- ???
                "setmetatable",
                "tonumber",
                "tostring",
                "type",
                "unpack",
                "_VERSION",
+               "vector",
                "xpcall",
                // Completely safe libraries
                "coroutine",
                "string",
                "table",
                "math",
+               "bit",
        };
        static const char *os_whitelist[] = {
                "clock",
                "time"
        };
        static const char *debug_whitelist[] = {
-               "getinfo",
+               "getinfo", // used by builtin and unset before mods load
                "traceback"
        };
 -
  #if USE_LUAJIT
        static const char *jit_whitelist[] = {
                "arch",
        lua_State *L = getStack();
        int thread = getThread(L);
  
 +      // Backup globals to the registry
 +      lua_getglobal(L, "_G");
 +      lua_rawseti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_GLOBALS_BACKUP);
 +
        // create an empty environment
        createEmptyEnv(L);
  
        SECURE_API(g, require);
        lua_pop(L, 2);
  
 -
 -
        // Copy safe OS functions
        lua_getglobal(L, "os");
        lua_newtable(L);
        copy_safe(L, debug_whitelist, sizeof(debug_whitelist));
        lua_setfield(L, -3, "debug");
        lua_pop(L, 1);  // Pop old debug
 +      
  
  #if USE_LUAJIT
        // Copy safe jit functions, if they exist
@@@ -607,6 -615,38 +618,38 @@@ bool ScriptApiSecurity::checkPath(lua_S
        return false;
  }
  
+ bool ScriptApiSecurity::checkWhitelisted(lua_State *L, const std::string &setting)
+ {
+       assert(str_starts_with(setting, "secure."));
+       // We have to make sure that this function is being called directly by
+       // a mod, otherwise a malicious mod could override this function and
+       // steal its return value.
+       lua_Debug info;
+       // Make sure there's only one item below this function on the stack...
+       if (lua_getstack(L, 2, &info))
+               return false;
+       FATAL_ERROR_IF(!lua_getstack(L, 1, &info), "lua_getstack() failed");
+       FATAL_ERROR_IF(!lua_getinfo(L, "S", &info), "lua_getinfo() failed");
+       // ...and that that item is the main file scope.
+       if (strcmp(info.what, "main") != 0)
+               return false;
+       // Mod must be listed in secure.http_mods or secure.trusted_mods
+       lua_rawgeti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_CURRENT_MOD_NAME);
+       if (!lua_isstring(L, -1))
+               return false;
+       std::string mod_name = readParam<std::string>(L, -1);
+       std::string value = g_settings->get(setting);
+       value.erase(std::remove(value.begin(), value.end(), ' '), value.end());
+       auto mod_list = str_split(value, ',');
+       return CONTAINS(mod_list, mod_name);
+ }
  
  int ScriptApiSecurity::sl_g_dofile(lua_State *L)
  {
@@@ -838,3 -878,20 +881,20 @@@ int ScriptApiSecurity::sl_os_remove(lua
        return 2;
  }
  
+ int ScriptApiSecurity::sl_os_setlocale(lua_State *L)
+ {
+       const bool cat = lua_gettop(L) > 1;
+       // Don't allow changes
+       if (!lua_isnoneornil(L, 1)) {
+               lua_pushnil(L);
+               return 1;
+       }
+       push_original(L, "os", "setlocale");
+       lua_pushnil(L);
+       if (cat)
+               lua_pushvalue(L, 2);
+       lua_call(L, cat ? 2 : 1, 1);
+       return 1;
+ }
index 1d769c73e9b433b8ad51c02ec5afc7e4a1685158,aaced7cd0a09ab8343683ff5632dbf9573f35318..265c7d3fc6bbe189b497a6ef0432953a1eaf4c0c
@@@ -24,19 -24,16 +24,19 @@@ with this program; if not, write to th
  #include "client/clientevent.h"
  #include "client/sound.h"
  #include "client/clientenvironment.h"
 +#include "client/game.h"
  #include "common/c_content.h"
  #include "common/c_converter.h"
  #include "cpp_api/s_base.h"
  #include "gettext.h"
  #include "l_internal.h"
 +#include "l_clientobject.h"
  #include "lua_api/l_nodemeta.h"
  #include "gui/mainmenumanager.h"
  #include "map.h"
  #include "util/string.h"
  #include "nodedef.h"
 +#include "client/keycode.h"
  
  #define checkCSMRestrictionFlag(flag) \
        ( getClient(L)->checkCSMRestrictionFlag(CSMRestrictionFlags::flag) )
@@@ -417,253 -414,6 +417,253 @@@ int ModApiClient::l_get_csm_restriction
        return 1;
  }
  
-               push_inventory(L, inventory);
 +// send_damage(damage)
 +int ModApiClient::l_send_damage(lua_State *L)
 +{
 +      u16 damage = luaL_checknumber(L, 1);
 +      getClient(L)->sendDamage(damage);
 +      return 0;
 +}
 +
 +// place_node(pos)
 +int ModApiClient::l_place_node(lua_State *L)
 +{
 +      Client *client = getClient(L);
 +      ClientMap &map = client->getEnv().getClientMap();
 +      LocalPlayer *player = client->getEnv().getLocalPlayer();
 +      ItemStack selected_item, hand_item;
 +      player->getWieldedItem(&selected_item, &hand_item);
 +      const ItemDefinition &selected_def = selected_item.getDefinition(getGameDef(L)->idef());
 +      v3s16 pos = read_v3s16(L, 1);
 +      PointedThing pointed;
 +      pointed.type = POINTEDTHING_NODE;
 +      pointed.node_abovesurface = pos;
 +      pointed.node_undersurface = pos;
 +      NodeMetadata *meta = map.getNodeMetadata(pos);
 +      g_game->nodePlacement(selected_def, selected_item, pos, pos, pointed, meta, true);
 +      return 0;
 +}
 +
 +// dig_node(pos)
 +int ModApiClient::l_dig_node(lua_State *L)
 +{
 +      Client *client = getClient(L);
 +      v3s16 pos = read_v3s16(L, 1);
 +      PointedThing pointed;
 +      pointed.type = POINTEDTHING_NODE;
 +      pointed.node_abovesurface = pos;
 +      pointed.node_undersurface = pos;
 +      client->interact(INTERACT_START_DIGGING, pointed);
 +      client->interact(INTERACT_DIGGING_COMPLETED, pointed);
 +      client->removeNode(pos);
 +      return 0;
 +}
 +
 +// get_inventory(location)
 +int ModApiClient::l_get_inventory(lua_State *L)
 +{
 +      Client *client = getClient(L);
 +      InventoryLocation inventory_location;
 +      Inventory *inventory;
 +      std::string location;
 +
 +      location = readParam<std::string>(L, 1);
 +
 +      try {
 +              inventory_location.deSerialize(location);
 +              inventory = client->getInventory(inventory_location);
 +              if (! inventory)
 +                      throw SerializationError(std::string("Attempt to access nonexistant inventory (") + location + ")");
++              push_inventory_lists(L, *inventory);
 +      } catch (SerializationError &) {
 +              lua_pushnil(L);
 +      }
 +
 +      return 1;
 +}
 +
 +// set_keypress(key_setting, pressed) -> returns true on success
 +int ModApiClient::l_set_keypress(lua_State *L)
 +{
 +      std::string setting_name = "keymap_" + readParam<std::string>(L, 1);
 +      bool pressed = lua_isboolean(L, 2) && readParam<bool>(L, 2);
 +      try {
 +              KeyPress keyCode = getKeySetting(setting_name.c_str());
 +              if (pressed)
 +                      g_game->input->setKeypress(keyCode);
 +              else
 +                      g_game->input->unsetKeypress(keyCode);
 +              lua_pushboolean(L, true);
 +      } catch (SettingNotFoundException &) {
 +              lua_pushboolean(L, false);
 +      }
 +      return 1;
 +}
 +
 +// drop_selected_item()
 +int ModApiClient::l_drop_selected_item(lua_State *L)
 +{
 +      g_game->dropSelectedItem();
 +      return 0;
 +}
 +
 +// get_objects_inside_radius(pos, radius)
 +int ModApiClient::l_get_objects_inside_radius(lua_State *L)
 +{
 +      ClientEnvironment &env = getClient(L)->getEnv();
 +
 +      v3f pos = checkFloatPos(L, 1);
 +      float radius = readParam<float>(L, 2) * BS;
 +
 +      std::vector<DistanceSortedActiveObject> objs;
 +      env.getActiveObjects(pos, radius, objs);
 +
 +      int i = 0;
 +      lua_createtable(L, objs.size(), 0);
 +      for (const auto obj : objs) {
 +              push_objectRef(L, obj.obj->getId());
 +              lua_rawseti(L, -2, ++i);
 +      }
 +      return 1;
 +}
 +
 +// make_screenshot()
 +int ModApiClient::l_make_screenshot(lua_State *L)
 +{
 +      getClient(L)->makeScreenshot();
 +      return 0;
 +}
 +
 +/*
 +`pointed_thing`
 +---------------
 +
 +* `{type="nothing"}`
 +* `{type="node", under=pos, above=pos}`
 +    * Indicates a pointed node selection box.
 +    * `under` refers to the node position behind the pointed face.
 +    * `above` refers to the node position in front of the pointed face.
 +* `{type="object", ref=ObjectRef}`
 +
 +Exact pointing location (currently only `Raycast` supports these fields):
 +
 +* `pointed_thing.intersection_point`: The absolute world coordinates of the
 +  point on the selection box which is pointed at. May be in the selection box
 +  if the pointer is in the box too.
 +* `pointed_thing.box_id`: The ID of the pointed selection box (counting starts
 +  from 1).
 +* `pointed_thing.intersection_normal`: Unit vector, points outwards of the
 +  selected selection box. This specifies which face is pointed at.
 +  Is a null vector `{x = 0, y = 0, z = 0}` when the pointer is inside the
 +  selection box.
 +*/
 +
 +// interact(action, pointed_thing)
 +int ModApiClient::l_interact(lua_State *L)
 +{
 +      std::string action_str = readParam<std::string>(L, 1);
 +      InteractAction action;
 +
 +      if (action_str == "start_digging")
 +              action = INTERACT_START_DIGGING;
 +      else if (action_str == "stop_digging")
 +              action = INTERACT_STOP_DIGGING;
 +      else if (action_str == "digging_completed")
 +              action = INTERACT_DIGGING_COMPLETED;
 +      else if (action_str == "place")
 +              action = INTERACT_PLACE;
 +      else if (action_str == "use")
 +              action = INTERACT_USE;
 +      else if (action_str == "activate")
 +              action = INTERACT_ACTIVATE;
 +      else
 +              return 0;
 +
 +      lua_getfield(L, 2, "type");
 +      if (! lua_isstring(L, -1))
 +              return 0;
 +      std::string type_str = lua_tostring(L, -1);
 +      lua_pop(L, 1);
 +
 +      PointedThingType type;
 +
 +      if (type_str == "nothing")
 +              type = POINTEDTHING_NOTHING;
 +      else if (type_str == "node")
 +              type = POINTEDTHING_NODE;
 +      else if (type_str == "object")
 +              type = POINTEDTHING_OBJECT;
 +      else
 +              return 0;
 +
 +      PointedThing pointed;
 +      pointed.type = type;
 +      ClientObjectRef *obj;
 +
 +      switch (type) {
 +      case POINTEDTHING_NODE:
 +              lua_getfield(L, 2, "under");
 +              pointed.node_undersurface = check_v3s16(L, -1);
 +
 +              lua_getfield(L, 2, "above");
 +              pointed.node_abovesurface = check_v3s16(L, -1);
 +              break;
 +      case POINTEDTHING_OBJECT:
 +              lua_getfield(L, 2, "ref");
 +              obj = ClientObjectRef::checkobject(L, -1);
 +              pointed.object_id = obj->getClientActiveObject()->getId();
 +              break;
 +      default:
 +              break;
 +      }
 +
 +      getClient(L)->interact(action, pointed);
 +      lua_pushboolean(L, true);
 +      return 1;
 +}
 +
 +StringMap *table_to_stringmap(lua_State *L, int index)
 +{
 +      StringMap *m = new StringMap;
 +
 +      lua_pushvalue(L, index);
 +      lua_pushnil(L);
 +
 +      while (lua_next(L, -2)) {
 +              lua_pushvalue(L, -2);
 +              std::basic_string<char> key = lua_tostring(L, -1);
 +              std::basic_string<char> value = lua_tostring(L, -2);
 +              (*m)[key] = value;
 +              lua_pop(L, 2);
 +      }
 +
 +      lua_pop(L, 1);
 +
 +      return m;
 +}
 +
 +// send_inventory_fields(formname, fields)
 +// Only works if the inventory form was opened beforehand.
 +int ModApiClient::l_send_inventory_fields(lua_State *L)
 +{
 +      std::string formname = luaL_checkstring(L, 1);
 +      StringMap *fields = table_to_stringmap(L, 2);
 +
 +      getClient(L)->sendInventoryFields(formname, *fields);
 +      return 0;
 +}
 +
 +// send_nodemeta_fields(position, formname, fields)
 +int ModApiClient::l_send_nodemeta_fields(lua_State *L)
 +{
 +      v3s16 pos = check_v3s16(L, 1);
 +      std::string formname = luaL_checkstring(L, 2);
 +      StringMap *m = table_to_stringmap(L, 3);
 +
 +      getClient(L)->sendNodemetaFields(pos, formname, *m);
 +      return 0;
 +}
 +
  void ModApiClient::Initialize(lua_State *L, int top)
  {
        API_FCT(get_current_modname);
        API_FCT(get_builtin_path);
        API_FCT(get_language);
        API_FCT(get_csm_restrictions);
 +      API_FCT(send_damage);
 +      API_FCT(place_node);
 +      API_FCT(dig_node);
 +      API_FCT(get_inventory);
 +      API_FCT(set_keypress);
 +      API_FCT(drop_selected_item);
 +      API_FCT(get_objects_inside_radius);
 +      API_FCT(make_screenshot);
 +      API_FCT(interact);
 +      API_FCT(send_inventory_fields);
 +      API_FCT(send_nodemeta_fields);
  }
index 876f84d537f443f50ef760a16e59de25bd84c5db,7640f27825dde091016f9ca0a848676836e11326..a489d245c431e18b0a4f079b3f5a85283f3ff0c3
@@@ -477,7 -477,7 +477,7 @@@ int ModApiEnvMod::l_place_node(lua_Stat
                return 1;
        }
        // Create item to place
-       ItemStack item(ndef->get(n).name, 1, 0, idef);
+       Optional<ItemStack> item = ItemStack(ndef->get(n).name, 1, 0, idef);
        // Make pointed position
        PointedThing pointed;
        pointed.type = POINTEDTHING_NODE;
@@@ -857,6 -857,7 +857,6 @@@ int ModApiEnvMod::l_find_node_near(lua_
        int radius = luaL_checkinteger(L, 2);
        std::vector<content_t> filter;
        collectNodeIds(L, 3, ndef, filter);
 -
        int start_radius = (lua_isboolean(L, 4) && readParam<bool>(L, 4)) ? 0 : 1;
  
  #ifndef SERVER
        return 0;
  }
  
 +// find_nodes_near(pos, radius, nodenames, [search_center])
 +// nodenames: eg. {"ignore", "group:tree"} or "default:dirt"
 +int ModApiEnvMod::l_find_nodes_near(lua_State *L)
 +{
 +      GET_PLAIN_ENV_PTR;
 +
 +      const NodeDefManager *ndef = env->getGameDef()->ndef();
 +      Map &map = env->getMap();
 +
 +      v3s16 pos = read_v3s16(L, 1);
 +      int radius = luaL_checkinteger(L, 2);
 +      std::vector<content_t> filter;
 +      collectNodeIds(L, 3, ndef, filter);
 +
 +      int start_radius = (lua_isboolean(L, 4) && readParam<bool>(L, 4)) ? 0 : 1;
 +
 +#ifndef SERVER
 +      // Client API limitations
 +      if (Client *client = getClient(L))
 +              radius = client->CSMClampRadius(pos, radius);
 +#endif
 +
 +      std::vector<u32> individual_count;
 +      individual_count.resize(filter.size());
 +
 +      lua_newtable(L);
 +      u32 i = 0;
 +
 +      for (int d = start_radius; d <= radius; d++) {
 +              const std::vector<v3s16> &list = FacePositionCache::getFacePositions(d);
 +              for (const v3s16 &posi : list) {
 +                      v3s16 p = pos + posi;
 +                      content_t c = map.getNode(p).getContent();
 +                      auto it = std::find(filter.begin(), filter.end(), c);
 +                      if (it != filter.end()) {
 +                              push_v3s16(L, p);
 +                              lua_rawseti(L, -2, ++i);
 +
 +                              u32 filt_index = it - filter.begin();
 +                              individual_count[filt_index]++;
 +                      }
 +              }
 +      }
 +      lua_createtable(L, 0, filter.size());
 +      for (u32 i = 0; i < filter.size(); i++) {
 +              lua_pushinteger(L, individual_count[i]);
 +              lua_setfield(L, -2, ndef->get(filter[i]).name.c_str());
 +      }
 +      return 2;
 +}
 +
 +// find_nodes_near_under_air(pos, radius, nodenames, [search_center])
 +// nodenames: eg. {"ignore", "group:tree"} or "default:dirt"
 +int ModApiEnvMod::l_find_nodes_near_under_air(lua_State *L)
 +{
 +      GET_PLAIN_ENV_PTR;
 +
 +      const NodeDefManager *ndef = env->getGameDef()->ndef();
 +      Map &map = env->getMap();
 +
 +      v3s16 pos = read_v3s16(L, 1);
 +      int radius = luaL_checkinteger(L, 2);
 +      std::vector<content_t> filter;
 +      collectNodeIds(L, 3, ndef, filter);
 +      int start_radius = (lua_isboolean(L, 4) && readParam<bool>(L, 4)) ? 0 : 1;
 +
 +#ifndef SERVER
 +      // Client API limitations
 +      if (Client *client = getClient(L))
 +              radius = client->CSMClampRadius(pos, radius);
 +#endif
 +
 +      std::vector<u32> individual_count;
 +      individual_count.resize(filter.size());
 +
 +      lua_newtable(L);
 +      u32 i = 0;
 +
 +      for (int d = start_radius; d <= radius; d++) {
 +              const std::vector<v3s16> &list = FacePositionCache::getFacePositions(d);
 +              for (const v3s16 &posi : list) {
 +                      v3s16 p = pos + posi;
 +                      content_t c = map.getNode(p).getContent();
 +                      v3s16 psurf(p.X, p.Y + 1, p.Z);
 +                      content_t csurf = map.getNode(psurf).getContent();
 +                      if (c == CONTENT_AIR || csurf != CONTENT_AIR)
 +                              continue;
 +                      auto it = std::find(filter.begin(), filter.end(), c);
 +                      if (it != filter.end()) {
 +                              push_v3s16(L, p);
 +                              lua_rawseti(L, -2, ++i);
 +
 +                              u32 filt_index = it - filter.begin();
 +                              individual_count[filt_index]++;
 +                      }
 +              }
 +      }
 +      lua_createtable(L, 0, filter.size());
 +      for (u32 i = 0; i < filter.size(); i++) {
 +              lua_pushinteger(L, individual_count[i]);
 +              lua_setfield(L, -2, ndef->get(filter[i]).name.c_str());
 +      }
 +      return 2;
 +}
 +
 +// find_nodes_near_under_air_except(pos, radius, nodenames, [search_center])
 +// nodenames: eg. {"ignore", "group:tree"} or "default:dirt"
 +int ModApiEnvMod::l_find_nodes_near_under_air_except(lua_State *L)
 +{
 +      GET_PLAIN_ENV_PTR;
 +
 +      const NodeDefManager *ndef = env->getGameDef()->ndef();
 +      Map &map = env->getMap();
 +
 +      v3s16 pos = read_v3s16(L, 1);
 +      int radius = luaL_checkinteger(L, 2);
 +      std::vector<content_t> filter;
 +      collectNodeIds(L, 3, ndef, filter);
 +      int start_radius = (lua_isboolean(L, 4) && readParam<bool>(L, 4)) ? 0 : 1;
 +
 +#ifndef SERVER
 +      // Client API limitations
 +      if (Client *client = getClient(L))
 +              radius = client->CSMClampRadius(pos, radius);
 +#endif
 +
 +      std::vector<u32> individual_count;
 +      individual_count.resize(filter.size());
 +
 +      lua_newtable(L);
 +      u32 i = 0;
 +
 +      for (int d = start_radius; d <= radius; d++) {
 +              const std::vector<v3s16> &list = FacePositionCache::getFacePositions(d);
 +              for (const v3s16 &posi : list) {
 +                      v3s16 p = pos + posi;
 +                      content_t c = map.getNode(p).getContent();
 +                      v3s16 psurf(p.X, p.Y + 1, p.Z);
 +                      content_t csurf = map.getNode(psurf).getContent();
 +                      if (c == CONTENT_AIR || csurf != CONTENT_AIR)
 +                              continue;
 +                      auto it = std::find(filter.begin(), filter.end(), c);
 +                      if (it == filter.end()) {
 +                              push_v3s16(L, p);
 +                              lua_rawseti(L, -2, ++i);
 +
 +                              u32 filt_index = it - filter.begin();
 +                              individual_count[filt_index]++;
 +                      }
 +              }
 +      }
 +      lua_createtable(L, 0, filter.size());
 +      for (u32 i = 0; i < filter.size(); i++) {
 +              lua_pushinteger(L, individual_count[i]);
 +              lua_setfield(L, -2, ndef->get(filter[i]).name.c_str());
 +      }
 +      return 2;
 +}
 +
+ static void checkArea(v3s16 &minp, v3s16 &maxp)
+ {
+       auto volume = VoxelArea(minp, maxp).getVolume();
+       // Volume limit equal to 8 default mapchunks, (80 * 2) ^ 3 = 4,096,000
+       if (volume > 4096000) {
+               throw LuaError("Area volume exceeds allowed value of 4096000");
+       }
+       // Clamp to map range to avoid problems
+ #define CLAMP(arg) core::clamp(arg, (s16)-MAX_MAP_GENERATION_LIMIT, (s16)MAX_MAP_GENERATION_LIMIT)
+       minp = v3s16(CLAMP(minp.X), CLAMP(minp.Y), CLAMP(minp.Z));
+       maxp = v3s16(CLAMP(maxp.X), CLAMP(maxp.Y), CLAMP(maxp.Z));
+ #undef CLAMP
+ }
  // find_nodes_in_area(minp, maxp, nodenames, [grouped])
  int ModApiEnvMod::l_find_nodes_in_area(lua_State *L)
  {
        }
  #endif
  
-       v3s16 cube = maxp - minp + 1;
-       // Volume limit equal to 8 default mapchunks, (80 * 2) ^ 3 = 4,096,000
-       if ((u64)cube.X * (u64)cube.Y * (u64)cube.Z > 4096000) {
-               luaL_error(L, "find_nodes_in_area(): area volume"
-                               " exceeds allowed value of 4096000");
-               return 0;
-       }
+       checkArea(minp, maxp);
  
        std::vector<content_t> filter;
        collectNodeIds(L, 3, ndef, filter);
@@@ -1168,13 -1019,7 +1177,7 @@@ int ModApiEnvMod::l_find_nodes_in_area_
        }
  #endif
  
-       v3s16 cube = maxp - minp + 1;
-       // Volume limit equal to 8 default mapchunks, (80 * 2) ^ 3 = 4,096,000
-       if ((u64)cube.X * (u64)cube.Y * (u64)cube.Z > 4096000) {
-               luaL_error(L, "find_nodes_in_area_under_air(): area volume"
-                               " exceeds allowed value of 4096000");
-               return 0;
-       }
+       checkArea(minp, maxp);
  
        std::vector<content_t> filter;
        collectNodeIds(L, 3, ndef, filter);
@@@ -1440,7 -1285,7 +1443,7 @@@ int ModApiEnvMod::l_delete_area(lua_Sta
  //     max_jump, max_drop, algorithm) -> table containing path
  int ModApiEnvMod::l_find_path(lua_State *L)
  {
 -      GET_ENV_PTR;
 +      Environment *env = getEnv(L);
  
        v3s16 pos1                  = read_v3s16(L, 1);
        v3s16 pos2                  = read_v3s16(L, 2);
                        algo = PA_DIJKSTRA;
        }
  
 -      std::vector<v3s16> path = get_path(&env->getServerMap(), env->getGameDef()->ndef(), pos1, pos2,
 +      std::vector<v3s16> path = get_path(&env->getMap(), env->getGameDef()->ndef(), pos1, pos2,
                searchdistance, max_jump, max_drop, algo);
  
        if (!path.empty()) {
@@@ -1541,7 -1386,7 +1544,7 @@@ int ModApiEnvMod::l_transforming_liquid
        GET_ENV_PTR;
  
        v3s16 p0 = read_v3s16(L, 1);
-       env->getMap().transforming_liquid_add(p0);
+       env->getServerMap().transforming_liquid_add(p0);
        return 1;
  }
  
@@@ -1659,13 -1504,8 +1662,13 @@@ void ModApiEnvMod::InitializeClient(lua
        API_FCT(get_node_level);
        API_FCT(find_nodes_with_meta);
        API_FCT(find_node_near);
 +      API_FCT(find_nodes_near);
 +      API_FCT(find_nodes_near_under_air);
 +      API_FCT(find_nodes_near_under_air_except);
        API_FCT(find_nodes_in_area);
        API_FCT(find_nodes_in_area_under_air);
 +      API_FCT(get_voxel_manip);
 +      API_FCT(find_path);
        API_FCT(line_of_sight);
        API_FCT(raycast);
  }
index a5ac21e21ffcb2d58954e33430adac5870be51d9,a7d406d2a51a6b4017b194cf4430b888102f784a..70a8d239881f9c36dff0f3ddb3728d720d4c7c3d
@@@ -114,7 -114,7 +114,7 @@@ private
  
        // get_objects_inside_radius(pos, radius)
        static int l_get_objects_inside_radius(lua_State *L);
-       
        // get_objects_in_area(pos, minp, maxp)
        static int l_get_objects_in_area(lua_State *L);
  
        // find_node_near(pos, radius, nodenames, search_center) -> pos or nil
        // nodenames: eg. {"ignore", "group:tree"} or "default:dirt"
        static int l_find_node_near(lua_State *L);
 +      
 +      // find_nodes_near(pos, radius, nodenames, search_center) -> list of positions
 +      // nodenames: eg. {"ignore", "group:tree"} or "default:dirt"
 +      static int l_find_nodes_near(lua_State *L);
 +      
 +      // find_nodes_near_under_air(pos, radius, nodenames, search_center) -> list of positions
 +      // nodenames: eg. {"ignore", "group:tree"} or "default:dirt"
 +      static int l_find_nodes_near_under_air(lua_State *L);
 +      
 +      // find_nodes_near_under_air(pos, radius, nodenames, search_center) -> list of positions
 +      // nodenames: eg. {"ignore", "group:tree"} or "default:dirt"
 +      static int l_find_nodes_near_under_air_except(lua_State *L);
  
        // find_nodes_in_area(minp, maxp, nodenames) -> list of positions
        // nodenames: eg. {"ignore", "group:tree"} or "default:dirt"
index 2ea2bd4f69cd21dea0a747f267c22a10a0548b3b,b58b994d95a78ea02f4df921e9b05397955c7751..9220259ff1d6bd59394a9daab87b4755d54115cd
@@@ -22,17 -22,13 +22,18 @@@ with this program; if not, write to th
  #include "lua_api/l_internal.h"
  #include "common/c_converter.h"
  #include "common/c_content.h"
+ #include "common/c_packer.h"
  #include "itemdef.h"
  #include "nodedef.h"
  #include "server.h"
  #include "inventory.h"
  #include "log.h"
 -
 +#include "script/cpp_api/s_base.h"
 +#ifndef SERVER
 +#include "client/client.h"
 +#include "client/renderingengine.h"
 +#include "client/shader.h"
 +#endif
  
  // garbage collector
  int LuaItemStack::gc_object(lua_State *L)
@@@ -446,6 -442,7 +447,7 @@@ int LuaItemStack::create_object(lua_Sta
        lua_setmetatable(L, -2);
        return 1;
  }
  // Not callable from Lua
  int LuaItemStack::create(lua_State *L, const ItemStack &item)
  {
@@@ -462,6 -459,20 +464,20 @@@ LuaItemStack *LuaItemStack::checkobject
        return *(LuaItemStack **)luaL_checkudata(L, narg, className);
  }
  
+ void *LuaItemStack::packIn(lua_State *L, int idx)
+ {
+       LuaItemStack *o = checkobject(L, idx);
+       return new ItemStack(o->getItem());
+ }
+ void LuaItemStack::packOut(lua_State *L, void *ptr)
+ {
+       ItemStack *stack = reinterpret_cast<ItemStack*>(ptr);
+       if (L)
+               create(L, *stack);
+       delete stack;
+ }
  void LuaItemStack::Register(lua_State *L)
  {
        lua_newtable(L);
  
        // Can be created from Lua (ItemStack(itemstack or itemstring or table or nil))
        lua_register(L, className, create_object);
+       script_register_packer(L, className, packIn, packOut);
  }
  
  const char LuaItemStack::className[] = "ItemStack";
@@@ -539,9 -552,9 +557,9 @@@ int ModApiItemMod::l_register_item_raw(
  
        // Get the writable item and node definition managers from the server
        IWritableItemDefManager *idef =
 -                      getServer(L)->getWritableItemDefManager();
 +                      getGameDef(L)->getWritableItemDefManager();
        NodeDefManager *ndef =
 -                      getServer(L)->getWritableNodeDefManager();
 +                      getGameDef(L)->getWritableNodeDefManager();
  
        // Check if name is defined
        std::string name;
                                        + itos(MAX_REGISTERED_CONTENT+1)
                                        + ") exceeded (" + name + ")");
                }
 +              
        }
 -
 +      
        return 0; /* number of results */
  }
  
@@@ -602,12 -614,12 +620,12 @@@ int ModApiItemMod::l_unregister_item_ra
        std::string name = luaL_checkstring(L, 1);
  
        IWritableItemDefManager *idef =
 -                      getServer(L)->getWritableItemDefManager();
 +                      getGameDef(L)->getWritableItemDefManager();
  
        // Unregister the node
        if (idef->get(name).type == ITEM_NODE) {
                NodeDefManager *ndef =
 -                      getServer(L)->getWritableNodeDefManager();
 +                      getGameDef(L)->getWritableNodeDefManager();
                ndef->removeNode(name);
        }
  
@@@ -625,7 -637,7 +643,7 @@@ int ModApiItemMod::l_register_alias_raw
  
        // Get the writable item definition manager from the server
        IWritableItemDefManager *idef =
 -                      getServer(L)->getWritableItemDefManager();
 +                      getGameDef(L)->getWritableItemDefManager();
  
        idef->registerAlias(name, convert_to);
  
@@@ -638,8 -650,8 +656,8 @@@ int ModApiItemMod::l_get_content_id(lua
        NO_MAP_LOCK_REQUIRED;
        std::string name = luaL_checkstring(L, 1);
  
-       const IItemDefManager *idef = getGameDef(L)->getItemDefManager();
-       const NodeDefManager *ndef = getGameDef(L)->getNodeDefManager();
+       const IItemDefManager *idef = getGameDef(L)->idef();
+       const NodeDefManager *ndef = getGameDef(L)->ndef();
  
        // If this is called at mod load time, NodeDefManager isn't aware of
        // aliases yet, so we need to handle them manually
@@@ -664,7 -676,7 +682,7 @@@ int ModApiItemMod::l_get_name_from_cont
        NO_MAP_LOCK_REQUIRED;
        content_t c = luaL_checkint(L, 1);
  
-       const NodeDefManager *ndef = getGameDef(L)->getNodeDefManager();
+       const NodeDefManager *ndef = getGameDef(L)->ndef();
        const char *name = ndef->get(c).name.c_str();
  
        lua_pushstring(L, name);
@@@ -679,3 -691,10 +697,10 @@@ void ModApiItemMod::Initialize(lua_Stat
        API_FCT(get_content_id);
        API_FCT(get_name_from_content_id);
  }
+ void ModApiItemMod::InitializeAsync(lua_State *L, int top)
+ {
+       // all read-only functions
+       API_FCT(get_content_id);
+       API_FCT(get_name_from_content_id);
+ }
index 769b3ef2b3bdf3adcefdc9a0e0febe537d2b675c,2efb976c7ed0fc78ebf05d0d56aa6b5273429b4f..1da0679d69ea5bfefce9f05fa7d234407615683d
@@@ -17,7 -17,6 +17,7 @@@ with this program; if not, write to th
  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  */
  
 +#include "l_clientobject.h"
  #include "l_localplayer.h"
  #include "l_internal.h"
  #include "lua_api/l_item.h"
@@@ -25,9 -24,7 +25,9 @@@
  #include "client/localplayer.h"
  #include "hud.h"
  #include "common/c_content.h"
 +#include "client/client.h"
  #include "client/content_cao.h"
 +#include "client/game.h"
  
  LuaLocalPlayer::LuaLocalPlayer(LocalPlayer *m) : m_localplayer(m)
  {
@@@ -63,57 -60,6 +63,57 @@@ int LuaLocalPlayer::l_get_velocity(lua_
        return 1;
  }
  
 +int LuaLocalPlayer::l_set_velocity(lua_State *L)
 +{
 +      LocalPlayer *player = getobject(L, 1);
 +
 +      v3f pos = checkFloatPos(L, 2);
 +      player->setSpeed(pos);
 +
 +      return 0;
 +}
 +
 +int LuaLocalPlayer::l_get_yaw(lua_State *L)
 +{
 +    lua_pushnumber(L, wrapDegrees_0_360(g_game->cam_view.camera_yaw));
 +    return 1;
 +}
 +
 +int LuaLocalPlayer::l_set_yaw(lua_State *L)
 +{
 +      LocalPlayer *player = getobject(L, 1);
 +
 +      if (lua_isnumber(L, 2)) {
 +              double yaw = lua_tonumber(L, 2);
 +              player->setYaw(yaw);
 +              g_game->cam_view.camera_yaw = yaw;
 +              g_game->cam_view_target.camera_yaw = yaw;
 +      }
 +
 +      return 0;
 +}
 +
 +int LuaLocalPlayer::l_get_pitch(lua_State *L)
 +{
 +    lua_pushnumber(L, -wrapDegrees_180(g_game->cam_view.camera_pitch) );
 +    return 1;
 +}
 +
 +int LuaLocalPlayer::l_set_pitch(lua_State *L)
 +{
 +      LocalPlayer *player = getobject(L, 1);
 +
 +      if (lua_isnumber(L, 2)) {
 +              double pitch = lua_tonumber(L, 2);
 +              player->setPitch(pitch);
 +              g_game->cam_view.camera_pitch = pitch;
 +              g_game->cam_view_target.camera_pitch = pitch;
 +      }
 +
 +      return 0;
 +}
 +
 +
  int LuaLocalPlayer::l_get_hp(lua_State *L)
  {
        LocalPlayer *player = getobject(L, 1);
@@@ -135,24 -81,10 +135,24 @@@ int LuaLocalPlayer::l_get_wield_index(l
  {
        LocalPlayer *player = getobject(L, 1);
  
 -      lua_pushinteger(L, player->getWieldIndex());
 +      lua_pushinteger(L, player->getWieldIndex() + 1);
        return 1;
  }
  
 +// set_wield_index(self)
 +int LuaLocalPlayer::l_set_wield_index(lua_State *L)
 +{
 +      LocalPlayer *player = getobject(L, 1);
 +      u32 index = luaL_checkinteger(L, 2) - 1;
 +
 +      player->setWieldIndex(index);
 +      g_game->processItemSelection(&g_game->runData.new_playeritem);
 +      ItemStack selected_item, hand_item;
 +      ItemStack &tool_item = player->getWieldedItem(&selected_item, &hand_item);
 +      g_game->camera->wield(tool_item);
 +      return 0;
 +}
 +
  // get_wielded_item(self)
  int LuaLocalPlayer::l_get_wielded_item(lua_State *L)
  {
@@@ -196,11 -128,11 +196,11 @@@ int LuaLocalPlayer::l_is_in_liquid_stab
        return 1;
  }
  
- int LuaLocalPlayer::l_get_liquid_viscosity(lua_State *L)
+ int LuaLocalPlayer::l_get_move_resistance(lua_State *L)
  {
        LocalPlayer *player = getobject(L, 1);
  
-       lua_pushinteger(L, player->liquid_viscosity);
+       lua_pushinteger(L, player->move_resistance);
        return 1;
  }
  
@@@ -225,30 -157,26 +225,30 @@@ int LuaLocalPlayer::l_get_physics_overr
  {
        LocalPlayer *player = getobject(L, 1);
  
 -      lua_newtable(L);
 -      lua_pushnumber(L, player->physics_override_speed);
 -      lua_setfield(L, -2, "speed");
 +      push_physics_override(L, player->physics_override_speed, player->physics_override_jump, player->physics_override_gravity, player->physics_override_sneak, player->physics_override_sneak_glitch, player->physics_override_new_move);
  
 -      lua_pushnumber(L, player->physics_override_jump);
 -      lua_setfield(L, -2, "jump");
 -
 -      lua_pushnumber(L, player->physics_override_gravity);
 -      lua_setfield(L, -2, "gravity");
 -
 -      lua_pushboolean(L, player->physics_override_sneak);
 -      lua_setfield(L, -2, "sneak");
 +      return 1;
 +}
  
 -      lua_pushboolean(L, player->physics_override_sneak_glitch);
 -      lua_setfield(L, -2, "sneak_glitch");
 +// set_physics_override(self, override)
 +int LuaLocalPlayer::l_set_physics_override(lua_State *L)
 +{
 +      LocalPlayer *player = getobject(L, 1);
  
 -      lua_pushboolean(L, player->physics_override_new_move);
 -      lua_setfield(L, -2, "new_move");
 +      player->physics_override_speed = getfloatfield_default(
 +                      L, 2, "speed", player->physics_override_speed);
 +      player->physics_override_jump = getfloatfield_default(
 +                      L, 2, "jump", player->physics_override_jump);
 +      player->physics_override_gravity = getfloatfield_default(
 +                      L, 2, "gravity", player->physics_override_gravity);
 +      player->physics_override_sneak = getboolfield_default(
 +                      L, 2, "sneak", player->physics_override_sneak);
 +      player->physics_override_sneak_glitch = getboolfield_default(
 +                      L, 2, "sneak_glitch", player->physics_override_sneak_glitch);
 +      player->physics_override_new_move = getboolfield_default(
 +                      L, 2, "new_move", player->physics_override_new_move);
  
 -      return 1;
 +      return 0;
  }
  
  int LuaLocalPlayer::l_get_last_pos(lua_State *L)
@@@ -302,13 -230,15 +302,15 @@@ int LuaLocalPlayer::l_get_control(lua_S
        set("dig",   c.dig);
        set("place", c.place);
        // Player movement in polar coordinates and non-binary speed
-       set("movement_speed",     c.movement_speed);
-       set("movement_direction", c.movement_direction);
+       lua_pushnumber(L, c.movement_speed);
+       lua_setfield(L, -2, "movement_speed");
+       lua_pushnumber(L, c.movement_direction);
+       lua_setfield(L, -2, "movement_direction");
        // Provide direction keys to ensure compatibility
-       set("up",    player->keyPressed & (1 << 0)); // Up, down, left, and right were removed in favor of
-       set("down",  player->keyPressed & (1 << 1)); // analog  direction indicators and are therefore not
-       set("left",  player->keyPressed & (1 << 2)); // available as booleans anymore. The corresponding values
-       set("right", player->keyPressed & (1 << 3)); // can still be read from the keyPressed bits though.
+       set("up",    c.direction_keys & (1 << 0));
+       set("down",  c.direction_keys & (1 << 1));
+       set("left",  c.direction_keys & (1 << 2));
+       set("right", c.direction_keys & (1 << 3));
  
        return 1;
  }
@@@ -331,17 -261,6 +333,17 @@@ int LuaLocalPlayer::l_get_pos(lua_Stat
        return 1;
  }
  
 +// set_pos(self, pos)
 +int LuaLocalPlayer::l_set_pos(lua_State *L)
 +{
 +      LocalPlayer *player = getobject(L, 1);
 +
 +      v3f pos = checkFloatPos(L, 2);
 +      player->setPosition(pos);
 +      getClient(L)->sendPlayerPos();
 +      return 0;
 +}
 +
  // get_movement_acceleration(self)
  int LuaLocalPlayer::l_get_movement_acceleration(lua_State *L)
  {
@@@ -481,27 -400,6 +483,27 @@@ int LuaLocalPlayer::l_hud_get(lua_Stat
        return 1;
  }
  
 +// get_object(self)
 +int LuaLocalPlayer::l_get_object(lua_State *L)
 +{
 +      LocalPlayer *player = getobject(L, 1);
 +      ClientEnvironment &env = getClient(L)->getEnv();
 +      ClientActiveObject *obj = env.getGenericCAO(player->getCAO()->getId());
 +
 +      push_objectRef(L, obj->getId());
 +
 +      return 1;
 +}
 +
 +// get_hotbar_size(self)
 +int LuaLocalPlayer::l_get_hotbar_size(lua_State *L)
 +{
 +      LocalPlayer *player = getobject(L, 1);
 +      lua_pushnumber(L, player->hud_hotbar_itemcount);
 +
 +      return 1;
 +}
 +
  LuaLocalPlayer *LuaLocalPlayer::checkobject(lua_State *L, int narg)
  {
        luaL_checktype(L, narg, LUA_TUSERDATA);
@@@ -562,25 -460,17 +564,24 @@@ void LuaLocalPlayer::Register(lua_Stat
  const char LuaLocalPlayer::className[] = "LocalPlayer";
  const luaL_Reg LuaLocalPlayer::methods[] = {
                luamethod(LuaLocalPlayer, get_velocity),
 +              luamethod(LuaLocalPlayer, set_velocity),
 +              luamethod(LuaLocalPlayer, get_yaw),
 +              luamethod(LuaLocalPlayer, set_yaw),
 +              luamethod(LuaLocalPlayer, get_pitch),
 +              luamethod(LuaLocalPlayer, set_pitch),
                luamethod(LuaLocalPlayer, get_hp),
                luamethod(LuaLocalPlayer, get_name),
                luamethod(LuaLocalPlayer, get_wield_index),
 +              luamethod(LuaLocalPlayer, set_wield_index),
                luamethod(LuaLocalPlayer, get_wielded_item),
                luamethod(LuaLocalPlayer, is_attached),
                luamethod(LuaLocalPlayer, is_touching_ground),
                luamethod(LuaLocalPlayer, is_in_liquid),
                luamethod(LuaLocalPlayer, is_in_liquid_stable),
-               luamethod(LuaLocalPlayer, get_liquid_viscosity),
                luamethod(LuaLocalPlayer, is_climbing),
                luamethod(LuaLocalPlayer, swimming_vertical),
                luamethod(LuaLocalPlayer, get_physics_override),
 +              luamethod(LuaLocalPlayer, set_physics_override),
                // TODO: figure our if these are useful in any way
                luamethod(LuaLocalPlayer, get_last_pos),
                luamethod(LuaLocalPlayer, get_last_velocity),
                luamethod(LuaLocalPlayer, get_control),
                luamethod(LuaLocalPlayer, get_breath),
                luamethod(LuaLocalPlayer, get_pos),
 +              luamethod(LuaLocalPlayer, set_pos),
                luamethod(LuaLocalPlayer, get_movement_acceleration),
                luamethod(LuaLocalPlayer, get_movement_speed),
                luamethod(LuaLocalPlayer, get_movement),
                luamethod(LuaLocalPlayer, hud_remove),
                luamethod(LuaLocalPlayer, hud_change),
                luamethod(LuaLocalPlayer, hud_get),
 +              luamethod(LuaLocalPlayer, get_object),
 +              luamethod(LuaLocalPlayer, get_hotbar_size),
  
+               luamethod(LuaLocalPlayer, get_move_resistance),
                {0, 0}
  };
index bb5a294ca95d4ccf3540d78822fa33b0546fbcb8,041545a49e7921cd51b680573a0f0416c4f50436..458c824e6c00bf88e2a51f49e8cfef3d3ede41c3
@@@ -35,21 -35,6 +35,21 @@@ private
        // get_velocity(self)
        static int l_get_velocity(lua_State *L);
  
 +      // set_velocity(self, vel)
 +      static int l_set_velocity(lua_State *L);
 +
 +      // get_yaw(self)
 +      static int l_get_yaw(lua_State *L);
 +
 +      // set_yaw(self, yaw)
 +      static int l_set_yaw(lua_State *L);
 +
 +      // get_pitch(self)
 +      static int l_get_pitch(lua_State *L);
 +
 +      // set_pitch(self,pitch)
 +      static int l_set_pitch(lua_State *L);
 +
        // get_hp(self)
        static int l_get_hp(lua_State *L);
  
        // get_wield_index(self)
        static int l_get_wield_index(lua_State *L);
  
 +      // set_wield_index(self)
 +      static int l_set_wield_index(lua_State *L);
 +
        // get_wielded_item(self)
        static int l_get_wielded_item(lua_State *L);
  
 +      // get_hotbar_size(self)
 +      static int l_get_hotbar_size(lua_State *L);
 +
        static int l_is_attached(lua_State *L);
        static int l_is_touching_ground(lua_State *L);
        static int l_is_in_liquid(lua_State *L);
        static int l_is_in_liquid_stable(lua_State *L);
-       static int l_get_liquid_viscosity(lua_State *L);
        static int l_is_climbing(lua_State *L);
        static int l_swimming_vertical(lua_State *L);
  
        static int l_get_physics_override(lua_State *L);
 +      static int l_set_physics_override(lua_State *L);
  
        static int l_get_override_pos(lua_State *L);
  
@@@ -95,9 -72,6 +94,9 @@@
        // get_pos(self)
        static int l_get_pos(lua_State *L);
  
 +      // set_pos(self, pos)
 +      static int l_set_pos(lua_State *L);
 +
        // get_movement_acceleration(self)
        static int l_get_movement_acceleration(lua_State *L);
  
        // hud_get(self, id)
        static int l_hud_get(lua_State *L);
  
+       static int l_get_move_resistance(lua_State *L);
 +      // get_object(self)
 +      static int l_get_object(lua_State *L);
 +
        LocalPlayer *m_localplayer = nullptr;
  
  public:
index 8eb0e252a11260f153338405e158f83eaa773b5d,db031dde5d49b36c8c1a55b7920d8fce5c4fa959..2b46a4d51b85041f7eed302d6fe58a7a92f01b40
@@@ -323,9 -323,9 +323,9 @@@ int ModApiMainMenu::l_get_games(lua_Sta
                lua_newtable(L);
                int table2 = lua_gettop(L);
                int internal_index = 1;
-               for (const std::string &addon_mods_path : game.addon_mods_paths) {
+               for (const auto &addon_mods_path : game.addon_mods_paths) {
                        lua_pushnumber(L, internal_index);
-                       lua_pushstring(L, addon_mods_path.c_str());
+                       lua_pushstring(L, addon_mods_path.second.c_str());
                        lua_settable(L,   table2);
                        internal_index++;
                }
@@@ -414,25 -414,53 +414,53 @@@ int ModApiMainMenu::l_create_world(lua_
        const char *name        = luaL_checkstring(L, 1);
        int gameidx                     = luaL_checkinteger(L,2) -1;
  
+       StringMap use_settings;
+       luaL_checktype(L, 3, LUA_TTABLE);
+       lua_pushnil(L);
+       while (lua_next(L, 3) != 0) {
+               // key at index -2 and value at index -1
+               use_settings[luaL_checkstring(L, -2)] = luaL_checkstring(L, -1);
+               lua_pop(L, 1);
+       }
+       lua_pop(L, 1);
        std::string path = porting::path_user + DIR_DELIM
                        "worlds" + DIR_DELIM
                        + sanitizeDirName(name, "world_");
  
        std::vector<SubgameSpec> games = getAvailableGames();
+       if (gameidx < 0 || gameidx >= (int) games.size()) {
+               lua_pushstring(L, "Invalid game index");
+               return 1;
+       }
  
-       if ((gameidx >= 0) &&
-                       (gameidx < (int) games.size())) {
+       // Set the settings for world creation
+       // this is a bad hack but the best we have right now..
+       StringMap backup;
+       for (auto it : use_settings) {
+               if (g_settings->existsLocal(it.first))
+                       backup[it.first] = g_settings->get(it.first);
+               g_settings->set(it.first, it.second);
+       }
  
-               // Create world if it doesn't exist
-               try {
-                       loadGameConfAndInitWorld(path, name, games[gameidx], true);
-                       lua_pushnil(L);
-               } catch (const BaseException &e) {
-                       lua_pushstring(L, (std::string("Failed to initialize world: ") + e.what()).c_str());
-               }
-       } else {
-               lua_pushstring(L, "Invalid game index");
+       // Create world if it doesn't exist
+       try {
+               loadGameConfAndInitWorld(path, name, games[gameidx], true);
+               lua_pushnil(L);
+       } catch (const BaseException &e) {
+               auto err = std::string("Failed to initialize world: ") + e.what();
+               lua_pushstring(L, err.c_str());
+       }
+       // Restore previous settings
+       for (auto it : use_settings) {
+               auto it2 = backup.find(it.first);
+               if (it2 == backup.end())
+                       g_settings->remove(it.first); // wasn't set before
+               else
+                       g_settings->set(it.first, it2->second); // was set before
        }
        return 1;
  }
  
@@@ -502,6 -530,21 +530,21 @@@ int ModApiMainMenu::l_get_modpath(lua_S
        return 1;
  }
  
+ /******************************************************************************/
+ int ModApiMainMenu::l_get_modpaths(lua_State *L)
+ {
+       lua_newtable(L);
+       ModApiMainMenu::l_get_modpath(L);
+       lua_setfield(L, -2, "mods");
+       for (const std::string &component : getEnvModPaths()) {
+               lua_pushstring(L, component.c_str());
+               lua_setfield(L, -2, fs::AbsolutePath(component).c_str());
+       }
+       return 1;
+ }
  /******************************************************************************/
  int ModApiMainMenu::l_get_clientmodpath(lua_State *L)
  {
@@@ -548,7 -591,10 +591,10 @@@ int ModApiMainMenu::l_get_cache_path(lu
  /******************************************************************************/
  int ModApiMainMenu::l_get_temp_path(lua_State *L)
  {
-       lua_pushstring(L, fs::TempPath().c_str());
+       if (lua_isnoneornil(L, 1) || !lua_toboolean(L, 1))
+               lua_pushstring(L, fs::TempPath().c_str());
+       else
+               lua_pushstring(L, fs::CreateTempFile().c_str());
        return 1;
  }
  
@@@ -588,26 -634,24 +634,24 @@@ int ModApiMainMenu::l_copy_dir(lua_Stat
        const char *destination = luaL_checkstring(L, 2);
  
        bool keep_source = true;
+       if (!lua_isnoneornil(L, 3))
+               keep_source = readParam<bool>(L, 3);
  
-       if ((!lua_isnone(L,3)) &&
-                       (!lua_isnil(L,3))) {
-               keep_source = readParam<bool>(L,3);
-       }
-       std::string absolute_destination = fs::RemoveRelativePathComponents(destination);
-       std::string absolute_source = fs::RemoveRelativePathComponents(source);
+       std::string abs_destination = fs::RemoveRelativePathComponents(destination);
+       std::string abs_source = fs::RemoveRelativePathComponents(source);
  
-       if ((ModApiMainMenu::mayModifyPath(absolute_destination))) {
-               bool retval = fs::CopyDir(absolute_source,absolute_destination);
-               if (retval && (!keep_source)) {
-                       retval &= fs::RecursiveDelete(absolute_source);
-               }
-               lua_pushboolean(L,retval);
+       if (!ModApiMainMenu::mayModifyPath(abs_destination) ||
+               (!keep_source && !ModApiMainMenu::mayModifyPath(abs_source))) {
+               lua_pushboolean(L, false);
                return 1;
        }
-       lua_pushboolean(L,false);
+       bool retval;
+       if (keep_source)
+               retval = fs::CopyDir(abs_source, abs_destination);
+       else
+               retval = fs::MoveDir(abs_source, abs_destination);
+       lua_pushboolean(L, retval);
        return 1;
  }
  
@@@ -629,9 -673,9 +673,9 @@@ int ModApiMainMenu::l_extract_zip(lua_S
        std::string absolute_destination = fs::RemoveRelativePathComponents(destination);
  
        if (ModApiMainMenu::mayModifyPath(absolute_destination)) {
-               auto rendering_engine = getGuiEngine(L)->m_rendering_engine;
-               fs::CreateAllDirs(absolute_destination);
-               lua_pushboolean(L, fs::extractZipFile(rendering_engine->get_filesystem(), zipfile, destination));
+               auto fs = RenderingEngine::get_raw_device()->getFileSystem();
+               bool ok = fs::extractZipFile(fs, zipfile, destination);
+               lua_pushboolean(L, ok);
                return 1;
        }
  
@@@ -661,10 -705,6 +705,10 @@@ bool ModApiMainMenu::mayModifyPath(std:
  
        if (fs::PathStartsWith(path, path_user + DIR_DELIM "client"))
                return true;
 +      if (fs::PathStartsWith(path, path_user + DIR_DELIM "clientmods"))
 +              return true;
 +      if (fs::PathStartsWith(path, path_user + DIR_DELIM "textures"))
 +              return true;
        if (fs::PathStartsWith(path, path_user + DIR_DELIM "games"))
                return true;
        if (fs::PathStartsWith(path, path_user + DIR_DELIM "mods"))
@@@ -759,8 -799,9 +803,9 @@@ int ModApiMainMenu::l_get_video_drivers
  /******************************************************************************/
  int ModApiMainMenu::l_gettext(lua_State *L)
  {
-       std::string text = strgettext(std::string(luaL_checkstring(L, 1)));
-       lua_pushstring(L, text.c_str());
+       const char *srctext = luaL_checkstring(L, 1);
+       const char *text = *srctext ? gettext(srctext) : "";
+       lua_pushstring(L, text);
  
        return 1;
  }
@@@ -860,6 -901,7 +905,7 @@@ void ModApiMainMenu::Initialize(lua_Sta
        API_FCT(get_mapgen_names);
        API_FCT(get_user_path);
        API_FCT(get_modpath);
+       API_FCT(get_modpaths);
        API_FCT(get_clientmodpath);
        API_FCT(get_gamepath);
        API_FCT(get_texturepath);
@@@ -893,6 -935,7 +939,7 @@@ void ModApiMainMenu::InitializeAsync(lu
        API_FCT(get_mapgen_names);
        API_FCT(get_user_path);
        API_FCT(get_modpath);
+       API_FCT(get_modpaths);
        API_FCT(get_clientmodpath);
        API_FCT(get_gamepath);
        API_FCT(get_texturepath);
        API_FCT(delete_dir);
        API_FCT(copy_dir);
        API_FCT(is_dir);
-       //API_FCT(extract_zip); //TODO remove dependency to GuiEngine
+       API_FCT(extract_zip);
        API_FCT(may_modify_path);
        API_FCT(download_file);
        API_FCT(get_min_supp_proto);
        API_FCT(get_max_supp_proto);
-       //API_FCT(gettext); (gettext lib isn't threadsafe)
+       API_FCT(gettext);
  }
index b4672fe2a4a4b69a15429545767da77306a82d8a,4b0b45887f5bc11a8b7a549ac256814aaeec204a..e8105dd750039e0de35214b2917dcf1071505e1d
@@@ -21,6 -21,7 +21,7 @@@ with this program; if not, write to th
  #include "lua_api/l_internal.h"
  #include "common/c_converter.h"
  #include "common/c_content.h"
+ #include "common/c_packer.h"
  #include "cpp_api/s_base.h"
  #include "cpp_api/s_security.h"
  #include "scripting_server.h"
@@@ -57,6 -58,14 +58,14 @@@ int ModApiServer::l_get_server_uptime(l
        return 1;
  }
  
+ // get_server_max_lag()
+ int ModApiServer::l_get_server_max_lag(lua_State *L)
+ {
+       NO_MAP_LOCK_REQUIRED;
+       GET_ENV_PTR;
+       lua_pushnumber(L, env->getMaxLagEstimate());
+       return 1;
+ }
  
  // print(text)
  int ModApiServer::l_print(lua_State *L)
@@@ -282,8 -291,10 +291,10 @@@ int ModApiServer::l_ban_player(lua_Stat
  {
        NO_MAP_LOCK_REQUIRED;
  
-       Server *server = getServer(L);
+       if (!getEnv(L))
+               throw LuaError("Can't ban player before server has started up");
  
+       Server *server = getServer(L);
        const char *name = luaL_checkstring(L, 1);
        RemotePlayer *player = server->getEnv().getPlayer(name);
        if (!player) {
        return 1;
  }
  
- // kick_player(name, [reason]) -> success
- int ModApiServer::l_kick_player(lua_State *L)
+ // disconnect_player(name, [reason]) -> success
+ int ModApiServer::l_disconnect_player(lua_State *L)
  {
        NO_MAP_LOCK_REQUIRED;
+       if (!getEnv(L))
+               throw LuaError("Can't kick player before server has started up");
        const char *name = luaL_checkstring(L, 1);
-       std::string message("Kicked");
+       std::string message;
        if (lua_isstring(L, 2))
-               message.append(": ").append(readParam<std::string>(L, 2));
+               message.append(readParam<std::string>(L, 2));
        else
-               message.append(".");
+               message.append("Disconnected.");
+       Server *server = getServer(L);
  
-       RemotePlayer *player = dynamic_cast<ServerEnvironment *>(getEnv(L))->getPlayer(name);
-       if (player == NULL) {
+       RemotePlayer *player = server->getEnv().getPlayer(name);
+       if (!player) {
                lua_pushboolean(L, false); // No such player
                return 1;
        }
-       getServer(L)->DenyAccess_Legacy(player->getPeerId(), utf8_to_wide(message));
+       server->DenyAccess(player->getPeerId(), SERVER_ACCESSDENIED_CUSTOM_STRING, message);
        lua_pushboolean(L, true);
        return 1;
  }
  
 +// remove_player(name)
  int ModApiServer::l_remove_player(lua_State *L)
  {
        NO_MAP_LOCK_REQUIRED;
        std::string name = luaL_checkstring(L, 1);
        ServerEnvironment *s_env = dynamic_cast<ServerEnvironment *>(getEnv(L));
-       assert(s_env);
+       if (!s_env)
+               throw LuaError("Can't remove player before server has started up");
  
        RemotePlayer *player = s_env->getPlayer(name.c_str());
        if (!player)
@@@ -375,12 -393,11 +394,11 @@@ int ModApiServer::l_get_modpath(lua_Sta
  {
        NO_MAP_LOCK_REQUIRED;
        std::string modname = luaL_checkstring(L, 1);
-       const ModSpec *mod = getServer(L)->getModSpec(modname);
-       if (!mod) {
+       const ModSpec *mod = getGameDef(L)->getModSpec(modname);
+       if (!mod)
                lua_pushnil(L);
-               return 1;
-       }
-       lua_pushstring(L, mod->path.c_str());
+       else
+               lua_pushstring(L, mod->path.c_str());
        return 1;
  }
  
@@@ -392,13 -409,14 +410,14 @@@ int ModApiServer::l_get_modnames(lua_St
  
        // Get a list of mods
        std::vector<std::string> modlist;
-       getServer(L)->getModNames(modlist);
+       for (auto &it : getGameDef(L)->getMods())
+               modlist.emplace_back(it.name);
  
        std::sort(modlist.begin(), modlist.end());
  
        // Package them up for Lua
        lua_createtable(L, modlist.size(), 0);
-       std::vector<std::string>::iterator iter = modlist.begin();
+       auto iter = modlist.begin();
        for (u16 i = 0; iter != modlist.end(); ++iter) {
                lua_pushstring(L, iter->c_str());
                lua_rawseti(L, -2, ++i);
  int ModApiServer::l_get_worldpath(lua_State *L)
  {
        NO_MAP_LOCK_REQUIRED;
-       std::string worldpath = getServer(L)->getWorldPath();
-       lua_pushstring(L, worldpath.c_str());
+       const Server *srv = getServer(L);
+       lua_pushstring(L, srv->getWorldPath().c_str());
        return 1;
  }
  
@@@ -479,7 -497,7 +498,7 @@@ int ModApiServer::l_dynamic_add_media(l
  
        CHECK_SECURE_PATH(L, filepath.c_str(), false);
  
-       u32 token = server->getScriptIface()->allocateDynamicMediaCallback(2);
+       u32 token = server->getScriptIface()->allocateDynamicMediaCallback(L, 2);
  
        bool ok = server->dynamicAddMedia(filepath, token, to_player, ephemeral);
        if (!ok)
  int ModApiServer::l_is_singleplayer(lua_State *L)
  {
        NO_MAP_LOCK_REQUIRED;
-       lua_pushboolean(L, getServer(L)->isSingleplayer());
+       const Server *srv = getServer(L);
+       lua_pushboolean(L, srv->isSingleplayer());
        return 1;
  }
  
@@@ -508,11 -527,82 +528,82 @@@ int ModApiServer::l_notify_authenticati
        return 0;
  }
  
+ // do_async_callback(func, params, mod_origin)
+ int ModApiServer::l_do_async_callback(lua_State *L)
+ {
+       NO_MAP_LOCK_REQUIRED;
+       ServerScripting *script = getScriptApi<ServerScripting>(L);
+       luaL_checktype(L, 1, LUA_TFUNCTION);
+       luaL_checktype(L, 2, LUA_TTABLE);
+       luaL_checktype(L, 3, LUA_TSTRING);
+       call_string_dump(L, 1);
+       size_t func_length;
+       const char *serialized_func_raw = lua_tolstring(L, -1, &func_length);
+       PackedValue *param = script_pack(L, 2);
+       std::string mod_origin = readParam<std::string>(L, 3);
+       u32 jobId = script->queueAsync(
+               std::string(serialized_func_raw, func_length),
+               param, mod_origin);
+       lua_settop(L, 0);
+       lua_pushinteger(L, jobId);
+       return 1;
+ }
+ // register_async_dofile(path)
+ int ModApiServer::l_register_async_dofile(lua_State *L)
+ {
+       NO_MAP_LOCK_REQUIRED;
+       std::string path = readParam<std::string>(L, 1);
+       CHECK_SECURE_PATH(L, path.c_str(), false);
+       // Find currently running mod name (only at init time)
+       lua_rawgeti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_CURRENT_MOD_NAME);
+       if (!lua_isstring(L, -1))
+               return 0;
+       std::string modname = readParam<std::string>(L, -1);
+       getServer(L)->m_async_init_files.emplace_back(modname, path);
+       lua_pushboolean(L, true);
+       return 1;
+ }
+ // serialize_roundtrip(value)
+ // Meant for unit testing the packer from Lua
+ int ModApiServer::l_serialize_roundtrip(lua_State *L)
+ {
+       NO_MAP_LOCK_REQUIRED;
+       int top = lua_gettop(L);
+       auto *pv = script_pack(L, 1);
+       if (top != lua_gettop(L))
+               throw LuaError("stack values leaked");
+ #ifndef NDEBUG
+       script_dump_packed(pv);
+ #endif
+       top = lua_gettop(L);
+       script_unpack(L, pv);
+       delete pv;
+       if (top + 1 != lua_gettop(L))
+               throw LuaError("stack values leaked");
+       return 1;
+ }
  void ModApiServer::Initialize(lua_State *L, int top)
  {
        API_FCT(request_shutdown);
        API_FCT(get_server_status);
        API_FCT(get_server_uptime);
+       API_FCT(get_server_max_lag);
        API_FCT(get_worldpath);
        API_FCT(is_singleplayer);
  
        API_FCT(get_ban_list);
        API_FCT(get_ban_description);
        API_FCT(ban_player);
-       API_FCT(kick_player);
+       API_FCT(disconnect_player);
+       API_FCT(remove_player);
        API_FCT(unban_player_or_ip);
        API_FCT(notify_authentication_modified);
+       API_FCT(do_async_callback);
+       API_FCT(register_async_dofile);
+       API_FCT(serialize_roundtrip);
+ }
+ void ModApiServer::InitializeAsync(lua_State *L, int top)
+ {
+       API_FCT(get_worldpath);
+       API_FCT(is_singleplayer);
+       API_FCT(get_current_modname);
+       API_FCT(get_modpath);
+       API_FCT(get_modnames);
  }
index d575eb6037017e7a6ea0d73501fc58d1fb548922,97068ce4c36c9b1a8a2cdb6683e4c2785b5defe9..fa749c2e5104ce6174bc7f854e05164d3e94d5ef
@@@ -41,7 -41,6 +41,6 @@@ with this program; if not, write to th
  #include "util/hex.h"
  #include "util/sha1.h"
  #include "util/png.h"
- #include <algorithm>
  #include <cstdio>
  
  // log([level,] text)
@@@ -160,28 -159,33 +159,33 @@@ int ModApiUtil::l_write_json(lua_State 
        return 1;
  }
  
- // get_dig_params(groups, tool_capabilities)
+ // get_dig_params(groups, tool_capabilities[, wear])
  int ModApiUtil::l_get_dig_params(lua_State *L)
  {
        NO_MAP_LOCK_REQUIRED;
        ItemGroupList groups;
        read_groups(L, 1, groups);
        ToolCapabilities tp = read_tool_capabilities(L, 2);
-       push_dig_params(L, getDigParams(groups, &tp));
+       if (lua_isnoneornil(L, 3)) {
+               push_dig_params(L, getDigParams(groups, &tp));
+       } else {
+               u16 wear = readParam<int>(L, 3);
+               push_dig_params(L, getDigParams(groups, &tp, wear));
+       }
        return 1;
  }
  
- // get_hit_params(groups, tool_capabilities[, time_from_last_punch])
+ // get_hit_params(groups, tool_capabilities[, time_from_last_punch, [, wear]])
  int ModApiUtil::l_get_hit_params(lua_State *L)
  {
        NO_MAP_LOCK_REQUIRED;
        std::unordered_map<std::string, int> groups;
        read_groups(L, 1, groups);
        ToolCapabilities tp = read_tool_capabilities(L, 2);
-       if(lua_isnoneornil(L, 3))
-               push_hit_params(L, getHitParams(groups, &tp));
-       else
-               push_hit_params(L, getHitParams(groups, &tp, readParam<float>(L, 3)));
+       float time_from_last_punch = readParam<float>(L, 3, 1000000);
+       int wear = readParam<int>(L, 4, 0);
+       push_hit_params(L, getHitParams(groups, &tp,
+               time_from_last_punch, wear));
        return 1;
  }
  
@@@ -344,6 -348,49 +348,49 @@@ int ModApiUtil::l_mkdir(lua_State *L
        return 1;
  }
  
+ // rmdir(path, recursive)
+ int ModApiUtil::l_rmdir(lua_State *L)
+ {
+       NO_MAP_LOCK_REQUIRED;
+       const char *path = luaL_checkstring(L, 1);
+       CHECK_SECURE_PATH(L, path, true);
+       bool recursive = readParam<bool>(L, 2, false);
+       if (recursive)
+               lua_pushboolean(L, fs::RecursiveDelete(path));
+       else
+               lua_pushboolean(L, fs::DeleteSingleFileOrEmptyDirectory(path));
+       return 1;
+ }
+ // cpdir(source, destination)
+ int ModApiUtil::l_cpdir(lua_State *L)
+ {
+       NO_MAP_LOCK_REQUIRED;
+       const char *source = luaL_checkstring(L, 1);
+       const char *destination = luaL_checkstring(L, 2);
+       CHECK_SECURE_PATH(L, source, false);
+       CHECK_SECURE_PATH(L, destination, true);
+       lua_pushboolean(L, fs::CopyDir(source, destination));
+       return 1;
+ }
+ // mpdir(source, destination)
+ int ModApiUtil::l_mvdir(lua_State *L)
+ {
+       NO_MAP_LOCK_REQUIRED;
+       const char *source = luaL_checkstring(L, 1);
+       const char *destination = luaL_checkstring(L, 2);
+       CHECK_SECURE_PATH(L, source, true);
+       CHECK_SECURE_PATH(L, destination, true);
+       lua_pushboolean(L, fs::MoveDir(source, destination));
+       return 1;
+ }
  // get_dir_list(path, is_dir)
  int ModApiUtil::l_get_dir_list(lua_State *L)
  {
@@@ -396,36 -443,7 +443,7 @@@ int ModApiUtil::l_request_insecure_envi
                return 1;
        }
  
-       // We have to make sure that this function is being called directly by
-       // a mod, otherwise a malicious mod could override this function and
-       // steal its return value.
-       lua_Debug info;
-       // Make sure there's only one item below this function on the stack...
-       if (lua_getstack(L, 2, &info)) {
-               return 0;
-       }
-       FATAL_ERROR_IF(!lua_getstack(L, 1, &info), "lua_getstack() failed");
-       FATAL_ERROR_IF(!lua_getinfo(L, "S", &info), "lua_getinfo() failed");
-       // ...and that that item is the main file scope.
-       if (strcmp(info.what, "main") != 0) {
-               return 0;
-       }
-       // Get mod name
-       lua_rawgeti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_CURRENT_MOD_NAME);
-       if (!lua_isstring(L, -1)) {
-               return 0;
-       }
-       // Check secure.trusted_mods
-       std::string mod_name = readParam<std::string>(L, -1);
-       std::string trusted_mods = g_settings->get("secure.trusted_mods");
-       trusted_mods.erase(std::remove_if(trusted_mods.begin(),
-                       trusted_mods.end(), static_cast<int(*)(int)>(&std::isspace)),
-                       trusted_mods.end());
-       std::vector<std::string> mod_list = str_split(trusted_mods, ',');
-       if (std::find(mod_list.begin(), mod_list.end(), mod_name) ==
-                       mod_list.end()) {
+       if (!ScriptApiSecurity::checkWhitelisted(L, "secure.trusted_mods")) {
                return 0;
        }
  
@@@ -583,6 -601,9 +601,9 @@@ void ModApiUtil::Initialize(lua_State *
        API_FCT(decompress);
  
        API_FCT(mkdir);
+       API_FCT(rmdir);
+       API_FCT(cpdir);
+       API_FCT(mvdir);
        API_FCT(get_dir_list);
        API_FCT(safe_file_write);
  
@@@ -619,8 -640,6 +640,8 @@@ void ModApiUtil::InitializeClient(lua_S
        API_FCT(compress);
        API_FCT(decompress);
  
 +      API_FCT(request_insecure_environment);
 +
        API_FCT(encode_base64);
        API_FCT(decode_base64);
  
        API_FCT(sha1);
        API_FCT(colorspec_to_colorstring);
        API_FCT(colorspec_to_bytes);
 +
 +      LuaSettings::create(L, g_settings, g_settings_path);
 +      lua_setfield(L, top, "settings");
  }
  
  void ModApiUtil::InitializeAsync(lua_State *L, int top)
        API_FCT(decompress);
  
        API_FCT(mkdir);
+       API_FCT(rmdir);
+       API_FCT(cpdir);
+       API_FCT(mvdir);
        API_FCT(get_dir_list);
+       API_FCT(safe_file_write);
+       API_FCT(request_insecure_environment);
  
        API_FCT(encode_base64);
        API_FCT(decode_base64);
        API_FCT(colorspec_to_colorstring);
        API_FCT(colorspec_to_bytes);
  
+       API_FCT(encode_png);
        API_FCT(get_last_run_mod);
        API_FCT(set_last_run_mod);
  
index b8ba25235ef330dd1dca98aa49c4ca4b3ee5f992,630c111c5a36c5252182db68b427645f82faf9a3..6a9001052e2008c3817b324f1b7d93c20cd2e67e
@@@ -393,7 -393,7 +393,7 @@@ static std::random_device seed
  
  ServerEnvironment::ServerEnvironment(ServerMap *map,
        ServerScripting *scriptIface, Server *server,
-       const std::string &path_world):
+       const std::string &path_world, MetricsBackend *mb):
        Environment(server),
        m_map(map),
        m_script(scriptIface),
  
        m_player_database = openPlayerDatabase(player_backend_name, path_world, conf);
        m_auth_database = openAuthDatabase(auth_backend_name, path_world, conf);
+       m_step_time_counter = mb->addCounter(
+               "minetest_env_step_time", "Time spent in environment step (in microseconds)");
+       m_active_block_gauge = mb->addGauge(
+               "minetest_env_active_blocks", "Number of active blocks");
+       m_active_object_gauge = mb->addGauge(
+               "minetest_env_active_objects", "Number of active objects");
  }
  
  ServerEnvironment::~ServerEnvironment()
@@@ -552,10 -561,8 +561,8 @@@ bool ServerEnvironment::removePlayerFro
  void ServerEnvironment::kickAllPlayers(AccessDeniedCode reason,
        const std::string &str_reason, bool reconnect)
  {
-       for (RemotePlayer *player : m_players) {
-               m_server->DenyAccessVerCompliant(player->getPeerId(),
-                       player->protocol_version, reason, str_reason, reconnect);
-       }
+       for (RemotePlayer *player : m_players)
+               m_server->DenyAccess(player->getPeerId(), reason, str_reason, reconnect);
  }
  
  void ServerEnvironment::saveLoadedPlayers(bool force)
@@@ -892,7 -899,7 +899,7 @@@ public
                        for (ActiveABM &aabm : *m_aabms[c]) {
                                if ((p.Y < aabm.min_y) || (p.Y > aabm.max_y))
                                        continue;
-                               
                                if (myrand() % aabm.chance != 0)
                                        continue;
  
@@@ -1178,7 -1185,7 +1185,7 @@@ void ServerEnvironment::clearObjects(Cl
                // Tell the object about removal
                obj->removingFromEnvironment();
                // Deregister in scripting api
 -              m_script->removeObjectReference(obj);
 +              m_script->removeObjectReference(dynamic_cast<ActiveObject *>(obj));
  
                // Delete active object
                if (obj->environmentDeletes())
  void ServerEnvironment::step(float dtime)
  {
        ScopeProfiler sp2(g_profiler, "ServerEnv::step()", SPT_AVG);
+       const auto start_time = porting::getTimeUs();
        /* Step time of day */
        stepTimeOfDay(dtime);
  
                m_active_blocks.update(players, active_block_range, active_object_range,
                        blocks_removed, blocks_added);
  
+               m_active_block_gauge->set(m_active_blocks.size());
                /*
                        Handle removed blocks
                */
        */
        m_script->environment_Step(dtime);
  
+       m_script->stepAsync();
        /*
                Step active objects
        */
                        send_recommended = true;
                }
  
-               auto cb_state = [this, dtime, send_recommended] (ServerActiveObject *obj) {
+               u32 object_count = 0;
+               auto cb_state = [&] (ServerActiveObject *obj) {
                        if (obj->isGone())
                                return;
+                       object_count++;
  
                        // Step object
                        obj->step(dtime, send_recommended);
                        obj->dumpAOMessagesToQueue(m_active_object_messages);
                };
                m_ao_manager.step(dtime, cb_state);
+               m_active_object_gauge->set(object_count);
        }
  
        /*
  
        // Send outdated detached inventories
        m_server->sendDetachedInventories(PEER_ID_INEXISTENT, true);
+       const auto end_time = porting::getTimeUs();
+       m_step_time_counter->increment(end_time - start_time);
  }
  
  ServerEnvironment::BlockStatus ServerEnvironment::getBlockStatus(v3s16 blockpos)
@@@ -1759,7 -1780,7 +1780,7 @@@ u16 ServerEnvironment::addActiveObjectR
        }
  
        // Register reference in scripting api (must be done before post-init)
 -      m_script->addObjectReference(object);
 +      m_script->addObjectReference(dynamic_cast<ActiveObject *>(object));
        // Post-initialize object
        object->addedToEnvironment(dtime_s);
  
@@@ -1849,7 -1870,7 +1870,7 @@@ void ServerEnvironment::removeRemovedOb
                // Tell the object about removal
                obj->removingFromEnvironment();
                // Deregister in scripting api
 -              m_script->removeObjectReference(obj);
 +              m_script->removeObjectReference(dynamic_cast<ActiveObject *>(obj));
  
                // Delete
                if (obj->environmentDeletes())
@@@ -2114,7 -2135,7 +2135,7 @@@ void ServerEnvironment::deactivateFarOb
                // Tell the object about removal
                obj->removingFromEnvironment();
                // Deregister in scripting api
 -              m_script->removeObjectReference(obj);
 +              m_script->removeObjectReference(dynamic_cast<ActiveObject *>(obj));
  
                // Delete active object
                if (obj->environmentDeletes())
diff --combined src/unittest/test.cpp
index d4841d5593598f8c6b941f5e5486930f0546ce02,f223d567ed4c5d557cb497dd0021be9f760fe9d4..4fd4b930b65f148bd77ffb46f025e6829b87760f
@@@ -25,6 -25,7 +25,7 @@@ with this program; if not, write to th
  #include "gamedef.h"
  #include "modchannels.h"
  #include "content/mods.h"
+ #include "database/database-dummy.h"
  #include "util/numeric.h"
  #include "porting.h"
  
@@@ -47,9 -48,7 +48,9 @@@ public
        ~TestGameDef();
  
        IItemDefManager *getItemDefManager() { return m_itemdef; }
 +      IWritableItemDefManager *getWritableItemDefManager() { return m_itemdef; }
        const NodeDefManager *getNodeDefManager() { return m_nodedef; }
 +      NodeDefManager *getWritableNodeDefManager() { return m_nodedef; }
        ICraftDefManager *getCraftDefManager() { return m_craftdef; }
        ITextureSource *getTextureSource() { return m_texturesrc; }
        IShaderSource *getShaderSource() { return m_shadersrc; }
@@@ -57,6 -56,7 +58,7 @@@
        scene::ISceneManager *getSceneManager() { return m_scenemgr; }
        IRollbackManager *getRollbackManager() { return m_rollbackmgr; }
        EmergeManager *getEmergeManager() { return m_emergemgr; }
+       ModMetadataDatabase *getModStorageDatabase() { return m_mod_storage_database; }
  
        scene::IAnimatedMesh *getMesh(const std::string &filename) { return NULL; }
        bool checkLocalPrivilege(const std::string &priv) { return false; }
@@@ -70,7 -70,6 +72,6 @@@
                return testmodspec;
        }
        virtual const ModSpec* getModSpec(const std::string &modname) const { return NULL; }
-       virtual std::string getModStoragePath() const { return "."; }
        virtual bool registerModStorage(ModMetadata *meta) { return true; }
        virtual void unregisterModStorage(const std::string &name) {}
        bool joinModChannel(const std::string &channel);
@@@ -82,8 -81,8 +83,8 @@@
        }
  
  private:
 -      IItemDefManager *m_itemdef = nullptr;
 -      const NodeDefManager *m_nodedef = nullptr;
 +      IWritableItemDefManager *m_itemdef = nullptr;
 +      NodeDefManager *m_nodedef = nullptr;
        ICraftDefManager *m_craftdef = nullptr;
        ITextureSource *m_texturesrc = nullptr;
        IShaderSource *m_shadersrc = nullptr;
        scene::ISceneManager *m_scenemgr = nullptr;
        IRollbackManager *m_rollbackmgr = nullptr;
        EmergeManager *m_emergemgr = nullptr;
+       ModMetadataDatabase *m_mod_storage_database = nullptr;
        std::unique_ptr<ModChannelMgr> m_modchannel_mgr;
  };
  
  
  TestGameDef::TestGameDef() :
+       m_mod_storage_database(new Database_Dummy()),
        m_modchannel_mgr(new ModChannelMgr())
  {
        m_itemdef = createItemDefManager();
@@@ -109,6 -110,7 +112,7 @@@ TestGameDef::~TestGameDef(
  {
        delete m_itemdef;
        delete m_nodedef;
+       delete m_mod_storage_database;
  }
  
  
index bfa1e4a09c99634a0b66117475903213433ab228,e0c431711614b808cb0de14bbca271b43d254dd4..4484b24a79de820b1d7d478e3ba79a002a374452
@@@ -1,12 -1,12 +1,12 @@@
  #!/bin/bash
  set -e
  
 -CORE_GIT=https://github.com/minetest/minetest
 +CORE_GIT=https://github.com/EliasFleckenstein03/dragonfireclient
  CORE_BRANCH=master
 -CORE_NAME=minetest
 -GAME_GIT=https://github.com/minetest/minetest_game
 +CORE_NAME=dragonfireclient
 +GAME_GIT=https://git.minetest.land/MineClone2/MineClone2
  GAME_BRANCH=master
 -GAME_NAME=minetest_game
 +GAME_NAME=MineClone2
  
  dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
  if [ $# -ne 1 ]; then
@@@ -19,28 -19,44 +19,44 @@@ builddir="$( cd "$builddir" && pwd )
  libdir=$builddir/libs
  
  # Test which win32 compiler is present
which i686-w64-mingw32-gcc &>/dev/null &&
-       toolchain_file=$dir/toolchain_i686-w64-mingw32.cmake
which i686-w64-mingw32-gcc-posix &>/dev/null &&
-       toolchain_file=$dir/toolchain_i686-w64-mingw32-posix.cmake
command -v i686-w64-mingw32-gcc >/dev/null &&
+       compiler=i686-w64-mingw32-gcc
command -v i686-w64-mingw32-gcc-posix >/dev/null &&
+       compiler=i686-w64-mingw32-gcc-posix
  
- if [ -z "$toolchain_file" ]; then
-       echo "Unable to determine which mingw32 compiler to use"
+ if [ -z "$compiler" ]; then
+       echo "Unable to determine which MinGW compiler to use"
        exit 1
  fi
+ toolchain_file=$dir/toolchain_${compiler/-gcc/}.cmake
  echo "Using $toolchain_file"
  
- irrlicht_version=1.9.0mt3
- ogg_version=1.3.4
+ # Try to find runtime DLLs in various paths (varies by distribution, sigh)
+ tmp=$(dirname "$(command -v $compiler)")/..
+ runtime_dlls=
+ for name in lib{gcc_,stdc++-,winpthread-}'*'.dll; do
+       for dir in $tmp/i686-w64-mingw32/{bin,lib} $tmp/lib/gcc/i686-w64-mingw32/*; do
+               [ -d "$dir" ] || continue
+               file=$(echo $dir/$name)
+               [ -f "$file" ] && { runtime_dlls+="$file;"; break; }
+       done
+ done
+ [ -z "$runtime_dlls" ] &&
+       echo "The compiler runtime DLLs could not be found, they might be missing in the final package."
+ # Get stuff
+ irrlicht_version=1.9.0mt5
+ ogg_version=1.3.5
+ openal_version=1.21.1
  vorbis_version=1.3.7
- curl_version=7.76.1
+ curl_version=7.81.0
  gettext_version=0.20.1
- freetype_version=2.10.4
- sqlite3_version=3.35.5
+ freetype_version=2.11.1
+ sqlite3_version=3.37.2
  luajit_version=2.1.0-beta3
  leveldb_version=1.23
  zlib_version=1.2.11
- zstd_version=1.4.9
+ zstd_version=1.5.2
  
  mkdir -p $libdir
  
@@@ -63,29 -79,33 +79,33 @@@ download () 
        fi
  }
  
- # Get stuff
+ # 'dw2' just points to rebuilt versions after a toolchain change
+ # this distinction should be gotten rid of next time
  cd $libdir
  download "https://github.com/minetest/irrlicht/releases/download/$irrlicht_version/win32.zip" irrlicht-$irrlicht_version.zip
- download "http://minetest.kitsunemimi.pw/zlib-$zlib_version-win32.zip"
+ download "http://minetest.kitsunemimi.pw/dw2/zlib-$zlib_version-win32.zip"
  download "http://minetest.kitsunemimi.pw/zstd-$zstd_version-win32.zip"
  download "http://minetest.kitsunemimi.pw/libogg-$ogg_version-win32.zip"
- download "http://minetest.kitsunemimi.pw/libvorbis-$vorbis_version-win32.zip"
+ download "http://minetest.kitsunemimi.pw/dw2/libvorbis-$vorbis_version-win32.zip"
  download "http://minetest.kitsunemimi.pw/curl-$curl_version-win32.zip"
- download "http://minetest.kitsunemimi.pw/gettext-$gettext_version-win32.zip"
+ download "http://minetest.kitsunemimi.pw/dw2/gettext-$gettext_version-win32.zip"
  download "http://minetest.kitsunemimi.pw/freetype2-$freetype_version-win32.zip" freetype-$freetype_version.zip
  download "http://minetest.kitsunemimi.pw/sqlite3-$sqlite3_version-win32.zip"
- download "http://minetest.kitsunemimi.pw/luajit-$luajit_version-win32.zip"
- download "http://minetest.kitsunemimi.pw/libleveldb-$leveldb_version-win32.zip" leveldb-$leveldb_version.zip
- download "http://minetest.kitsunemimi.pw/openal_stripped.zip" '' unzip_nofolder
+ download "http://minetest.kitsunemimi.pw/dw2/luajit-$luajit_version-win32.zip"
+ download "http://minetest.kitsunemimi.pw/dw2/libleveldb-$leveldb_version-win32.zip" leveldb-$leveldb_version.zip
+ download "http://minetest.kitsunemimi.pw/openal-soft-$openal_version-win32.zip"
  
  # Set source dir, downloading Minetest as needed
  if [ -n "$EXISTING_MINETEST_DIR" ]; then
        sourcedir="$( cd "$EXISTING_MINETEST_DIR" && pwd )"
  else
+       cd $builddir
        sourcedir=$PWD/$CORE_NAME
        [ -d $CORE_NAME ] && { pushd $CORE_NAME; git pull; popd; } || \
                git clone -b $CORE_BRANCH $CORE_GIT $CORE_NAME
        if [ -z "$NO_MINETEST_GAME" ]; then
+               cd $sourcedir
                [ -d games/$GAME_NAME ] && { pushd games/$GAME_NAME; git pull; popd; } || \
                        git clone -b $GAME_BRANCH $GAME_GIT games/$GAME_NAME
        fi
@@@ -96,23 -116,21 +116,21 @@@ git_hash=$(cd $sourcedir && git rev-par
  # Build the thing
  cd $builddir
  [ -d build ] && rm -rf build
- mkdir build
- cd build
  
  irr_dlls=$(echo $libdir/irrlicht/lib/*.dll | tr ' ' ';')
  vorbis_dlls=$(echo $libdir/libvorbis/bin/libvorbis{,file}-*.dll | tr ' ' ';')
  gettext_dlls=$(echo $libdir/gettext/bin/lib{intl,iconv}-*.dll | tr ' ' ';')
  
- cmake -S $sourcedir -B . \
+ cmake -S $sourcedir -B build \
        -DCMAKE_TOOLCHAIN_FILE=$toolchain_file \
        -DCMAKE_INSTALL_PREFIX=/tmp \
        -DVERSION_EXTRA=$git_hash \
        -DBUILD_CLIENT=1 -DBUILD_SERVER=0 \
+       -DEXTRA_DLL="$runtime_dlls" \
        \
        -DENABLE_SOUND=1 \
        -DENABLE_CURL=1 \
        -DENABLE_GETTEXT=1 \
-       -DENABLE_FREETYPE=1 \
        -DENABLE_LEVELDB=1 \
        \
        -DCMAKE_PREFIX_PATH=$libdir/irrlicht \
        -DVORBIS_DLL="$vorbis_dlls" \
        -DVORBISFILE_LIBRARY=$libdir/libvorbis/lib/libvorbisfile.dll.a \
        \
-       -DOPENAL_INCLUDE_DIR=$libdir/openal_stripped/include/AL \
-       -DOPENAL_LIBRARY=$libdir/openal_stripped/lib/libOpenAL32.dll.a \
-       -DOPENAL_DLL=$libdir/openal_stripped/bin/OpenAL32.dll \
+       -DOPENAL_INCLUDE_DIR=$libdir/openal/include/AL \
+       -DOPENAL_LIBRARY=$libdir/openal/lib/libOpenAL32.dll.a \
+       -DOPENAL_DLL=$libdir/openal/bin/OpenAL32.dll \
        \
        -DCURL_DLL=$libdir/curl/bin/libcurl-4.dll \
        -DCURL_INCLUDE_DIR=$libdir/curl/include \
        -DCURL_LIBRARY=$libdir/curl/lib/libcurl.dll.a \
        \
-       -DGETTEXT_MSGFMT=`which msgfmt` \
+       -DGETTEXT_MSGFMT=`command -v msgfmt` \
        -DGETTEXT_DLL="$gettext_dlls" \
        -DGETTEXT_INCLUDE_DIR=$libdir/gettext/include \
        -DGETTEXT_LIBRARY=$libdir/gettext/lib/libintl.dll.a \
        -DLEVELDB_LIBRARY=$libdir/leveldb/lib/libleveldb.dll.a \
        -DLEVELDB_DLL=$libdir/leveldb/bin/libleveldb.dll
  
make -j$(nproc)
cmake --build build -j$(nproc)
  
- [ -z "$NO_PACKAGE" ] && make package
+ [ -z "$NO_PACKAGE" ] && cmake --build build --target package
  
  exit 0
  # EOF
index 5acb43b73317067e2762e6b46e79709ad2df4480,e79397be4088048527c230dcafb18bda3d2c86d2..f367e68a538c1787ed708366af8470f8fe1412bf
@@@ -1,12 -1,12 +1,12 @@@
  #!/bin/bash
  set -e
  
 -CORE_GIT=https://github.com/minetest/minetest
 +CORE_GIT=https://github.com/EliasFleckenstein03/dragonfireclient
  CORE_BRANCH=master
 -CORE_NAME=minetest
 -GAME_GIT=https://github.com/minetest/minetest_game
 +CORE_NAME=dragonfireclient
 +GAME_GIT=https://git.minetest.land/MineClone2/MineClone2
  GAME_BRANCH=master
 -GAME_NAME=minetest_game
 +GAME_NAME=MineClone2
  
  dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
  if [ $# -ne 1 ]; then
@@@ -19,28 -19,44 +19,44 @@@ builddir="$( cd "$builddir" && pwd )
  libdir=$builddir/libs
  
  # Test which win64 compiler is present
which x86_64-w64-mingw32-gcc &>/dev/null &&
-       toolchain_file=$dir/toolchain_x86_64-w64-mingw32.cmake
which x86_64-w64-mingw32-gcc-posix &>/dev/null &&
-       toolchain_file=$dir/toolchain_x86_64-w64-mingw32-posix.cmake
command -v x86_64-w64-mingw32-gcc >/dev/null &&
+       compiler=x86_64-w64-mingw32-gcc
command -v x86_64-w64-mingw32-gcc-posix >/dev/null &&
+       compiler=x86_64-w64-mingw32-gcc-posix
  
- if [ -z "$toolchain_file" ]; then
-       echo "Unable to determine which mingw32 compiler to use"
+ if [ -z "$compiler" ]; then
+       echo "Unable to determine which MinGW compiler to use"
        exit 1
  fi
+ toolchain_file=$dir/toolchain_${compiler/-gcc/}.cmake
  echo "Using $toolchain_file"
  
- irrlicht_version=1.9.0mt3
- ogg_version=1.3.4
+ # Try to find runtime DLLs in various paths (varies by distribution, sigh)
+ tmp=$(dirname "$(command -v $compiler)")/..
+ runtime_dlls=
+ for name in lib{gcc_,stdc++-,winpthread-}'*'.dll; do
+       for dir in $tmp/x86_64-w64-mingw32/{bin,lib} $tmp/lib/gcc/x86_64-w64-mingw32/*; do
+               [ -d "$dir" ] || continue
+               file=$(echo $dir/$name)
+               [ -f "$file" ] && { runtime_dlls+="$file;"; break; }
+       done
+ done
+ [ -z "$runtime_dlls" ] &&
+       echo "The compiler runtime DLLs could not be found, they might be missing in the final package."
+ # Get stuff
+ irrlicht_version=1.9.0mt5
+ ogg_version=1.3.5
+ openal_version=1.21.1
  vorbis_version=1.3.7
- curl_version=7.76.1
+ curl_version=7.81.0
  gettext_version=0.20.1
- freetype_version=2.10.4
- sqlite3_version=3.35.5
+ freetype_version=2.11.1
+ sqlite3_version=3.37.2
  luajit_version=2.1.0-beta3
  leveldb_version=1.23
  zlib_version=1.2.11
- zstd_version=1.4.9
+ zstd_version=1.5.2
  
  mkdir -p $libdir
  
@@@ -63,7 -79,6 +79,6 @@@ download () 
        fi
  }
  
- # Get stuff
  cd $libdir
  download "https://github.com/minetest/irrlicht/releases/download/$irrlicht_version/win64.zip" irrlicht-$irrlicht_version.zip
  download "http://minetest.kitsunemimi.pw/zlib-$zlib_version-win64.zip"
@@@ -76,16 -91,18 +91,18 @@@ download "http://minetest.kitsunemimi.p
  download "http://minetest.kitsunemimi.pw/sqlite3-$sqlite3_version-win64.zip"
  download "http://minetest.kitsunemimi.pw/luajit-$luajit_version-win64.zip"
  download "http://minetest.kitsunemimi.pw/libleveldb-$leveldb_version-win64.zip" leveldb-$leveldb_version.zip
- download "http://minetest.kitsunemimi.pw/openal_stripped64.zip" 'openal_stripped.zip' unzip_nofolder
+ download "http://minetest.kitsunemimi.pw/openal-soft-$openal_version-win64.zip"
  
  # Set source dir, downloading Minetest as needed
  if [ -n "$EXISTING_MINETEST_DIR" ]; then
        sourcedir="$( cd "$EXISTING_MINETEST_DIR" && pwd )"
  else
+       cd $builddir
        sourcedir=$PWD/$CORE_NAME
        [ -d $CORE_NAME ] && { pushd $CORE_NAME; git pull; popd; } || \
                git clone -b $CORE_BRANCH $CORE_GIT $CORE_NAME
        if [ -z "$NO_MINETEST_GAME" ]; then
+               cd $sourcedir
                [ -d games/$GAME_NAME ] && { pushd games/$GAME_NAME; git pull; popd; } || \
                        git clone -b $GAME_BRANCH $GAME_GIT games/$GAME_NAME
        fi
@@@ -96,23 -113,21 +113,21 @@@ git_hash=$(cd $sourcedir && git rev-par
  # Build the thing
  cd $builddir
  [ -d build ] && rm -rf build
- mkdir build
- cd build
  
  irr_dlls=$(echo $libdir/irrlicht/lib/*.dll | tr ' ' ';')
  vorbis_dlls=$(echo $libdir/libvorbis/bin/libvorbis{,file}-*.dll | tr ' ' ';')
  gettext_dlls=$(echo $libdir/gettext/bin/lib{intl,iconv}-*.dll | tr ' ' ';')
  
- cmake -S $sourcedir -B . \
+ cmake -S $sourcedir -B build \
        -DCMAKE_TOOLCHAIN_FILE=$toolchain_file \
        -DCMAKE_INSTALL_PREFIX=/tmp \
        -DVERSION_EXTRA=$git_hash \
        -DBUILD_CLIENT=1 -DBUILD_SERVER=0 \
+       -DEXTRA_DLL="$runtime_dlls" \
        \
        -DENABLE_SOUND=1 \
        -DENABLE_CURL=1 \
        -DENABLE_GETTEXT=1 \
-       -DENABLE_FREETYPE=1 \
        -DENABLE_LEVELDB=1 \
        \
        -DCMAKE_PREFIX_PATH=$libdir/irrlicht \
        -DVORBIS_DLL="$vorbis_dlls" \
        -DVORBISFILE_LIBRARY=$libdir/libvorbis/lib/libvorbisfile.dll.a \
        \
-       -DOPENAL_INCLUDE_DIR=$libdir/openal_stripped/include/AL \
-       -DOPENAL_LIBRARY=$libdir/openal_stripped/lib/libOpenAL32.dll.a \
-       -DOPENAL_DLL=$libdir/openal_stripped/bin/OpenAL32.dll \
+       -DOPENAL_INCLUDE_DIR=$libdir/openal/include/AL \
+       -DOPENAL_LIBRARY=$libdir/openal/lib/libOpenAL32.dll.a \
+       -DOPENAL_DLL=$libdir/openal/bin/OpenAL32.dll \
        \
        -DCURL_DLL=$libdir/curl/bin/libcurl-4.dll \
        -DCURL_INCLUDE_DIR=$libdir/curl/include \
        -DCURL_LIBRARY=$libdir/curl/lib/libcurl.dll.a \
        \
-       -DGETTEXT_MSGFMT=`which msgfmt` \
+       -DGETTEXT_MSGFMT=`command -v msgfmt` \
        -DGETTEXT_DLL="$gettext_dlls" \
        -DGETTEXT_INCLUDE_DIR=$libdir/gettext/include \
        -DGETTEXT_LIBRARY=$libdir/gettext/lib/libintl.dll.a \
        -DLEVELDB_LIBRARY=$libdir/leveldb/lib/libleveldb.dll.a \
        -DLEVELDB_DLL=$libdir/leveldb/bin/libleveldb.dll
  
make -j$(nproc)
cmake --build build -j$(nproc)
  
- [ -z "$NO_PACKAGE" ] && make package
+ [ -z "$NO_PACKAGE" ] && cmake --build build --target package
  
  exit 0
  # EOF